- New core partial templates/partials/app-input-copy.phtml renders a
standard label + input with a compact copy button overlaid on the
right edge of the input. Used for privacy/imprint URLs on the tenant
form; reusable for any field where a one-click copy is helpful.
- Extend the shared copy-field component (web/js/components/app-copy-field.js)
with data-copy-target support — the button now reads the target input's
.value at click-time, so users can edit a field and still copy the
current value. Static data-copy-value keeps working unchanged.
- New component CSS web/css/components/app-input-copy.css positions the
button absolute/inset + margin-block:auto (robust vertical centering
regardless of input height) and uses a descendant selector (0,2,0)
so the button wins over the global [data-tooltip] position:relative.
- Also register app-input-copy.css + app-tenant-logo.css in core.css
@import list — they were only in the shared asset group before, which
default-template admin pages don't load, so tenant-logo styles were
effectively missing on admin tenant edit (topbar size etc.).
- Document the Copy-to-Clipboard + Copyable Input standards in
docs/reference-frontend-javascript.md so future consumers don't
re-invent the markup/selectors.
- File-upload preview: pending-file block restructured to a compact
list-item row (small square thumb + filename/size stack + clear X)
and removed the transparency checker pattern on the current-image
preview in favour of a flat --app-preview-bg surface.
- Tenant edit page title + breadcrumb now show the tenant description
("Acme GmbH") instead of the generic "Mandant bearbeiten" when one
is present.
- i18n: add "Copy to clipboard" / "In Zwischenablage kopieren" across
both locales.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Server renders initial file-upload state (src, alt, label text,
has-current class) so there is no flash of broken-image icon while
JS boots. JS no longer duplicates that work — it only reacts to
user input (select, clear, drag-and-drop) and uses data-current-src
solely to restore the server file after a blob preview.
- Clicking the current preview image now opens the file dialog, mirrors
the Replace button, with hover affordance (cursor + primary border).
- Drop the transparency checker pattern in the preview — the admin UI
does not need Figma-style transparency semantics. Preview is now a
flat theme-aware surface (--app-preview-bg) that keeps white or
black logos visible against a neutral gray.
- Move tenant favicon out of the aside into the Master-data tab as its
own details block next to the Tenant-logos section. Uses the same
barrier-form pattern (HTML5 form attribute + app-file-upload.phtml)
for a consistent instant-upload UX.
- Tenant-logo slot label is a native <label> element — picks up the
global form-label typography and spacing, no custom muted-color
class needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the single tenant avatar with a pair of theme-scoped brand logos.
Render only the theme-matching <img> server-side and swap src on theme
toggle via a JS hook — no reload, no double request, no CSS tricks.
Tenant logos
- TenantLogoService (ImageUploadTrait) with theme whitelist and per-theme
storage storage/tenants/{uuid}/logo/{light|dark}/, SIZES 128/256/512
- Public serving endpoint auth/tenant-logo-file so login can show the
logo pre-auth; matching authenticated admin preview endpoint
- appTenantLogoUrl(?size, ?theme) with 4-step fallback cascade; PDF +
mail always request 'light'
- Admin tenant edit: avatar block replaced by "Tenant logos" details
block inside the Master-data tab, two side-by-side slots via Pico
.grid with the core app-file-upload partial
- Policy rename ABILITY_ADMIN_TENANTS_AVATAR_VIEW -> LOGO_VIEW, action
routes logo / logo-delete / logo-file with theme body/query param
- API endpoint path kept (backward compat), internals on new service
- CLI tenant:logo-migrate-avatars moves legacy avatar/ -> logo/light/
idempotently (--dry-run, --yes, --cleanup)
- i18n "Tenant image" removed, 12 new keys synced across de/en
File upload component
- Full-width preview + filename/actions below (3D stack layout)
- Fixed 16:9 aspect ratio with 1rem inner padding for consistent
preview size across any logo aspect
- Transparency checker pattern as background so black logos stay
visible on dark mode and white logos on light mode
- form="" + deleteFormId support so the partial works with barrier
forms inside another form
Buttons
- width:100% dropped from button[type="submit"]; scoped back via
.login-main for the auth-flow primary CTA
- .outline base rule now tints background via color-mix of --app-color
so secondary/primary/danger outlines all gain a subtle surface
- .outline.secondary restyled Stripe-style in both themes: solid white
chip with soft shadow in light, solid elevated dark chip with white
text in dark; neutral border replaces role-colored border
- .app-action-success/.app-action-danger outlines get color-mix bg +
theme-aware outline-text tokens for stronger contrast
- Filled .primary/.app-action-success/.app-action-danger get raised
box-shadow (inset highlight + drop) — opt-in via class so chrome
buttons stay flat
- Dropped the legacy .secondary utility that was clobbering the
custom-property cascade with a hardcoded muted color
Theme swap
- Logo img carries data-src-light + data-src-dark; theme-toggle JS
swaps src when data-theme changes, keeping the topbar/login logo in
sync without a page reload
Quality gates: PHPUnit (2045), PHPStan L5, CS-Fixer, docs link/drift,
codex skills sync — all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove the extensible theme catalog (config/themes.php + ThemeConfigGateway)
in favor of a two-entry const on SettingsAppGateway. appThemes() now returns
the list directly — no container lookup, no file include. Drop the
dark-green theme assets, narrow [data-theme^="dark"] selectors to
[data-theme="dark"], and tighten isDarkTheme() to an exact match. Ship an
idempotent migration that corrects any leftover 'dark-green' rows on users
and tenants to safe defaults (light / NULL).
Tenant scoping (default_theme, allow_user_theme) and per-user override stay
intact; only the catalog extensibility and the third theme are removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tokenSelectForm() in core/Support/helpers/ui.php: an inline
typeahead combobox + flat alphabetical removable list, designed for
selections that outgrow the chip-header of the vendor MultiSelect
(roles, permissions, and similar admin pickers).
Contract:
- Hidden <select multiple name="<name>[]"> is the form submission source —
consumers read via $request->body() verbatim, no parsing
- Items are ['id' => int, <labelKey> => string, 'key' => string?]
where labelKeys default to ['description', 'label', 'name']; 'key' is
an invisible fuzzy-match hint
- $labelOverrides lets callers swap emptyState / removeTooltip /
noMatches / clearAll / countSuffix / errorMessage with domain copy
- $disabled renders pure-presentation list (no combobox, no remove)
Runtime:
- initTokenSelect in web/js/components/app-token-select.js is registered
as 'token-select' in app-init.js; destroy()/cleanupFns contract
- Syncs the hidden <select> on every mutation and dispatches 'change'
so dependent UI can react
Wired up: pages/admin/permissions/_form.phtml (assigned roles),
pages/admin/roles/_form.phtml (permissions + assignable roles).
Helper contract lives in tests/Support/Helpers/TokenSelectFormHelperTest.php;
runtime contract + entrypoint registration + host usage are enforced by
FrontendRuntime*ContractTest.php.
Coexists with multiSelectForm() — pick tokenSelectForm() when the
selected set can grow beyond ~10 items or typeahead is expected.
Docs in CLAUDE.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Unifies the breakpoint system and fixes cross-layer CSS bugs that kept
the old mobile hide rules from ever taking effect.
Breakpoints:
- Document canonical xs/sm/md/lg/xl/2xl scale in variables.base.css
- Replace 968px one-offs with 1024px (app-details, app-page-editor)
- Make details tab-panel max-width responsive (min(70ch, 100%))
Topbar (< md):
- Hide breadcrumb in components layer so cross-layer overrides actually win
- Dock search icon alongside right-column icons via flex layout
- Add xs (400px) tightening for hit-area preservation
- Hide brand divider alongside hidden brand name
- Switch hover states to theme-aware --app-dropdown-hover-background-color
Mobile drawer:
- Icon-bar now vertical like desktop (was horizontal top strip, effectively invisible)
- Sidebar shifted 52px right so icon-bar and sidebar slide in as one 280px unit
- Add breathing room above first nav item (20px top padding)
List tabs:
- Rewrite .app-list-tabs to horizontal scroll with hidden scrollbar, snap,
and mask-image gradient fades; add initListTabs to scroll active into view
- Register as runtime component in app-init.js
List toolbar:
- Stack wide controls (multi-select, search/text inputs) full-width below sm,
keep compact controls at natural width
Footer:
- New dedicated app-footer.css with mobile-first stacking, border-top separator,
theme-aware hover; remove redundant rules from app-shell.css
Notification dropdown:
- Width with min(360px, 100vw - 2rem) and min-width: 0 override global
details > ul rule; switch to position: fixed below md so it hugs the
viewport right edge instead of the non-rightmost bell button
Content spacing:
- Add padding-block-start to .app-main-content on mobile (was 0)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Simplifies the tenant Visibility tab to match the tenant-only appearance
model established by the previous commits:
- primary_color: now required. Removed the "No brand color / Use system
default (no brand accent)" toggle — every tenant carries a concrete
primary color. Legacy NULL rows render the neutral app default (#2fa4a4)
in the color picker and are converted to that explicit hex on save.
- default_theme: the select no longer offers a "Use system default" empty
option. Legacy NULL rows resolve to 'light' at render time; saves always
persist a valid theme via SettingsAppGateway::normalizeTheme().
- allow_user_theme: replaced the tri-state "Use system default (allowed) /
Force allow / Force disallow" select with a single boolean switch
("Users may choose their own theme"). Legacy NULL rows load as checked.
Saves persist 0/1 explicitly.
TenantService: sanitize no longer reads primary_color_use_default or
allow_user_theme_mode; it validates primary_color as a required hex and
treats allow_user_theme as a plain boolean. Both create and update paths
write concrete values only — no more NULL writes for these three fields.
DirectorySettingsGateway gains a normalizeTheme() delegate so TenantService
can route through the same gateway it uses for isAllowedTheme().
Removed now-unused app-color-default-toggle JS component + its runtime
registration + its architecture-test entry. i18n cleanup: "No brand
color", "Use system default (no brand accent)", "When enabled the tenant
renders without a brand accent color.", "Use system default (light)",
"Use system default (allowed)", "Force allow user theme", "Force disallow
user theme", "User theme policy is invalid" all removed. New copy: "Users
may choose their own theme" + helper text, plus a tightened tab blockquote.
Tests: TenantServiceTest validInput() updated to send concrete values;
settingsGateway mock gets normalizeTheme() + isAllowedTheme() defaults.
All 1985 tests pass; PHPStan level 5 clean; QG-006 clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Converts the "Open full page" control in the detail drawer header from an
icon-only link into a text+icon button using the standard
`secondary outline small` classes. Because the base button rule in
app-shell.css only targets `button, [type=…], [role=button]`, the anchor
gets `role="button"` so the core cascade applies (border-radius, padding,
hover/focus states). Component-local CSS is trimmed to just the flex
layout (icon + label with gap) and text-decoration reset — border, radius
and colors come from the shared button base.
Tooltip is removed (redundant with visible text); aria-label remains for
screen readers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactors the address-book detail partial (templates/partials/app-user-profile.phtml)
toward a Stripe/Linear aesthetic: the banner + avatar still carry identity,
but the header now also carries meta chips (primary tenant, department,
hire date) and primary CTAs (Send email, Call). Email/phone/mobile move
out of a tab into an always-visible contact stack directly under the
identity block, with icon-led rows and a hover-revealed copy button.
Tabs reduce to Address / About / Organization, with Organization only
appearing for multi-tenant users — single-tenant assignments are fully
communicated through chips. The detail aside is gone; main content is
single-column and more focused.
Introduces web/js/components/app-copy-field.js — a generic, server-
markup-driven copy-to-clipboard button component wired from app-init.js.
The markup is rendered by the PHP partial (role-compatible, SSR-safe);
the component only attaches the click handler and drives icon-swap
feedback via an is-copied class.
Address-book index page now loads the 'address-book' CSS group alongside
'address-book-index' so the detail drawer (which mounts the same profile
partial inline) gets the correct styles.
Contact rows use inline-block + vertical-align centering inside the
clickable link; the field-label tooltip lives on a <span> wrapper rather
than the <i> itself (Bootstrap icons render their glyph via ::before, and
the tooltip rule would otherwise cap it). Hover reveals the copy button
on pointer devices; @media (hover: none) shows it permanently on touch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both endpoints were declared as bare relative paths ("admin/users/theme",
"admin/users/switch-tenant") and handed to postForm(), which normalizes
through `new URL(value, window.location.href)`. On a deep route like
/de/admin/permissions that resolves to /de/admin/admin/users/theme —
404 → 302 to error page → HTML response → JSON.parse throws → the theme
toggle reverts visually. The tenant switcher had the same latent bug.
- Topbar renders data-theme-url and data-switch-tenant-url via lurl(), so
both carry the locale-aware root-anchored path.
- Tenant switcher reads data-switch-tenant-url from the switcher root
(matching the theme-menu convention) and passes it through to
switchTenant(); fail-fast warning if the attribute is missing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces a reusable core detail-drawer primitive that slides in from the
right and loads any view via a `*-fragment(none).phtml` endpoint. Bundles the
address book list overhaul that is its first consumer.
Core additions:
- `app-detail-drawer.js` — generic drawer with stepper, focus trap, body
scroll-lock, URL-hash deep-linking, session-expiry detection
- `app-fragment-init.js` — auto-wires tabs/lookups/confirm/file-upload/
fslightbox inside injected HTML; consumers do not re-initialize components
- `app-focus-trap.js` — shared focus-trap + refcounted scroll-lock, used by
both filter-drawer and detail-drawer
- `getHtml()` in `app-http.js` + `SessionExpiredError`; drawer reloads the
page on auth redirect instead of rendering the login form in the panel
- `DetailDrawerFragmentContractTest` enforces that every `initDetailDrawer`
consumer ships matching `*-fragment($id).php` + `*-fragment(none).phtml`
Address book list:
- Grid collapses from 9 columns to 4 (identity / context / phone / actions)
with a two-line identity cell (avatar + name + email)
- Tenant register tabs above the grid using the `app-list-tabs` partial;
tenant filter wired via hidden toolbar field so grid.js forwards it on
every data fetch
- Profile body extracted to a shared partial so the full-page view and the
new drawer fragment share the same markup
- New i18n keys for the drawer/list labels
Also refactors `app-filter-drawer` to reuse the shared focus-trap and
scroll-lock instead of maintaining its own copy, and documents the
detail-drawer convention in CLAUDE.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implement handover protocols as a new entity in the helpdesk module,
allowing users to create fillable protocol records from admin-defined
software product schemas.
Key additions:
- DB migration (helpdesk_handovers table) with tenant scope
- HandoverService with status workflow (draft/in_progress/completed/archived)
- Three-tier permissions (view/create/manage)
- Two-step creation wizard (Stripe-style assistant)
- Grid.js list page with search and status filter
- Edit/detail page with aside metadata and status controls
- Reusable core autocomplete lookup component (app-lookup-field)
- Debitor lookup data endpoint for autocomplete
- Dynamic form rendering from schema snapshots
- 11 PHPUnit tests for HandoverService
- DE+EN i18n translations (48 keys each)
Also includes: PHPStan fixes (dead code removal, stale baseline cleanup),
software product edit title improvement, fieldset simplification,
and Stripe-style hover for schema preview.
Workflow: HD-HANDOVERS-001 (.agents/runs/HD-HANDOVERS-001/)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move topbar outside the grid container as a standalone sticky header
(Shopify/Stripe pattern). Sidebar and icon bar use position:sticky
relative to topbar height. Mobile uses drawer pattern with backdrop,
focus trap, ESC close, and scroll lock. Tenant branding moved from
sidebar header into the topbar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace bare <input type="file"> across all upload locations with a
card-based dropzone component via shared partial. Three visual states:
current server file (thumbnail + Replace/Delete), empty dropzone, and
pending file preview (local FileReader thumbnail + metadata). Delete
actions use data-confirm-message for confirmation dialog.
Centralized as templates/partials/app-file-upload.phtml to prevent
markup drift — documented as deliberate exception to plain-HTML inputs
convention in CLAUDE.md and developer checklist.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move breadcrumb rendering from individual page templates into the core
topbar. Each page now sets $breadcrumbs in its action .php file; the
topbar renders it automatically via the shared partial.
- Remove global back/forward buttons and app-nav-history.js component
- Remove Alt+Arrow keyboard shortcuts for history navigation
- Render breadcrumb in topbar-left section (replaces button area)
- Clean up breadcrumb CSS: context-neutral base (flex, no margin)
- Recalculate sticky titlebar offset in details container
- Migrate all 41 pages (core + helpdesk, audit, addressbook, api-docs)
- Add missing breadcrumbs to addressbook detail view
- Update architecture contract tests (nav-history references removed)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- add helpdesk module pages, services, settings and tests
- standardize debtor list on drawer/grid contracts and robust filter drawer behavior
- add helpdesk aside panel navigation and settings visibility provider
- switch primary list slug to helpdesk/debitor and remove helpdesk/search compatibility
- include required core contract updates for list contracts and detail/drawer integration
Hide LDAP connection, search/bind, and attribute mapping cards until
the "Enable LDAP login" toggle is activated — same UX pattern as the
Microsoft SSO section using data-tenant-ldap-* attributes and a
mirrored JS toggle component.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Custom-built error handler for the web channel that shows a tabbed debug
dashboard (exception, request context, environment, SQL queries) in dev mode
and a clean branded error page with copyable request ID in prod mode.
All errors are logged as JSON lines to storage/logs/errors/ for lookup by
request ID. Uses the project's design tokens for visual consistency.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace prominent outlined chevron buttons with borderless ghost icons
and gradient fade masks for a cleaner overflow indicator. Reset shell-level
button custom properties to prevent global button styles from bleeding through.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs in the toast redesign:
1. Variant backgrounds (success/warning/info/error) used rgba with 12-20%
opacity, making toasts semi-transparent over page content. Fixed by
layering the tint over a solid background via linear-gradient compositing.
2. Flash messages with scoped paths including query strings (e.g.
edit pages with ?return=...) were not matched by Flash::peek(Request::path())
since path() strips the query. Added fallback to pathWithQuery() matching.
Also moved flash partial before JS init scripts to ensure DOM is present
when the component runtime mounts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevents session timeout for users who stay active on a single page
(scrolling, reading) without making requests. Pings every 5 min only
when interaction was detected since last sync — no false keep-alive.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Convert app-global-search.js from a self-executing module to an
exported initGlobalSearch() function following the established
component pattern. Extract 6 inline handlers to named variables,
add destroy() for proper listener cleanup, double-bind guard,
and return a public API (destroy, openSearch, focusSearch).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace innerHTML assignments in app-tabs.js and app-password-toggle.js
with createElement/appendChild to prevent potential XSS vectors. Add
destroy() methods to app-tabs, app-filter-drawer, and
app-toggle-aside-panels so document/window-level listeners can be
properly removed when components are torn down.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DRY duplicated helpers (escapeAttr, belongsToForm, isSubmitControl,
isEditableTarget) into web/js/core/app-form-utils.js and update 7
consumers. Add aria-live="polite" to flash messages, async messages,
and search results for screen reader announcements. Add
prefers-reduced-motion support to CSS tooltips.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Session timeout redirected to /de/login?return_to=... but the remember-me
cookie was not cleared by logoutDueToSessionTimeout(). On the next request,
autoLoginFromCookie() silently re-logged the user in, causing the MintyPHP
router to resolve the login route to pages/index().php (dashboard) instead
of the login action — the intended URL redirect logic never executed.
Fix: intercept ?return_to= immediately after successful auto-login in
index.php and redirect to the sanitized route path. Also harden the login
flow with hidden return_to form fields and POST body fallback for cases
without remember-me.
Additional improvements in this commit:
- Convert stored REQUEST_URI to router-compatible path (strip leading
slash and locale prefix) via IntendedUrlService::toRoutePath()
- Consolidate duplicate CSRF data attributes into shared data-csrf-key
and data-csrf-token on <html> element
- Add tenant branding (logo) to auth pages via appAuthLogoUrl() helper
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a session expires, the user's current URL is now captured and restored
after re-authentication (via remember-me cookie or manual login), instead of
always redirecting to the dashboard. A JavaScript session warning dialog
appears ~2 minutes before idle timeout, allowing users to extend their session
with a single click. Includes open-redirect prevention via URL validation,
Microsoft SSO callback support, and 33 unit tests.
Refs: SESSION-REDIRECT-PRESERVE-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>