diff --git a/apps/web/src/app/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx index 58377e6..bb8f8e1 100644 --- a/apps/web/src/app/(auth)/login/page.tsx +++ b/apps/web/src/app/(auth)/login/page.tsx @@ -5,14 +5,13 @@ import { useTranslations } from 'next-intl'; import Link from 'next/link'; import { login } from '@/lib/auth-actions'; import { sanitizeNextPath } from '@/lib/safe-next'; -import { BRAND_YELLOW } from '@/components/brand/brand'; import { TesseraLogo } from '@/components/brand/tessera-logo'; import { DesktopDownloadLinks } from '@/components/desktop/desktop-download-links'; /** - * Split-screen login page (D-01). - * Left: Tessera branding on the brand yellow background (BRAND_YELLOW). - * Right: Login form on neutral background. + * Split-screen login page (D-01), Design „Mosaik“. + * Left (ab lg): dunkles Markenpanel mit Bildmarke, Claim und Farbmosaik. + * Right: Anmeldekarte auf der Arbeitsflaeche; unter lg nur die Karte. * No sidebar or header (D-04, handled by (auth) layout). */ export default function LoginPage() { @@ -42,41 +41,52 @@ export default function LoginPage() { } return ( -
- {/* Left: Branding panel (hidden on mobile, visible on md+) */} -
-
- +
+ {/* Links (ab lg): dunkles Markenpanel im Ton der App-Leiste mit + grosser Bildmarke, Produktname, einem ruhigen Satz und einem + stillen Mosaik in Grau und Gelb — kein Verlauf, keine Animation. */} +
+ + +
+ +

+ {t('branding.claim')} +

+

+ {t('branding.tagline')} +

- {/* Right: Login form */} -
-
- {/* Mobile-only branding */} -
- -

- {t('branding.tagline')} -

+ {/* Rechts: Anmeldekarte zentriert auf der Arbeitsflaeche */} +
+
+ {/* Unter lg: Bildmarke ueber der Karte */} +
+
+
{/* Login heading */} -
-

+
+

{t('login')} -

+

+

+ {t('loginSubtitle')} +

{/* Error message */} @@ -87,12 +97,12 @@ export default function LoginPage() { )} {/* Login form */} -
+ {/* Username */} -
+
@@ -102,16 +112,15 @@ export default function LoginPage() { type="text" required autoComplete="username" - className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" - placeholder={t('username')} + className="flex h-8 w-full rounded-md border border-input bg-card px-2.5 text-sm placeholder:text-muted-foreground" />
{/* Password */} -
+
@@ -121,33 +130,30 @@ export default function LoginPage() { type="password" required autoComplete="current-password" - className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" - placeholder={t('password')} + className="flex h-8 w-full rounded-md border border-input bg-card px-2.5 text-sm placeholder:text-muted-foreground" />
- {/* Remember me (D-02) */} -
+ {/* Angemeldet bleiben (D-02) und Passwort vergessen (D-03) in einer Zeile */} +
+
- - {/* Forgot password link (D-03) */} -
{t('forgotPassword')} @@ -157,7 +163,7 @@ export default function LoginPage() {
{/* Desktop-App-Link (D-12), blendet sich aus ohne API-Antwort */} - +
+ +
); } + +/** + * Stilles Mosaik (Design „Mosaik“): acht Kacheln in einem lockeren + * 4 x 3-Raster in Grautoenen, eine davon in der Akzentfarbe und — wie die + * Signalkachel der Bildmarke — leicht gedreht. Rein dekorativ. + */ +function LoginMosaic() { + // Runde 2: nur Gelb und Graphit-/Grautoene des dunklen Panels. + const tiles: { col: number; row: number; color: string; rotate?: boolean }[] = [ + { col: 1, row: 1, color: 'oklch(1 0 0 / 0.14)' }, + { col: 2, row: 1, color: 'oklch(1 0 0 / 0.08)' }, + { col: 4, row: 1, color: 'oklch(1 0 0 / 0.1)' }, + { col: 2, row: 2, color: 'oklch(1 0 0 / 0.18)' }, + { col: 3, row: 2, color: 'var(--primary)', rotate: true }, + { col: 4, row: 2, color: 'oklch(1 0 0 / 0.06)' }, + { col: 1, row: 3, color: 'oklch(1 0 0 / 0.1)' }, + { col: 3, row: 3, color: 'oklch(1 0 0 / 0.05)' }, + ]; + return ( + + ); +} diff --git a/apps/web/src/app/(auth)/reset-password/[token]/page.tsx b/apps/web/src/app/(auth)/reset-password/[token]/page.tsx index 6f5907e..7d09ecb 100644 --- a/apps/web/src/app/(auth)/reset-password/[token]/page.tsx +++ b/apps/web/src/app/(auth)/reset-password/[token]/page.tsx @@ -83,7 +83,7 @@ export default function ResetPasswordTokenPage() { {/* Heading */}
-

+

{t('resetPassword.newPasswordTitle')}

@@ -99,7 +99,7 @@ export default function ResetPasswordTokenPage() {

{t('resetPassword.backToLogin')} @@ -164,7 +164,7 @@ export default function ResetPasswordTokenPage() {
@@ -67,7 +67,7 @@ export default function ResetPasswordPage() {
{t('resetPassword.backToLogin')} @@ -104,7 +104,7 @@ export default function ResetPasswordPage() { diff --git a/apps/web/src/app/(portal)/admin/groups/components/GroupFormModal.tsx b/apps/web/src/app/(portal)/admin/groups/components/GroupFormModal.tsx index eefe97f..bdd8799 100644 --- a/apps/web/src/app/(portal)/admin/groups/components/GroupFormModal.tsx +++ b/apps/web/src/app/(portal)/admin/groups/components/GroupFormModal.tsx @@ -77,7 +77,7 @@ export function GroupFormModal({ group, onClose, onSaved }: GroupFormModalProps) return (
-
+

{group ? t('edit') : t('create')}

@@ -101,7 +101,7 @@ export function GroupFormModal({ group, onClose, onSaved }: GroupFormModalProps) {!group && (

{t('createLdapHint')}{' '} - + {t('goToLdap')}

@@ -139,14 +139,14 @@ export function GroupFormModal({ group, onClose, onSaved }: GroupFormModalProps) diff --git a/apps/web/src/app/(portal)/admin/groups/components/GroupMembersModal.tsx b/apps/web/src/app/(portal)/admin/groups/components/GroupMembersModal.tsx index 84912fa..1579279 100644 --- a/apps/web/src/app/(portal)/admin/groups/components/GroupMembersModal.tsx +++ b/apps/web/src/app/(portal)/admin/groups/components/GroupMembersModal.tsx @@ -158,7 +158,7 @@ export function GroupMembersModal({ group, onClose, onChanged }: GroupMembersMod return (
-
+

{t('members.title')} — {group.name}

@@ -252,7 +252,7 @@ export function GroupMembersModal({ group, onClose, onChanged }: GroupMembersMod @@ -260,7 +260,7 @@ export function GroupMembersModal({ group, onClose, onChanged }: GroupMembersMod type="button" onClick={handleAdd} disabled={adding || selectedUserIds.length === 0} - className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50" + className="btn btn-primary" > {adding ? tCommon('loading') : `${t('members.addTitle')} (${selectedUserIds.length})`} diff --git a/apps/web/src/app/(portal)/admin/groups/page.tsx b/apps/web/src/app/(portal)/admin/groups/page.tsx index f85f235..0845dbc 100644 --- a/apps/web/src/app/(portal)/admin/groups/page.tsx +++ b/apps/web/src/app/(portal)/admin/groups/page.tsx @@ -124,11 +124,11 @@ export default function AdminGroupsPage() { return (
-

{t('title')}

+

{t('title')}

@@ -149,7 +149,7 @@ export default function AdminGroupsPage() { diff --git a/apps/web/src/app/(portal)/admin/layout.tsx b/apps/web/src/app/(portal)/admin/layout.tsx index 7d72dce..b0657ce 100644 --- a/apps/web/src/app/(portal)/admin/layout.tsx +++ b/apps/web/src/app/(portal)/admin/layout.tsx @@ -12,9 +12,9 @@ export default function AdminLayout({ const t = useTranslations('settings'); return ( -
+
-
+
-

{t('title')}

+

{t('title')}

{/* Section 1: Connection Settings */}
@@ -646,7 +646,7 @@ export default function AdminLdapPage() { @@ -654,7 +654,7 @@ export default function AdminLdapPage() { type="button" onClick={handleTestConnection} disabled={!config && !formData.serverUrl} - className="rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-muted transition-colors disabled:opacity-50" + className="btn btn-secondary" > {t('testConnection')} @@ -816,7 +816,7 @@ export default function AdminLdapPage() { type="button" onClick={handleDiscoverGroups} disabled={discovering} - className="rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-muted transition-colors disabled:opacity-50" + className="btn btn-secondary" > {discovering ? tCommon('loading') : t('groupFilter.discover')} @@ -919,7 +919,7 @@ export default function AdminLdapPage() { type="button" onClick={handleSaveGroupFilter} disabled={savingFilter} - className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50" + className="btn btn-primary" > {savingFilter ? tCommon('loading') : t('groupFilter.save')} @@ -941,7 +941,7 @@ export default function AdminLdapPage() { type="button" onClick={handleDiscoverGroupsToImport} disabled={discoveringGroupImport} - className="rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-muted transition-colors disabled:opacity-50" + className="btn btn-secondary" > {discoveringGroupImport ? tCommon('loading') @@ -1015,7 +1015,7 @@ export default function AdminLdapPage() { type="button" onClick={handleImportGroups} disabled={importingGroups || selectedImportDns.length === 0} - className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50" + className="btn btn-primary" > {importingGroups ? tCommon('loading') @@ -1142,7 +1142,7 @@ export default function AdminLdapPage() { type="button" onClick={() => handleImportUsers(selectedUserDns)} disabled={importingUsers || selectedUserDns.length === 0} - className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50" + className="btn btn-primary" > {importingUsers ? tCommon('loading') @@ -1234,7 +1234,7 @@ export default function AdminLdapPage() { type="button" onClick={handleSaveExcludeList} disabled={savingExclude} - className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50" + className="btn btn-primary" > {savingExclude ? tCommon('loading') : t('userExclude.save')} @@ -1297,7 +1297,7 @@ export default function AdminLdapPage() { type="button" onClick={handleSave} disabled={saving} - className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50" + className="btn btn-primary" > {t('save')} @@ -1319,7 +1319,7 @@ export default function AdminLdapPage() { type="button" onClick={handleSync} disabled={syncing} - className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50" + className="btn btn-primary" > {syncing ? t('sync.syncing') : t('sync.trigger')} diff --git a/apps/web/src/app/(portal)/admin/modules/components/ActivateModuleDialog.tsx b/apps/web/src/app/(portal)/admin/modules/components/ActivateModuleDialog.tsx index 1aad7df..5dde13f 100644 --- a/apps/web/src/app/(portal)/admin/modules/components/ActivateModuleDialog.tsx +++ b/apps/web/src/app/(portal)/admin/modules/components/ActivateModuleDialog.tsx @@ -142,7 +142,7 @@ export function ActivateModuleDialog({ role="dialog" aria-modal="true" > -
+

{t('title', { module: moduleName })}

@@ -157,7 +157,7 @@ export function ActivateModuleDialog({ type="button" onClick={onCancel} disabled={isSubmitting} - className="rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-muted transition-colors disabled:opacity-50" + className="btn btn-secondary" > {t('cancel')} @@ -165,7 +165,7 @@ export function ActivateModuleDialog({ type="button" onClick={handleConfigureLater} disabled={isSubmitting} - className="rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-muted transition-colors disabled:opacity-50" + className="btn btn-secondary" > {t('configureLater')} @@ -173,7 +173,7 @@ export function ActivateModuleDialog({ type="button" onClick={handleGrantNow} disabled={grantNowDisabled} - className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:cursor-not-allowed disabled:opacity-50" + className="btn btn-primary" > {t('grantNow')} diff --git a/apps/web/src/app/(portal)/admin/modules/grants/page.tsx b/apps/web/src/app/(portal)/admin/modules/grants/page.tsx index 78edfe0..30b147e 100644 --- a/apps/web/src/app/(portal)/admin/modules/grants/page.tsx +++ b/apps/web/src/app/(portal)/admin/modules/grants/page.tsx @@ -189,7 +189,7 @@ export default function AdminModuleGrantsPage() { return (
-

{t('title')}

+

{t('title')}

{error && ( @@ -205,7 +205,7 @@ export default function AdminModuleGrantsPage() {

{t('emptyModules')}

{t('emptyModulesLink')} diff --git a/apps/web/src/app/(portal)/admin/modules/page.tsx b/apps/web/src/app/(portal)/admin/modules/page.tsx index 5cd8e26..9701a67 100644 --- a/apps/web/src/app/(portal)/admin/modules/page.tsx +++ b/apps/web/src/app/(portal)/admin/modules/page.tsx @@ -134,12 +134,12 @@ export default function AdminModulesPage() {
-

{t('title')}

+

{t('title')}

{t('description')}

{t('grantsLink')} diff --git a/apps/web/src/app/(portal)/admin/tenants/page.tsx b/apps/web/src/app/(portal)/admin/tenants/page.tsx index 0c76f84..d22d4e3 100644 --- a/apps/web/src/app/(portal)/admin/tenants/page.tsx +++ b/apps/web/src/app/(portal)/admin/tenants/page.tsx @@ -146,11 +146,11 @@ export default function AdminTenantsPage() {
{/* Page header */}
-

{t('title')}

+

{t('title')}

@@ -244,7 +244,7 @@ export default function AdminTenantsPage() { {/* Create/Edit modal */} {showForm && (
-
+

{editingTenant ? t('edit') : t('create')}

@@ -287,13 +287,13 @@ export default function AdminTenantsPage() { @@ -306,7 +306,7 @@ export default function AdminTenantsPage() { {/* Delete confirmation modal */} {deleteConfirm && (
-
+

{t('deleteConfirm')}

@@ -314,7 +314,7 @@ export default function AdminTenantsPage() { diff --git a/apps/web/src/app/(portal)/admin/users/components/UserAccessModal.tsx b/apps/web/src/app/(portal)/admin/users/components/UserAccessModal.tsx index c3be598..ad2257e 100644 --- a/apps/web/src/app/(portal)/admin/users/components/UserAccessModal.tsx +++ b/apps/web/src/app/(portal)/admin/users/components/UserAccessModal.tsx @@ -128,7 +128,7 @@ export function UserAccessModal({ userId, username, onClose }: UserAccessModalPr role="dialog" aria-modal="true" > -
+

{t('modalTitle', { username })}

@@ -349,7 +349,7 @@ export default function AdminUsersPage() { {/* Create/Edit modal */} {showForm && (
-
+

{editingUser ? t('edit') : t('create')}

@@ -455,13 +455,13 @@ export default function AdminUsersPage() { @@ -474,7 +474,7 @@ export default function AdminUsersPage() { {/* Delete confirmation modal */} {deleteConfirm && (
-
+

{t('deleteConfirm')}

@@ -490,7 +490,7 @@ export default function AdminUsersPage() { diff --git a/apps/web/src/app/(portal)/change-password/page.tsx b/apps/web/src/app/(portal)/change-password/page.tsx index 97ba2a8..f669a6e 100644 --- a/apps/web/src/app/(portal)/change-password/page.tsx +++ b/apps/web/src/app/(portal)/change-password/page.tsx @@ -47,7 +47,7 @@ export default function ChangePasswordPage() { return (
-

+

{t('changePassword.title')}

@@ -129,7 +129,7 @@ export default function ChangePasswordPage() { diff --git a/apps/web/src/app/(portal)/marketplace/components/CategoryFilter.tsx b/apps/web/src/app/(portal)/marketplace/components/CategoryFilter.tsx index ea7b11a..be5f3d0 100644 --- a/apps/web/src/app/(portal)/marketplace/components/CategoryFilter.tsx +++ b/apps/web/src/app/(portal)/marketplace/components/CategoryFilter.tsx @@ -1,6 +1,7 @@ 'use client'; import { useTranslations } from 'next-intl'; +import { useCategoryLabel } from '@/lib/use-category-label'; interface CategoryFilterProps { categories: string[]; @@ -10,17 +11,23 @@ interface CategoryFilterProps { export function CategoryFilter({ categories, value, onChange }: CategoryFilterProps) { const t = useTranslations('marketplace'); + const categoryLabel = useCategoryLabel(); + // Dezente Pillen (Fluent „subtle“): gewaehlt = dunkle Fuellung wie bei + // GitHub-Filtern, nicht die gelbe Akzentflaeche. + const chip = (selected: boolean) => + `inline-flex h-7 items-center rounded-full border px-3 text-xs font-medium whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${ + selected + ? 'border-foreground bg-foreground text-background' + : 'border-border bg-card text-foreground hover:bg-muted' + }`; return ( -
+
@@ -29,13 +36,10 @@ export function CategoryFilter({ categories, value, onChange }: CategoryFilterPr key={cat} type="button" onClick={() => onChange(cat)} - className={`rounded-full px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${ - value === cat - ? 'bg-primary text-primary-foreground' - : 'bg-muted text-foreground hover:bg-muted/80' - }`} + aria-pressed={value === cat} + className={chip(value === cat)} > - {cat} + {categoryLabel(cat)} ))}
diff --git a/apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx b/apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx index 008bd68..4c01457 100644 --- a/apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx +++ b/apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx @@ -1,6 +1,8 @@ 'use client'; import { useTranslations } from 'next-intl'; +import { ModuleTile } from '@/components/modules/module-tile'; +import { useCategoryLabel } from '@/lib/use-category-label'; /** * Props for the MarketplaceCard component. @@ -25,53 +27,6 @@ export interface MarketplaceCardProps { onLockedClick: () => void; } -/** - * Default icon mapping for known module icons. - * Renders inline SVG to avoid external icon library dependency. - */ -function ModuleIcon({ icon }: { icon?: string }) { - if (icon === 'Globe') { - return ( - - ); - } - - // Default puzzle-piece icon for generic modules - return ( - - ); -} - /** * Marketplace card showing a module with activation controls. * @@ -84,7 +39,6 @@ export function MarketplaceCard({ name, slug, description, - icon, category, isActive, hasAccess, @@ -95,6 +49,7 @@ export function MarketplaceCard({ onLockedClick, }: MarketplaceCardProps) { const t = useTranslations('marketplace'); + const categoryLabel = useCategoryLabel(); const locale = (typeof window !== 'undefined' && document.documentElement.lang) || 'de'; const localizedDescription = description[locale] || description.en || description.de || ''; @@ -114,10 +69,9 @@ export function MarketplaceCard({ return (
{/* Deckende Schaltflaeche: traegt den Kartenklick, damit die verschachtelte Aktivieren-Schaltflaeche im Fuss kein -
- )} + )} +
); } diff --git a/apps/web/src/app/(portal)/marketplace/components/MarketplaceSearch.tsx b/apps/web/src/app/(portal)/marketplace/components/MarketplaceSearch.tsx index 6e69c08..64be631 100644 --- a/apps/web/src/app/(portal)/marketplace/components/MarketplaceSearch.tsx +++ b/apps/web/src/app/(portal)/marketplace/components/MarketplaceSearch.tsx @@ -23,13 +23,30 @@ export function MarketplaceSearch({ onChange }: MarketplaceSearchProps) { }, [value, onChange]); return ( -
+
+ setValue(e.target.value)} placeholder={t('searchPlaceholder')} - className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" + aria-label={t('searchPlaceholder')} + className="flex h-8 w-full rounded-md border border-input bg-card pl-8 pr-8 text-sm placeholder:text-muted-foreground" /> {value && ( - ))} + {tabs.map((tab) => { + const selected = value === tab.key; + return ( + + ); + })}
); } diff --git a/apps/web/src/app/(portal)/marketplace/components/TenantContextSelector.tsx b/apps/web/src/app/(portal)/marketplace/components/TenantContextSelector.tsx index 57062a1..30a7e9d 100644 --- a/apps/web/src/app/(portal)/marketplace/components/TenantContextSelector.tsx +++ b/apps/web/src/app/(portal)/marketplace/components/TenantContextSelector.tsx @@ -37,8 +37,8 @@ export function TenantContextSelector() { if (!isSuperAdmin) return null; return ( -
-