Commit Graph

501 Commits

Author SHA1 Message Date
Luca fd46c171ce chore(branding): move Customer dashboard card between Company Info and Gallery Theme
Keeps the customer-surface branding toggles adjacent to the other
brand-visibility controls instead of floating at the bottom of the
page, where they were easy to miss.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-11 01:59:30 +02:00
Luca b252cb67eb feat(branding): Customer dashboard header toggles in Branding page
Adds back the "Show logo" / "Show company name" toggles for the
customer dashboard, scoped to /customer/* surfaces only. Lives as a
dedicated card at the bottom of Settings → Branding, gated by the
customerPortal feature flag so admins who haven't enabled the portal
don't see it.

* Backend: restored GET/PUT /admin/settings/customer-surface
  endpoints, whitelisted only to the two branding keys
  (customer_show_logo, customer_show_company_name). The
  calendar/quotes/bills feature globals that used to live on this
  endpoint are now driven by the Features tab (feature_flags table).
* customerAccountsService.getCustomerSurfaceGlobals() reads from
  app_settings again so /api/customer/auth/session honours the
  toggles in its branding payload.
* New CustomerDashboardBrandingCard component with its own save
  flow — separate from the main BrandingPage payload so flipping a
  toggle doesn't replay the full branding mutation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-11 01:52:27 +02:00
Luca da08a5828a fix(customer): unwrap /customer/* from RequireFeature gate
RequireFeature calls useFeatureFlags(), which throws unless mounted
inside FeatureFlagsProvider — and that provider only wraps
AdminLayout. So unauthenticated visitors hitting /customer/login
crashed into the React error boundary with 'Oops! Something went
wrong'.

The customerPortal flag continues to hide every admin-side surface
(sidebar entry, /admin/customers routes, CustomerAccountPicker on
event forms), which is what the flag is actually for. The
customer-side tree stays reachable so existing customers can still
log in even if the admin flips the flag off temporarily.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-11 01:30:19 +02:00
Luca 087ef45942 feat(customers): customer portal (#354) on top of feature-flags reorg
Implements the recurring-customer login surface from
the-luap/picpeak#354 plugged into the maintainer's
new feature-flag infrastructure (PR #443) instead of
a parallel toggle.

* New `customerPortal` feature flag (foundation flag for the
  not-yet-built calendar/quotes/bills/messaging customer
  surfaces). Defaults FALSE on fresh installs, TRUE on existing
  installs (events > 0) via migration 095 so live customer
  accounts don't disappear mid-deployment.
* Foundation schema: customer_accounts, customer_invitations,
  event_customer_assignments, customer_password_resets, plus
  RBAC permissions customers.view / .create / .delete granted
  to super_admin + admin system roles.
* Backend: /api/admin/customers (invite, list, search, assign,
  deactivate, reset password) + /api/customer/auth/* +
  /api/customer/* (login, dashboard, accept-invite, reset).
  Customer JWT bypass minted via
  /api/customer/events/:slug/access-token so existing gallery
  middleware stays untouched.
* Frontend: /customer/* route tree gated by RequireFeature flag
  customerPortal, with login / dashboard / accept-invite /
  reset pages and a customer-side sidebar layout.
  /admin/customers and /admin/customers/:id gated identically.
* Settings → Features grows a "Customers" section with a
  Customer portal card. The maintainer's Features tab stays the
  single source of truth — no parallel Advanced features tab.
* CustomerAccountPicker on event create/edit forms hides itself
  when the flag is off; backend ignores customer_account_ids in
  that case instead of erroring the whole event save.

Translations: en + de hand-translated. nl/pt/ru fall through to
en — flagged here as needing native review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-11 00:05:20 +02:00
github-actions[bot] 55e6395e9e chore(beta): release 3.45.1-beta.0 2026-05-10 20:09:09 +00:00
Paul Nothaft d62c529b02 fix(create-event): branding-default theme survives eventTypes refetch
The "apply recommended preset on event-type change" effect was firing on
the initial mount AND every time the eventTypes API resolved (because
availableEventTypes is recomputed when that query settles). The first
fire matched the wedding default and clobbered the global Branding
theme that the previous effect had just applied.

Track the previous event_type in a ref and bail out when it hasn't
actually changed. The Branding-default effect now wins on first paint,
and the recommended-preset behaviour still kicks in when the user
manually picks a different event type.

Restores the green state of smoke spec 07 (#323-B regression).
2026-05-10 22:02:53 +02:00
github-actions[bot] 421354bc5a chore(beta): release 3.45.0-beta.0 2026-05-10 19:57:54 +00:00
Paul Nothaft f3505c2631 Merge pull request #450 from the-luap/feat/footer-overhaul-441-440
feat(footer): hideable legal links + socials + promo banner (#441 + #440)
2026-05-10 21:57:35 +02:00
github-actions[bot] d2864854e7 chore(beta): release 3.44.2-beta.0 2026-05-10 19:51:03 +00:00
Paul Nothaft 3a731e7c95 feat(footer): hideable legal links + socials + promo banner (#441 + #440)
Combined footer overhaul:

- Per-CMS-page show_in_footer toggle (#441) — admins can hide
  Impressum / Datenschutz from the gallery footer when an external
  privacy / imprint URL is enough.
- Five social-media URL fields in branding settings (#441) — Facebook,
  Instagram, WhatsApp, X/Twitter, YouTube. Empty string hides each
  icon individually; the row is omitted when none are set.
- Promotional banner slot above or below the gallery footer (#440) —
  global default authored as markdown in branding settings, plus a
  three-way per-event override on the Edit Event form
  (inherit / custom / off). Backend nulls promo_markdown automatically
  when mode != 'custom' so stale text never persists.

Sanitization: marked with gfm/breaks → DOMPurify with a tight
allowlist (no img, no tables, no inline html). Post-process forces
target=_blank rel="noopener noreferrer nofollow" on every link so
admin-set URLs can't tab-nap the gallery context.

i18n covers all six locales (en/de/nl/pt/ru/fr).

Targets the beta branch.
2026-05-10 21:46:20 +02:00
Paul Nothaft 9c4a96fe97 fix(events): clamp page state when totalPages drops below current page (#442)
Bulk-deleting all events on the current page left the list empty until
manual reload. After the React Query refetch returned `events: []` with
a smaller `totalPages`, the page state was stuck on the old (now
out-of-range) page index — the backend correctly serves an empty page
for `page > totalPages`, but the UI had no logic to step back.

Add a useEffect that watches `data.pagination.totalPages` against the
current `page` and resets `page = max(1, totalPages)` whenever the
result count shrinks. Fires after every refetch so it covers bulk
delete, individual delete, archive, and any filter change that
shrinks the result set — same one-line guarantee.

Reported by @Rekoo-PS in #442.
2026-05-10 21:18:10 +02:00
github-actions[bot] 481f8c2f9f chore(beta): release 3.44.1-beta.0 2026-05-10 19:01:51 +00:00
Paul Nothaft e54456135c fix(events): admins can clear expiration on edit even when "Require expiration" is ON (#426)
iSchumi6210 reported that with the global "Require expiration date"
toggle ON, an admin couldn't clear the expiration on an existing event
via the Edit Event form. The PUT returned 400 "Expiration date is
required."

The cause was intentional in the original code: the global setting was
enforced on both create AND edit, so once flipped ON, no event could
ever be cleared of its expiration — not even by admins editing one-by-
one. Reproduced the exact scenario byte-for-byte against beta:

  Toggle ON → POST /admin/events {expiration_days: 30} → 200 created
  Toggle ON → PUT /admin/events/:id {expires_at: null} → 400 rejected

The setting now controls only the create-time default. On edit, an
admin can clear the field and the value persists as NULL ("never
expires"). Matches CMS-style admin tool conventions where field-
required-by-default doesn't lock the field after creation.

Backend: drop the `getEventFieldRequirements()` enforcement on the
expires_at branch in PUT /admin/events/:id. Empty/null on edit
normalizes to NULL.

Frontend: drop the matching `requireExpiration && !editForm.expires_at`
toast in EventDetailsPage. The variable is no longer referenced, so
remove its declaration too.

Verified end-to-end with toggle ON:
  STEP 1: create with expiration → ok (unchanged)
  STEP 2: create without expiration → backend auto-applies default 30d
          (create-time enforcement intact)
  STEP 3: PUT {expires_at: null} on existing → "Event updated
          successfully" (was 400)
  STEP 4: DB column expires_at is NULL
  STEP 5: PUT {expires_at: ''} also accepted (matches what an HTML
          date input sends when cleared)

Smoke 13/13 green; no regressions.
2026-05-10 20:54:36 +02:00
github-actions[bot] f6be07ed4b chore(beta): release 3.44.0-beta.0 2026-05-10 18:47:55 +00:00
Paul Nothaft 15e333681f feat(settings): Features tab + sidebar reorg with feature-flag gating
Reorganises the admin sidebar around what users actually do, and adds a
single Features page that gates which feature surfaces appear in the
nav. Shrinks the main sidebar from 11 items to 4-6 (depending on
feature flags) and groups configuration screens into a single Settings
home with six logical sections.

Why
---
The current sidebar mixes three concerns: workspaces (Dashboard, Events,
Archives), feature surfaces (Analytics, Users), and configuration
screens that get touched maybe once a month (Email Settings, Branding,
Event Types, Backup, CMS Pages). That's 11 items, half of them config.

Backend
-------
- New `feature_flags` table (key, value, updated_at, updated_by).
  Migration 088 detects existing-vs-fresh installs from the events
  table:
    * Existing install (events>0)  → all 9 flags TRUE so nothing
      vanishes from an admin's UI on upgrade.
    * Fresh install      (events=0) → spec defaults: galleries,
      reminderEmails, analytics, userManagement TRUE; calendar,
      calendarBooking, quotes, bills, messaging FALSE.

- New `/api/admin/feature-flags` (GET/PUT) under `settings.view` and
  `settings.edit`. Server enforces the same dependency rules the
  frontend does (galleries always TRUE, quotes=false → bills=false,
  calendar=false → calendarBooking=false). PUT writes one
  `feature_flags_updated` activity log row with the diff.

Frontend
--------
- `FeatureFlagsContext` provides `useFeatureFlags()` (with staged/save/
  reset/isDirty) and `useFeatureEnabled(key)`. Mounted inside
  AdminLayout so flag fetches carry the auth cookie. Source of truth
  is the server response; staged is a local copy that the Features tab
  edits and the Save button PUTs.

- `RequireFeature` route guard for /admin/analytics and /admin/users —
  redirects to /admin/dashboard when the corresponding flag is OFF.

- AdminSidebar dropped from 11 to 6 items. Removed: Email Settings,
  Branding, Event Types, Backup, CMS Pages (now Settings tabs).
  Feature-gated: Analytics, Users.

- Old top-level routes (/admin/email, /admin/branding, /admin/event-
  types, /admin/backup, /admin/cms) kept as <Navigate> redirects to
  /admin/settings?tab=<key> so existing bookmarks don't 404.

- SettingsPage rewritten with a 6-group inner-nav (General /
  Content & Appearance / Communication / Privacy & Security /
  Integrations / System) and 19 tabs. New Features tab is the
  default landing tab. URL ?tab=<key> roundtrips with state — deep
  links and the back button work.

- FeaturesTab renders 9 cards across 5 sections. Toggles enabled for
  Analytics + User Management (the two flags that gate sidebar items
  in this PR). All other toggles disabled with a "Not yet available"
  lockedReason — the cards still render so admins see the roadmap, but
  the flag has no UI effect until the surface ships in its own PR. The
  galleries card is locked TRUE per spec (foundation, can't be off).

- Live SidebarPreview reflects unsaved staged changes — admins see
  what their sidebar will look like before they save.

- New i18n keys across all 5 locales (en, de, nl, pt, ru) for the
  Features tab copy, the new Settings group labels, and the lifted
  tab titles.

Verified end-to-end
-------------------
- Migration on this dev DB (existing install, 977 events): all 9 flags
  set to TRUE.
- Migration on simulated fresh install (events table emptied): spec
  defaults applied (5 OFF, 4 ON).
- Backend round-trip: GET → PUT → audit-log entry written, dependency
  rule enforced (bills forced false when quotes=false even when bills=
  true requested).
- UI Playwright spec: sidebar dropped 5 items, old top-level routes
  redirect, Features tab is default, Galleries+Calendar+Quotes+Bills+
  Messaging+ReminderEmails toggles disabled, Analytics+Users toggles
  enabled, toggling Analytics off + saving updates the sidebar +
  redirects /admin/analytics to /admin/dashboard.
- Smoke 13/13 still green; no regressions on existing flows.
2026-05-10 20:36:32 +02:00
github-actions[bot] fab0fcdde3 chore(beta): release 3.43.3-beta.0 2026-05-09 19:00:52 +00:00
github-actions[bot] 197524a918 chore(beta): release 3.43.2-beta.0 2026-05-09 18:36:38 +00:00
github-actions[bot] 9ca9563dbe chore(beta): release 3.43.1-beta.0 2026-05-09 18:34:26 +00:00
github-actions[bot] 967ade7a9a chore(beta): release 3.43.0-beta.0 2026-05-09 18:22:30 +00:00
PiR1 46b99c6292 feat(localization): improve English translations for clarity and consistency 2026-05-09 19:13:45 +02:00
PiR1 2c1288583f feat(localization): add French translations for fit options in thumbnails 2026-05-09 19:03:30 +02:00
PiR1 5fc427c74b feat(localization): update thumbnail settings and add fit options translations 2026-05-09 18:42:15 +02:00
PiR1 e7228b0780 feat(localization): add i18next extraction helper & refactor backup configuration component to tsx 2026-05-09 18:23:12 +02:00
PiR1 86ee6c80aa feat(localization): add missing translations 2026-05-09 18:23:11 +02:00
PiR1 74e87b968b feat(localization): add i18next configuration and CLI commands for localization management 2026-05-09 18:23:11 +02:00
PiR1 a5db4bd46e feat(translations): add French language support and improve localization handling 2026-05-09 18:23:11 +02:00
github-actions[bot] 72a7a43a4a chore(beta): release 3.42.7-beta.0 2026-05-09 14:00:44 +00:00
github-actions[bot] 715a806991 chore(beta): release 3.42.6-beta.0 2026-05-09 13:59:36 +00:00
github-actions[bot] 08707eee9e chore(beta): release 3.42.5-beta.0 2026-05-08 08:15:24 +00:00
github-actions[bot] 9ade631e13 chore(beta): release 3.42.4-beta.0 2026-05-08 08:14:16 +00:00
Paul Nothaft 99e420b1b9 fix(events): typed-DELETE confirmation for bulk delete (#417)
The bulk-delete modal previously used a password input as a confirmation
gate, with an Enter-to-submit handler. Windows Hello / passkey flows
that target password fields were able to autofill and synthesise an
Enter keystroke, which submitted the form and triggered the destructive
delete without an explicit click on the red Delete button (Rekoo's
report in #417).

Replace the password gate with a GitHub-style typed-literal pattern:
the user types the literal "DELETE" (English, case-sensitive) into a
plain text input. The Delete button stays disabled until the input
matches, and there is no Enter-to-submit handler — only an explicit
click on the red button proceeds. Plain text inputs aren't subject to
password autofill or passkey ceremony so the auto-submit class of bug
is gone.

Server side, drop the bcrypt password verify on /admin/events/bulk-delete
and the related INVALID_PASSWORD response. The server's auth boundary
remains adminAuth + requirePermission('events.delete'); this matches
DELETE /admin/events/:id which has never required a re-entered password.
The client-side typed gate is the safeguard against accidental clicks.

i18n: drop password-related keys, add confirmLabel + confirmHelp across
en, de, nl, pt, ru. The literal "DELETE" stays English in all locales
to keep the gesture immune to translation drift and unambiguous.

Verified locally: typed-DELETE sanity spec covers the gate (wrong case
disabled, correct enables, Enter-on-input no-ops, click submits, events
deleted). Existing 03-bulk-archive smoke remains green.
2026-05-08 09:46:24 +02:00
github-actions[bot] e8df3de3fb chore(beta): release 3.42.3-beta.0 2026-05-07 21:04:20 +00:00
Paul Nothaft 401abf7a27 fix(create-event): re-apply Branding theme on stale→fresh settings (#323-B)
CreateEventPage's branding-default effect used a boolean ref guard that
locked in whichever theme_config arrived first. React Query can hand the
observer a cached (stale) copy on initial render and then push fresh data
once the network call resolves — the boolean ref meant the form kept the
stale theme and ignored the fresh one.

Replace the ref with a stringified-hash check: re-apply when the source
actually changes (including stale → fresh) but skip when nothing has.
User edits via the customizer aren't disturbed because settings.theme_config
only refreshes on a real Branding save, not on form state.

This unblocks the local pre-push smoke gate's 07-branding-default test,
which was test.fixme'd against this exact React Query staleness.
2026-05-07 22:42:16 +02:00
Paul Nothaft 6b6191a426 fix(security): scan triage cleanup — drop dead deps, harden Docker/nginx/postMessage
Triage of an external SAST/SCA scan run on 2026-05-06. Most loud findings
were already resolved by PR #412 (the 18-CVE backport); this PR addresses
the residual real items:

* Drop unused `handlebars` from backend deps. The runtime require was
  removed in PR #367 (#367) but the package.json line stayed. handlebars
  was the source of two flagged criticals (CVE-2026-33937 RCE,
  GHSA-2w6w-674q-4c4q AST injection) plus 8 highs — all now gone.

* `npm audit fix` on backend + frontend. Bumps transitive picomatch,
  flatted, postcss, brace-expansion via lockfile, and direct dompurify,
  lodash, vite, i18next-http-backend within their existing semver ranges.
  Both audits now report 0 vulnerabilities.

* Add `event.origin === window.location.origin` check to the THEME_PREVIEW
  message listener in PreviewPage. The branding page posts from the same
  origin, so nothing legitimate is rejected; without the check, any third
  party that window.open()'d the preview could push arbitrary
  branding/theme payloads (semgrep
  insufficient-postmessage-origin-validation).

* nginx: `proxy_hide_header` for X-Frame-Options, X-Content-Type-Options,
  Referrer-Policy, Content-Security-Policy, Permissions-Policy,
  Strict-Transport-Security at server level. nginx adds these itself, but
  helmet on the backend was also emitting them — clients were seeing
  duplicates (testssl flagged "Multiple X-Frame-Options / CSP /
  Permissions-Policy / Referrer-Policy headers" on the live origin).
  Single source of truth now.

* Dockerfile hardening (checkov):
  - HEALTHCHECK on backend/Dockerfile, backend/Dockerfile.dev,
    frontend/Dockerfile.dev. Frontend production Dockerfile already had
    one.
  - USER node in frontend/Dockerfile.dev (was running as root).

* GitHub Actions docker-build.yml: explicit top-level
  `permissions: contents: read`. Per-job blocks already declare
  `packages: write` where needed; this stops future steps from
  inheriting unintended privileges (CKV2_GHA_1).

Backend npm audit: 4 vulns -> 0.
Frontend npm audit: 6 vulns -> 0.
Backend unit tests: 13 suites, 131/132 passing (1 pre-existing skip).
Frontend type-check + lint: clean.

The pre-existing integration-test failures (live DB / S3 required) and
the ThemeCustomizerEnhanced QueryClientProvider failures are unrelated
and reproduce on origin/beta without these changes.
2026-05-07 22:31:51 +02:00
github-actions[bot] 8ab6fc6bbd chore(beta): release 3.42.2-beta.0 2026-05-07 12:14:07 +00:00
Paul Nothaft b7d6ca0b65 fix(security): patch 18 dependency CVEs (axios + transitives + nodemailer + i18next-http-backend)
Closes the open Trivy code-scanning alerts for app-side dependencies.
The npm-bundled CVEs in /usr/local/lib/node_modules/npm (picomatch,
brace-expansion, ip-address inside the Node image itself) are deferred
to a separate Node-base-image PR — they're build-environment-side and
need their own compatibility testing.

## Direct dependency bumps

| Package | From | To | CVEs cleared |
|---|---|---|---|
| axios (backend + frontend) | 1.14.0 | 1.15.2 | CVE-2026-42264 (HIGH), CVE-2026-42043 (HIGH), CVE-2026-42035 (HIGH), CVE-2026-42033 (HIGH), CVE-2026-42044, CVE-2026-42042, CVE-2026-42041, CVE-2026-42040, CVE-2026-42039, CVE-2026-42038, CVE-2026-42037, CVE-2026-42036, CVE-2026-42034, CVE-2026-40175, CVE-2025-62718 |
| nodemailer (backend) | ^7.0.13 | ^8.0.5 | GHSA-vvjj-xcjg-gr5g, GHSA-c7w3-x93f-qmm8 |
| i18next-http-backend (frontend) | ^3.0.2 | ^3.0.5 | CVE-2026-41691 |
| uuid (backend) | ^11.1.0 | ^11.1.1 | CVE-2026-41907 |
| postcss (frontend, devDep) | ^8.4.21 | ^8.5.10 | CVE-2026-41305 |

## Transitive bumps (npm overrides)

For transitives whose direct parents haven't released a version that
picks up the patched range, pinned via npm overrides:

| Package | Min | CVE |
|---|---|---|
| follow-redirects (backend + frontend) | >=1.16.0 | GHSA-r4q5-vmmm-2653 |
| fast-xml-parser (backend) | >=5.7.0 | CVE-2026-41650 |
| @tootallnate/once (backend) | >=3.0.1 | CVE-2026-3449 |
| ip-address (backend) | >=10.1.1 | CVE-2026-42338 |

## Why axios is now safe to bump past 1.14.0

PR #268 originally pinned axios to 1.14.0 to avoid a supply-chain
attack on a specific compromised version range. The 1.15.x series
are post-incident upstream releases — clean. Confirmed with the
maintainer before bumping.

## Verified

* `npx tsc --noEmit` (frontend) — clean
* `npx vite build` (frontend) — clean (~4s, existing bundle-size
  warning, not new)
* Backend module-load smoke test — all critical modules load
  (`auth`, `adminAuth` middleware, `emailProcessor`, `recaptcha`,
  `storage`) with the new axios + nodemailer
* Lockfile re-verification — every targeted CVE now resolves to
  the patched version range

## Remaining out of scope

* npm-bundled CVEs inside `/usr/local/lib/node_modules/npm/` —
  picomatch CVE-2026-33671 (HIGH), CVE-2026-33672, brace-expansion
  CVE-2026-33750, ip-address (npm-internal) CVE-2026-42338. These
  live in the Node base image and require a Node base image bump
  with its own compatibility testing — separate PR.

Targeting `beta` so the bumps go through the normal release-please
flow before promotion to `main`.
2026-05-07 13:51:55 +02:00
github-actions[bot] ba405c1dd2 chore(beta): release 3.42.1-beta.0 2026-05-07 10:36:15 +00:00
Paul Nothaft 0c80abd57b fix(gallery): WCAG-safe Download button text + extract HeaderDownloadButton (#401 follow-ups)
Two follow-ups from PR #401's review:

1. Download button text was hardcoded `color: '#ffffff'`. Once admins
   start picking palettes via #400's expanded customizer, a pale accent
   (yellow, pastel blue, etc.) leaves the button unreadable — white
   text on near-white background.

   Fix: derive the foreground colour from the accent's WCAG relative
   luminance and expose it as the new `--color-accent-fg` CSS variable
   in ThemeContext.applyTheme. Light backgrounds (L >= 0.5) get black
   text; dark backgrounds get white. Same treatment applied to
   `--color-accent-dark-fg` for the filled-CTA token.

   The Download button now reads `var(--color-accent-fg, #ffffff)` so
   any future component that paints on accent gets the same treatment
   for free, and legacy deployments before the variable is set fall
   back to the previous hardcoded white.

   Threshold-based (rather than "highest contrast ratio") to preserve
   how saturated mid-tone accents have always rendered. The Picpeak
   default green (#5C8762, L≈0.20) keeps white text — same visual
   identity as before. Only genuinely pale accents flip to black,
   which is the actual scenario the review flagged.

2. The Download button JSX was duplicated three times in
   GalleryLayout.tsx (standard/banner, minimal, hero — ~15 lines
   each). Extracted into a small inline `HeaderDownloadButton`
   component above the GalleryLayout export. Three call sites now
   collapse to a 5-line component invocation each. Markup,
   accessibility, and styling live in one place — future tweaks
   only need to happen once.

## Files

- `frontend/src/utils/contrast.ts` — new helper module:
  `relativeLuminance(hex)` (WCAG 2.x sRGB luminance) and
  `getReadableForeground(hex)` (white-or-black picker).
- `frontend/src/utils/__tests__/contrast.test.ts` — 10 cases:
  fallbacks, saturated mid-tones, pale accents, near-black,
  shorthand `#RGB`, no-leading-`#`, case-insensitive, anchors
  (black/white luminance).
- `frontend/src/contexts/ThemeContext.tsx` — wire the helper into
  `applyTheme`: set `--color-accent-fg` from `accentColor` and
  `--color-accent-dark-fg` from `accentDarkColor`/`primaryColor`.
- `frontend/src/components/gallery/GalleryLayout.tsx` — extract
  `HeaderDownloadButton` component above `GalleryLayout`, replace
  three inline button blocks with the component, update its inline
  style to read `--color-accent-fg` (with the legacy `#ffffff` as
  the CSS-variable fallback).

## Verified

- `npx vitest run src/utils/__tests__/contrast.test.ts` — 10/10 pass
- `npx tsc --noEmit` — clean
- `npx eslint` clean on every touched file
- Default PicPeak green still renders white text (no regression)
- Pale accent (#fef9c3 yellow-100) now correctly renders black text
2026-05-07 11:42:25 +02:00
github-actions[bot] 9cf92f3e8e chore(beta): release 3.42.0-beta.0 2026-05-07 09:34:05 +00:00
Paul Nothaft 876b35b4a5 Merge pull request #401 from Luca-Timo/feat/gallery-header-cleanup
feat(gallery): icon-only menu, accent Download CTA (#386)
2026-05-07 11:33:34 +02:00
github-actions[bot] 4a5395b3ac chore(beta): release 3.41.0-beta.0 2026-05-06 18:26:41 +00:00
Paul Nothaft 8050927607 Merge pull request #400 from Luca-Timo/feat/darkmode-color-improvement
feat(branding): 8-token CI palette + force color mode + dark-mode consistency
2026-05-06 20:26:09 +02:00
Luca de8ad5fdd5 feat(gallery): icon-only menu, accent Download CTA, logo aligned (#386)
Addresses the-luap/picpeak#386 — gallery header layout cleanup.

- Drop the redundant "Menu" text label; menu button is icon-only with
  tight padding (p-2).
- Absolute-position the menu icon at the very left of the header so it
  no longer pushes the logo right with every other action. Logo wrapper
  picks up pl-12 sm:pl-14 only when a menu button is rendered, so the
  icon and logo don't overlap. When no menu button (controlsStyle:
  classic), logo is flush with .container.
- New accent-coloured "Download" CTA placed immediately left of Logout.
  Always visible when downloads are allowed; replaces the previous
  primary-coloured "Download All" header button. Same CTA appears in
  standard, hero, and minimal headers. Intentionally NOT shown in the
  no-header variant (chromeless by design).
- Coloured via var(--color-accent) inline so the button automatically
  tracks whatever palette the admin has chosen — works on plain beta
  today (#22c55e) and auto-upgrades to the CI accent when #400 lands.

The sidebar's own Download All is untouched. Old showDownloadAll prop
stays on GalleryLayout for back-compat; GalleryView now passes
showDownloadAll={false} so only the new accent button renders in the
header.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-06 13:01:09 +02:00
Luca 21188f48d7 fix(theme): centralise force-mode enforcement inside ThemeContext so every gallery flips 2026-05-06 02:27:05 +02:00
Luca a76ecf8496 chore(theme): remove LBM-specific preset (private to maintainer instance) 2026-05-06 02:22:25 +02:00
Luca bdbe7b80a1 feat(events): Sync from Branding button in gallery theme customizer + clarified default inheritance 2026-05-06 01:59:02 +02:00
Luca 47b6b39f3a feat(email): expand email palette to 8 tokens + Sync from Branding button 2026-05-06 01:41:45 +02:00
Luca 565ae45ca7 fix(admin): tab underlines use accent (not accent-dark) for proper highlight color 2026-05-06 01:29:54 +02:00
Luca 578a1745b8 fix(branding): comprehensive sweep — replace remaining primary-* legacy colors with accent tokens 2026-05-06 01:16:52 +02:00
Luca fc2bce3a01 fix(branding): admin sidebar uses accent-dark, primary buttons follow CI token 2026-05-06 00:57:54 +02:00