fix(ui): theme tokens, primary CTAs, billing; invitations; connections

Made-with: Cursor
This commit is contained in:
ValueOn AG 2026-04-02 23:53:24 +02:00
parent d3d054b132
commit 2a60f322ca
20 changed files with 175 additions and 114 deletions

View file

@ -25,7 +25,7 @@
.treeNode.multiSelected { .treeNode.multiSelected {
background: var(--color-bg-multi-selected, rgba(25, 118, 210, 0.14)); background: var(--color-bg-multi-selected, rgba(25, 118, 210, 0.14));
box-shadow: inset 3px 0 0 var(--color-primary, #1976d2); box-shadow: inset 3px 0 0 var(--color-primary, #F25843);
} }
.treeNode.multiSelected:hover { .treeNode.multiSelected:hover {
@ -34,7 +34,7 @@
.treeNode.dropTarget { .treeNode.dropTarget {
background: var(--color-bg-drop, rgba(25, 118, 210, 0.15)); background: var(--color-bg-drop, rgba(25, 118, 210, 0.15));
outline: 2px dashed var(--color-primary, #1976d2); outline: 2px dashed var(--color-primary, #F25843);
outline-offset: -2px; outline-offset: -2px;
} }
@ -77,7 +77,7 @@
.renameInput { .renameInput {
flex: 1; flex: 1;
border: 1px solid var(--color-primary, #1976d2); border: 1px solid var(--color-primary, #F25843);
border-radius: 3px; border-radius: 3px;
padding: 1px 4px; padding: 1px 4px;
font-size: inherit; font-size: inherit;

View file

@ -243,9 +243,9 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
<div style={{ <div style={{
position: 'absolute', inset: 0, position: 'absolute', inset: 0,
background: 'rgba(25, 118, 210, 0.08)', background: 'rgba(25, 118, 210, 0.08)',
border: '2px dashed #1976d2', borderRadius: 8, border: '2px dashed #F25843', borderRadius: 8,
zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 13, fontWeight: 600, color: '#1976d2', fontSize: 13, fontWeight: 600, color: '#F25843',
}}> }}>
Dateien hier ablegen Dateien hier ablegen
</div> </div>
@ -257,14 +257,14 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
<button <button
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
disabled={uploading} disabled={uploading}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: '#1976d2' }} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: '#F25843' }}
title="Upload files" title="Upload files"
> >
{uploading ? '...' : '+'} {uploading ? '...' : '+'}
</button> </button>
<button <button
onClick={_refreshAll} onClick={_refreshAll}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: '#1976d2' }} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: '#F25843' }}
> >
{'\u21BB'} {'\u21BB'}
</button> </button>

View file

@ -142,12 +142,12 @@ const _SOURCE_COLORS: Record<string, string> = {
ftpFolder: '#795548', ftpFolder: '#795548',
files: '#795548', files: '#795548',
'local:ftp': '#795548', 'local:ftp': '#795548',
'local:jira': '#1976d2', 'local:jira': '#0052CC',
clickup: '#7b68ee', clickup: '#7b68ee',
}; };
function _getSourceColor(sourceType: string): string { function _getSourceColor(sourceType: string): string {
return _SOURCE_COLORS[sourceType] || '#1976d2'; return _SOURCE_COLORS[sourceType] || '#F25843';
} }
const _SOURCE_ICONS: Record<string, string> = { const _SOURCE_ICONS: Record<string, string> = {
@ -335,7 +335,7 @@ function _Spinner(): React.ReactElement {
return ( return (
<span style={{ <span style={{
display: 'inline-block', width: 10, height: 10, display: 'inline-block', width: 10, height: 10,
border: '1.5px solid #ccc', borderTopColor: '#1976d2', border: '1.5px solid var(--border-color, #ccc)', borderTopColor: 'var(--primary-color, #F25843)',
borderRadius: '50%', borderRadius: '50%',
animation: 'spin 0.6s linear infinite', animation: 'spin 0.6s linear infinite',
}} /> }} />
@ -876,7 +876,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
<button <button
onClick={_loadConnections} onClick={_loadConnections}
disabled={loadingRoot} disabled={loadingRoot}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: '#1976d2' }} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: 'var(--primary-color, #F25843)' }}
> >
{loadingRoot ? '...' : '\u21BB'} {loadingRoot ? '...' : '\u21BB'}
</button> </button>
@ -1157,9 +1157,9 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
onClick={e => { e.stopPropagation(); onAdd(node); }} onClick={e => { e.stopPropagation(); onAdd(node); }}
disabled={isAdding} disabled={isAdding}
style={{ style={{
background: 'none', border: '1px solid #1976d2', borderRadius: 3, background: 'none', border: '1px solid var(--primary-color, #F25843)', borderRadius: 3,
cursor: isAdding ? 'not-allowed' : 'pointer', cursor: isAdding ? 'not-allowed' : 'pointer',
fontSize: 10, color: '#1976d2', padding: '1px 5px', fontSize: 10, color: 'var(--primary-color, #F25843)', padding: '1px 5px',
opacity: isAdding ? 0.5 : 1, opacity: isAdding ? 0.5 : 1,
flexShrink: 0, flexShrink: 0,
}} }}

View file

@ -116,7 +116,7 @@ export function useConfirm() {
style={{ style={{
padding: '8px 18px', borderRadius: '6px', fontSize: '0.875rem', fontWeight: 600, padding: '8px 18px', borderRadius: '6px', fontSize: '0.875rem', fontWeight: 600,
border: 'none', border: 'none',
background: isDanger ? '#ef4444' : 'var(--color-primary, #3b82f6)', background: isDanger ? '#ef4444' : 'var(--primary-color, #F25843)',
color: '#fff', color: '#fff',
cursor: 'pointer', cursor: 'pointer',
}} }}

View file

@ -49,7 +49,7 @@ export interface Invitation {
export interface InvitationCreate { export interface InvitationCreate {
/** Username of the user to invite (optional when email is provided) */ /** Username of the user to invite (optional when email is provided) */
targetUsername?: string; targetUsername?: string;
/** Email address to send invitation link (required for new users) */ /** Email to send invitation link; optional if targetUsername is set */
email?: string; email?: string;
roleIds: string[]; roleIds: string[];
featureInstanceId?: string; featureInstanceId?: string;

View file

@ -144,7 +144,7 @@ export function usePrompt() {
style={{ style={{
padding: '8px 18px', borderRadius: '6px', fontSize: '0.875rem', fontWeight: 600, padding: '8px 18px', borderRadius: '6px', fontSize: '0.875rem', fontWeight: 600,
border: 'none', border: 'none',
background: isDanger ? '#ef4444' : 'var(--color-primary, #3b82f6)', background: isDanger ? '#ef4444' : 'var(--primary-color, #F25843)',
color: '#fff', color: '#fff',
cursor: 'pointer', cursor: 'pointer',
}} }}

View file

@ -99,6 +99,7 @@
/* Let child components handle their own scrolling for sticky headers */ /* Let child components handle their own scrolling for sticky headers */
overflow: hidden; overflow: hidden;
background: var(--bg-primary, #ffffff); background: var(--bg-primary, #ffffff);
color: var(--text-primary, #1a1a1a);
} }
/* Fills .content flex column so admin pages get a bounded height for inner scroll */ /* Fills .content flex column so admin pages get a bounded height for inner scroll */
@ -168,6 +169,7 @@
:global(.dark-theme) .content { :global(.dark-theme) .content {
background: var(--bg-dark, #0a0a0a); background: var(--bg-dark, #0a0a0a);
color: var(--text-primary, #e5e7eb);
} }
:global(.dark-theme) .mobileMenuButton { :global(.dark-theme) .mobileMenuButton {

View file

@ -550,8 +550,8 @@
.statusBadge.starting, .statusBadge.starting,
.statusBadge.running { .statusBadge.running {
background: #e3f2fd; background: var(--primary-dark-bg, rgba(242, 88, 67, 0.12));
color: #1976d2; color: var(--primary-color, #F25843);
} }
.statusBadge.completed { .statusBadge.completed {
@ -617,7 +617,7 @@
} }
.logStatus { .logStatus {
color: #1976d2; color: var(--primary-color, #F25843);
} }
.logEntryError .logStatus, .logEntryError .logStatus,

View file

@ -4,7 +4,7 @@
* 4-step invitation wizard: * 4-step invitation wizard:
* 1. Choose invite type: "Invite to mandate" OR "Invite to feature instance" * 1. Choose invite type: "Invite to mandate" OR "Invite to feature instance"
* 2. Select mandate (and feature instance if applicable) * 2. Select mandate (and feature instance if applicable)
* 3. Add invitees (email required, username optional; existing users; role per invitee) * 3. Add invitees (mindestens E-Mail oder Benutzername für neue Benutzer; bestehende Benutzer; Rolle pro Einladung)
* 4. Summary and send * 4. Summary and send
*/ */
@ -167,13 +167,24 @@ export const AdminInvitationWizardPage: React.FC = () => {
const addInviteeByEmail = () => { const addInviteeByEmail = () => {
const email = inviteeForm.email.trim(); const email = inviteeForm.email.trim();
if (!email) { const username = inviteeForm.username.trim();
setError('E-Mail ist erforderlich'); if (!email && !username) {
setError('Bitte mindestens eine E-Mail-Adresse oder einen Benutzernamen angeben.');
return;
}
const emailLower = email.toLowerCase();
const userLower = username.toLowerCase();
if (email && invitees.some(i => !i.isExisting && (i.email || '').toLowerCase() === emailLower)) {
setError('Diese E-Mail ist bereits in der Liste');
return;
}
if (username && invitees.some(i => !i.isExisting && (i.username || '').toLowerCase() === userLower)) {
setError('Dieser Benutzername ist bereits in der Liste');
return; return;
} }
setInvitees(prev => [...prev, { setInvitees(prev => [...prev, {
email, email,
username: undefined, username: username || undefined,
roleIds: [...inviteeForm.roleIds], roleIds: [...inviteeForm.roleIds],
isExisting: false, isExisting: false,
}]); }]);
@ -189,10 +200,6 @@ export const AdminInvitationWizardPage: React.FC = () => {
const user = allSystemUsers.find(u => u.id === selectedExistingUserId); const user = allSystemUsers.find(u => u.id === selectedExistingUserId);
if (!user) return; if (!user) return;
const email = (user.email || '').trim(); const email = (user.email || '').trim();
if (!email) {
setError('Dieser Benutzer hat keine E-Mail-Adresse');
return;
}
if (invitees.some(i => i.userId === user.id)) { if (invitees.some(i => i.userId === user.id)) {
setError('Dieser Benutzer ist bereits in der Liste'); setError('Dieser Benutzer ist bereits in der Liste');
return; return;
@ -232,8 +239,9 @@ export const AdminInvitationWizardPage: React.FC = () => {
const results: DispatchResult[] = []; const results: DispatchResult[] = [];
try { try {
for (const inv of invitees) { for (const inv of invitees) {
const emailTrim = (inv.email || '').trim();
const payload = { const payload = {
email: inv.email, ...(emailTrim ? { email: emailTrim } : {}),
targetUsername: inv.username || undefined, targetUsername: inv.username || undefined,
roleIds: inv.roleIds, roleIds: inv.roleIds,
expiresInHours: EXPIRES_IN_HOURS, expiresInHours: EXPIRES_IN_HOURS,
@ -244,14 +252,14 @@ export const AdminInvitationWizardPage: React.FC = () => {
const result = await createInvitation(selectedMandate.id, payload); const result = await createInvitation(selectedMandate.id, payload);
if (result.success) { if (result.success) {
results.push({ results.push({
email: inv.email, email: emailTrim,
username: inv.username, username: inv.username,
success: true, success: true,
emailSent: result.data?.emailSent, emailSent: result.data?.emailSent,
}); });
} else { } else {
results.push({ results.push({
email: inv.email, email: emailTrim,
username: inv.username, username: inv.username,
success: false, success: false,
error: result.error, error: result.error,
@ -452,7 +460,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
<div style={_cardStyle}> <div style={_cardStyle}>
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px' }}>Einladungen hinzufügen</h3> <h3 style={{ margin: '0 0 8px 0', fontSize: '16px' }}>Einladungen hinzufügen</h3>
<p style={{ color: 'var(--text-secondary)', fontSize: '13px', margin: '0 0 16px 0' }}> <p style={{ color: 'var(--text-secondary)', fontSize: '13px', margin: '0 0 16px 0' }}>
E-Mail ist erforderlich. Neue Benutzer legen ihren Benutzernamen beim Annehmen der Einladung selbst fest. Sie können neue Benutzer per E-Mail oder bestehende Benutzer hinzufügen. Für neue Benutzer: mindestens eine E-Mail <em>oder</em> ein Benutzername (vorgegeben). Ohne E-Mail wird kein Link per Mail versendet der Einladungslink kann manuell geteilt werden. Bestehende Benutzer wählen Sie im zweiten Tab.
</p> </p>
{/* Add form: toggle email vs existing */} {/* Add form: toggle email vs existing */}
@ -462,7 +470,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
style={{ fontSize: '12px', padding: '6px 12px' }} style={{ fontSize: '12px', padding: '6px 12px' }}
onClick={() => setAddMode('email')} onClick={() => setAddMode('email')}
> >
Per E-Mail (neue Benutzer) Neue Benutzer (E-Mail und/oder Benutzername)
</button> </button>
<button <button
className={addMode === 'existing' ? styles.primaryButton : styles.secondaryButton} className={addMode === 'existing' ? styles.primaryButton : styles.secondaryButton}
@ -476,7 +484,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
{addMode === 'email' ? ( {addMode === 'email' ? (
<div style={{ padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px', marginBottom: '16px', display: 'grid', gap: '12px' }}> <div style={{ padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px', marginBottom: '16px', display: 'grid', gap: '12px' }}>
<div> <div>
<label className={`${styles.formLabel} ${styles.required}`}>E-Mail *</label> <label className={styles.formLabel}>E-Mail (optional)</label>
<input <input
className={styles.formInput} className={styles.formInput}
type="email" type="email"
@ -484,8 +492,19 @@ export const AdminInvitationWizardPage: React.FC = () => {
onChange={e => setInviteeForm(p => ({ ...p, email: e.target.value }))} onChange={e => setInviteeForm(p => ({ ...p, email: e.target.value }))}
placeholder="beispiel@firma.com" placeholder="beispiel@firma.com"
/> />
</div>
<div>
<label className={styles.formLabel}>Benutzername (optional)</label>
<input
className={styles.formInput}
type="text"
autoComplete="off"
value={inviteeForm.username}
onChange={e => setInviteeForm(p => ({ ...p, username: e.target.value }))}
placeholder="z. B. vorname.nachname"
/>
<p style={{ fontSize: '11px', color: 'var(--text-secondary)', marginTop: '4px' }}> <p style={{ fontSize: '11px', color: 'var(--text-secondary)', marginTop: '4px' }}>
Der Benutzername wird vom eingeladenen Benutzer beim Annehmen der Einladung festgelegt. Mindestens eines der beiden Felder ausfüllen. Mit Benutzername muss der Eingeladene genau diesen Namen beim Annehmen verwenden.
</p> </p>
</div> </div>
{roles.length > 0 && ( {roles.length > 0 && (
@ -497,7 +516,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
display: 'flex', alignItems: 'center', gap: '6px', display: 'flex', alignItems: 'center', gap: '6px',
padding: '6px 12px', borderRadius: '6px', padding: '6px 12px', borderRadius: '6px',
background: inviteeForm.roleIds.includes(r.id) ? '#dbeafe' : 'var(--bg-secondary, #f8fafc)', background: inviteeForm.roleIds.includes(r.id) ? '#dbeafe' : 'var(--bg-secondary, #f8fafc)',
border: `1px solid ${inviteeForm.roleIds.includes(r.id) ? 'var(--primary-color, #3b82f6)' : 'var(--border-color, #e2e8f0)'}`, border: `1px solid ${inviteeForm.roleIds.includes(r.id) ? 'var(--primary-color, #F25843)' : 'var(--border-color, #e2e8f0)'}`,
fontSize: '12px', cursor: 'pointer', fontSize: '12px', cursor: 'pointer',
}}> }}>
<input <input
@ -519,7 +538,10 @@ export const AdminInvitationWizardPage: React.FC = () => {
<button <button
className={styles.primaryButton} className={styles.primaryButton}
onClick={addInviteeByEmail} onClick={addInviteeByEmail}
disabled={!inviteeForm.email.trim() || (roles.length > 0 && inviteeForm.roleIds.length === 0)} disabled={
(!inviteeForm.email.trim() && !inviteeForm.username.trim())
|| (roles.length > 0 && inviteeForm.roleIds.length === 0)
}
> >
Hinzufügen Hinzufügen
</button> </button>
@ -552,7 +574,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
display: 'flex', alignItems: 'center', gap: '6px', display: 'flex', alignItems: 'center', gap: '6px',
padding: '6px 12px', borderRadius: '6px', padding: '6px 12px', borderRadius: '6px',
background: inviteeForm.roleIds.includes(r.id) ? '#dbeafe' : 'var(--bg-secondary, #f8fafc)', background: inviteeForm.roleIds.includes(r.id) ? '#dbeafe' : 'var(--bg-secondary, #f8fafc)',
border: `1px solid ${inviteeForm.roleIds.includes(r.id) ? 'var(--primary-color, #3b82f6)' : 'var(--border-color, #e2e8f0)'}`, border: `1px solid ${inviteeForm.roleIds.includes(r.id) ? 'var(--primary-color, #F25843)' : 'var(--border-color, #e2e8f0)'}`,
fontSize: '12px', cursor: 'pointer', fontSize: '12px', cursor: 'pointer',
}}> }}>
<input <input
@ -586,7 +608,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}> <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
<thead> <thead>
<tr style={{ borderBottom: '2px solid var(--border-color, #C5D9E8)' }}> <tr style={{ borderBottom: '2px solid var(--border-color, #C5D9E8)' }}>
<th style={{ textAlign: 'left', padding: '8px' }}>E-Mail</th> <th style={{ textAlign: 'left', padding: '8px' }}>E-Mail / Benutzer</th>
<th style={{ textAlign: 'left', padding: '8px' }}>Benutzername</th> <th style={{ textAlign: 'left', padding: '8px' }}>Benutzername</th>
<th style={{ textAlign: 'left', padding: '8px' }}>Rollen</th> <th style={{ textAlign: 'left', padding: '8px' }}>Rollen</th>
<th style={{ textAlign: 'left', padding: '8px' }}>Typ</th> <th style={{ textAlign: 'left', padding: '8px' }}>Typ</th>
@ -596,14 +618,16 @@ export const AdminInvitationWizardPage: React.FC = () => {
<tbody> <tbody>
{invitees.map((inv, idx) => ( {invitees.map((inv, idx) => (
<tr key={idx} style={{ borderBottom: '1px solid var(--bg-secondary, #f1f5f9)' }}> <tr key={idx} style={{ borderBottom: '1px solid var(--bg-secondary, #f1f5f9)' }}>
<td style={{ padding: '8px' }}>{inv.email}</td> <td style={{ padding: '8px' }}>{inv.email || '—'}</td>
<td style={{ padding: '8px', color: 'var(--text-secondary)' }}>{inv.isExisting ? inv.username : ''}</td> <td style={{ padding: '8px', color: 'var(--text-secondary)' }}>
{inv.username || ''}
</td>
<td style={{ padding: '8px' }}> <td style={{ padding: '8px' }}>
{inv.roleIds.length > 0 {inv.roleIds.length > 0
? roles.filter(r => inv.roleIds.includes(r.id)).map(r => r.roleLabel).join(', ') ? roles.filter(r => inv.roleIds.includes(r.id)).map(r => r.roleLabel).join(', ')
: '-'} : '-'}
</td> </td>
<td style={{ padding: '8px', fontSize: '12px' }}>{inv.isExisting ? 'Bestehend' : 'Neu'}</td> <td style={{ padding: '8px', fontSize: '12px' }}>{inv.isExisting ? 'Bestehend' : 'Neu (Einladung)'}</td>
<td style={{ padding: '8px', textAlign: 'right' }}> <td style={{ padding: '8px', textAlign: 'right' }}>
<button <button
onClick={() => removeInvitee(idx)} onClick={() => removeInvitee(idx)}
@ -654,7 +678,8 @@ export const AdminInvitationWizardPage: React.FC = () => {
<ul style={{ margin: '8px 0 0 20px', padding: 0 }}> <ul style={{ margin: '8px 0 0 20px', padding: 0 }}>
{invitees.map((inv, i) => ( {invitees.map((inv, i) => (
<li key={i} style={{ marginBottom: '4px' }}> <li key={i} style={{ marginBottom: '4px' }}>
{inv.email}{inv.isExisting && inv.username ? ` (${inv.username})` : ''} {[inv.email || null, inv.username ? `@${inv.username}` : null].filter(Boolean).join(' · ')
|| '—'}
{inv.roleIds.length > 0 && ` ${roles.filter(r => inv.roleIds.includes(r.id)).map(r => r.roleLabel).join(', ')}`} {inv.roleIds.length > 0 && ` ${roles.filter(r => inv.roleIds.includes(r.id)).map(r => r.roleLabel).join(', ')}`}
</li> </li>
))} ))}
@ -680,7 +705,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}> <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
<thead> <thead>
<tr style={{ borderBottom: '2px solid var(--border-color, #C5D9E8)' }}> <tr style={{ borderBottom: '2px solid var(--border-color, #C5D9E8)' }}>
<th style={{ textAlign: 'left', padding: '8px' }}>E-Mail</th> <th style={{ textAlign: 'left', padding: '8px' }}>E-Mail / Benutzer</th>
<th style={{ textAlign: 'left', padding: '8px' }}>Status</th> <th style={{ textAlign: 'left', padding: '8px' }}>Status</th>
<th style={{ textAlign: 'left', padding: '8px' }}>E-Mail gesendet</th> <th style={{ textAlign: 'left', padding: '8px' }}>E-Mail gesendet</th>
</tr> </tr>
@ -688,7 +713,11 @@ export const AdminInvitationWizardPage: React.FC = () => {
<tbody> <tbody>
{dispatchResults.map((r, idx) => ( {dispatchResults.map((r, idx) => (
<tr key={idx} style={{ borderBottom: '1px solid var(--bg-secondary, #f1f5f9)' }}> <tr key={idx} style={{ borderBottom: '1px solid var(--bg-secondary, #f1f5f9)' }}>
<td style={{ padding: '8px' }}>{r.email}{r.username ? ` (${r.username})` : ''}</td> <td style={{ padding: '8px' }}>
{(r.email || '').trim() && r.username
? `${(r.email || '').trim()} (@${r.username})`
: (r.email || '').trim() || (r.username ? `@${r.username}` : '—')}
</td>
<td style={{ padding: '8px' }}> <td style={{ padding: '8px' }}>
<span style={{ <span style={{
padding: '2px 8px', borderRadius: '4px', fontSize: '12px', padding: '2px 8px', borderRadius: '4px', fontSize: '12px',

View file

@ -13,6 +13,9 @@ import { FaSync, FaPlug, FaGoogle, FaMicrosoft, FaLink, FaRedo, FaShieldAlt, FaT
import { getApiBaseUrl } from '../../../config/config'; import { getApiBaseUrl } from '../../../config/config';
import styles from '../admin/Admin.module.css'; import styles from '../admin/Admin.module.css';
/** Wenn false: keine neue ClickUp-Verbindung über diese Seite (Buttons inaktiv). */
const isClickupConnectionUiEnabled = false;
export const ConnectionsPage: React.FC = () => { export const ConnectionsPage: React.FC = () => {
// Use the consolidated hook // Use the consolidated hook
const { const {
@ -190,8 +193,9 @@ export const ConnectionsPage: React.FC = () => {
} }
}; };
// Handle create ClickUp connection // Handle create ClickUp connection (UI kann per Flag abgeschaltet sein)
const handleCreateClickup = async () => { const handleCreateClickup = async () => {
if (!isClickupConnectionUiEnabled) return;
try { try {
await createClickupConnectionAndAuth(); await createClickupConnectionAndAuth();
refetch(); refetch();
@ -245,7 +249,8 @@ export const ConnectionsPage: React.FC = () => {
<div> <div>
<h1 className={styles.pageTitle}>Verbindungen</h1> <h1 className={styles.pageTitle}>Verbindungen</h1>
<p className={styles.pageSubtitle}> <p className={styles.pageSubtitle}>
Persönliche Datenanbindungen verwalten (OAuth: Google, Microsoft, ClickUp) Persönliche Datenanbindungen verwalten (OAuth: Google, Microsoft
{isClickupConnectionUiEnabled ? ', ClickUp' : ''})
</p> </p>
</div> </div>
<div className={styles.headerActions}> <div className={styles.headerActions}>
@ -280,14 +285,17 @@ export const ConnectionsPage: React.FC = () => {
> >
<FaMicrosoft /> Microsoft <FaMicrosoft /> Microsoft
</button> </button>
<button {isClickupConnectionUiEnabled && (
className={styles.clickupButton} <button
onClick={handleCreateClickup} type="button"
disabled={isConnecting} className={styles.clickupButton}
title="ClickUp-Konto verbinden" onClick={handleCreateClickup}
> disabled={isConnecting}
<FaTasks /> ClickUp title="ClickUp-Konto verbinden"
</button> >
<FaTasks /> ClickUp
</button>
)}
</> </>
)} )}
</div> </div>
@ -304,7 +312,9 @@ export const ConnectionsPage: React.FC = () => {
<FaPlug className={styles.emptyIcon} /> <FaPlug className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>Keine Verbindungen vorhanden</h3> <h3 className={styles.emptyTitle}>Keine Verbindungen vorhanden</h3>
<p className={styles.emptyDescription}> <p className={styles.emptyDescription}>
Verbinden Sie Ihr Google-, Microsoft- oder ClickUp-Konto, um loszulegen. {isClickupConnectionUiEnabled
? 'Verbinden Sie Ihr Google-, Microsoft- oder ClickUp-Konto, um loszulegen.'
: 'Verbinden Sie Ihr Google- oder Microsoft-Konto, um loszulegen.'}
</p> </p>
{canCreate && ( {canCreate && (
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center', flexWrap: 'wrap' }}>
@ -322,13 +332,16 @@ export const ConnectionsPage: React.FC = () => {
> >
<FaMicrosoft /> Mit Microsoft verbinden <FaMicrosoft /> Mit Microsoft verbinden
</button> </button>
<button {isClickupConnectionUiEnabled && (
className={styles.clickupButton} <button
onClick={handleCreateClickup} type="button"
disabled={isConnecting} className={styles.clickupButton}
> onClick={handleCreateClickup}
<FaTasks /> Mit ClickUp verbinden disabled={isConnecting}
</button> >
<FaTasks /> Mit ClickUp verbinden
</button>
)}
</div> </div>
)} )}
</div> </div>
@ -362,7 +375,9 @@ export const ConnectionsPage: React.FC = () => {
icon: <FaLink />, icon: <FaLink />,
onClick: handleConnect, onClick: handleConnect,
title: 'Verbinden', title: 'Verbinden',
visible: (row: Connection) => row.status !== 'active', visible: (row: Connection) =>
row.status !== 'active' &&
(isClickupConnectionUiEnabled || row.authority !== 'clickup'),
loading: () => isConnecting, loading: () => isConnecting,
}, },
{ {

View file

@ -381,7 +381,7 @@ export const FilesPage: React.FC = () => {
style={{ style={{
width: 6, width: 6,
cursor: 'col-resize', cursor: 'col-resize',
background: isDragging ? 'var(--color-primary, #1976d2)' : 'transparent', background: isDragging ? 'var(--primary-dark-bg, rgba(242, 88, 67, 0.2))' : 'transparent',
transition: isDragging ? 'none' : 'background 0.15s', transition: isDragging ? 'none' : 'background 0.15s',
flexShrink: 0, flexShrink: 0,
zIndex: 10, zIndex: 10,

View file

@ -676,7 +676,7 @@ export const BillingAdmin: React.FC = () => {
padding: '8px 16px', padding: '8px 16px',
textDecoration: 'none', textDecoration: 'none',
borderRadius: '4px', borderRadius: '4px',
backgroundColor: isActive ? 'var(--color-primary, #3b82f6)' : 'transparent', backgroundColor: isActive ? 'var(--primary-color, #F25843)' : 'transparent',
color: isActive ? 'white' : 'var(--color-text, #e0e0e0)', color: isActive ? 'white' : 'var(--color-text, #e0e0e0)',
fontWeight: isActive ? 600 : 400, fontWeight: isActive ? 600 : 400,
cursor: 'pointer', cursor: 'pointer',

View file

@ -128,7 +128,7 @@ const TabNav: React.FC<TabNavProps> = ({ activeTab, onTabChange }) => {
padding: '8px 16px', padding: '8px 16px',
textDecoration: 'none', textDecoration: 'none',
borderRadius: '4px', borderRadius: '4px',
backgroundColor: isActive ? 'var(--color-primary, #3b82f6)' : 'transparent', backgroundColor: isActive ? 'var(--primary-color, #F25843)' : 'transparent',
color: isActive ? 'white' : 'var(--color-text, #e0e0e0)', color: isActive ? 'white' : 'var(--color-text, #e0e0e0)',
fontWeight: isActive ? 600 : 400, fontWeight: isActive ? 600 : 400,
cursor: 'pointer', cursor: 'pointer',
@ -646,7 +646,7 @@ export const BillingDataView: React.FC = () => {
? 'var(--color-error, #ef4444)' ? 'var(--color-error, #ef4444)'
: pct >= 70 : pct >= 70
? 'var(--color-warning, #f59e0b)' ? 'var(--color-warning, #f59e0b)'
: 'var(--primary-color, #3b82f6)'; : 'var(--primary-color, #F25843)';
return ( return (
<div key={sv.mandateId} className={styles.balanceCard}> <div key={sv.mandateId} className={styles.balanceCard}>
<h3 className={styles.mandateName}>{sv.mandateName}</h3> <h3 className={styles.mandateName}>{sv.mandateName}</h3>
@ -726,7 +726,7 @@ export const BillingDataView: React.FC = () => {
? 'var(--color-error, #ef4444)' ? 'var(--color-error, #ef4444)'
: pct >= 70 : pct >= 70
? 'var(--color-warning, #f59e0b)' ? 'var(--color-warning, #f59e0b)'
: 'var(--primary-color, #3b82f6)'; : 'var(--primary-color, #F25843)';
return ( return (
<div key={sv.mandateId} style={{ <div key={sv.mandateId} style={{

View file

@ -44,7 +44,7 @@ const _statusLabel: Record<string, { label: string; color: string }> = {
PENDING: { label: 'Zahlung ausstehend', color: '#f59e0b' }, PENDING: { label: 'Zahlung ausstehend', color: '#f59e0b' },
SCHEDULED: { label: 'Geplant', color: '#8b5cf6' }, SCHEDULED: { label: 'Geplant', color: '#8b5cf6' },
ACTIVE: { label: 'Aktiv', color: '#22c55e' }, ACTIVE: { label: 'Aktiv', color: '#22c55e' },
TRIALING: { label: 'Testphase', color: '#3b82f6' }, TRIALING: { label: 'Testphase', color: '#38bdf8' },
PAST_DUE: { label: 'Zahlung ausstehend', color: '#f59e0b' }, PAST_DUE: { label: 'Zahlung ausstehend', color: '#f59e0b' },
EXPIRED: { label: 'Abgelaufen', color: '#6b7280' }, EXPIRED: { label: 'Abgelaufen', color: '#6b7280' },
}; };
@ -75,13 +75,13 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, isCurrent, onActivate, activa
return ( return (
<div style={{ <div style={{
border: isCurrent ? '2px solid var(--color-primary, #3b82f6)' : '1px solid var(--color-border, #333)', border: isCurrent ? '2px solid var(--primary-color, #F25843)' : '1px solid var(--color-border, var(--border-color, #333))',
borderRadius: '8px', borderRadius: '8px',
padding: '1.25rem', padding: '1.25rem',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: '0.75rem', gap: '0.75rem',
background: isCurrent ? 'rgba(59,130,246,0.06)' : 'var(--color-surface, #1a1a2e)', background: isCurrent ? 'var(--primary-dark-bg, rgba(242, 88, 67, 0.12))' : 'var(--surface-color, var(--bg-secondary, #1a1a2e))',
minWidth: 220, minWidth: 220,
}}> }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
@ -89,12 +89,12 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, isCurrent, onActivate, activa
{isCurrent && ( {isCurrent && (
<span style={{ <span style={{
fontSize: '0.7rem', padding: '2px 8px', borderRadius: '4px', fontSize: '0.7rem', padding: '2px 8px', borderRadius: '4px',
background: 'var(--color-primary, #3b82f6)', color: '#fff', fontWeight: 600, background: 'var(--primary-color, #F25843)', color: '#fff', fontWeight: 600,
}}>Aktuell</span> }}>Aktuell</span>
)} )}
</div> </div>
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary, #888)', margin: 0 }}> <p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', margin: 0 }}>
{_t(plan.description)} {_t(plan.description)}
</p> </p>
@ -102,7 +102,7 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, isCurrent, onActivate, activa
<div style={{ fontSize: '0.85rem' }}> <div style={{ fontSize: '0.85rem' }}>
<div>User: <strong>{_formatCurrency(plan.pricePerUserCHF)}</strong> / {_periodLabel[plan.billingPeriod] || plan.billingPeriod}</div> <div>User: <strong>{_formatCurrency(plan.pricePerUserCHF)}</strong> / {_periodLabel[plan.billingPeriod] || plan.billingPeriod}</div>
<div>Instanz: <strong>{_formatCurrency(plan.pricePerFeatureInstanceCHF)}</strong> / {_periodLabel[plan.billingPeriod] || plan.billingPeriod}</div> <div>Instanz: <strong>{_formatCurrency(plan.pricePerFeatureInstanceCHF)}</strong> / {_periodLabel[plan.billingPeriod] || plan.billingPeriod}</div>
<div style={{ marginTop: '0.35rem', color: 'var(--text-secondary, #888)' }}> <div style={{ marginTop: '0.35rem', color: 'var(--text-secondary)' }}>
AI-Budget (inkl.): <strong>{_formatCurrency(plan.budgetAiCHF ?? 0)}</strong> / Periode AI-Budget (inkl.): <strong>{_formatCurrency(plan.budgetAiCHF ?? 0)}</strong> / Periode
{' · '} {' · '}
Speicher (inkl.):{' '} Speicher (inkl.):{' '}
@ -112,7 +112,7 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, isCurrent, onActivate, activa
: formatBinaryDataSizeFromMebibytes(plan.maxDataVolumeMB)} : formatBinaryDataSizeFromMebibytes(plan.maxDataVolumeMB)}
</strong> </strong>
</div> </div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary, #888)', marginTop: '0.25rem' }}> <div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginTop: '0.25rem' }}>
Speicher über Plan: {_formatCurrency(storageOverageChfPerGbMonth)} / GB / Monat Speicher über Plan: {_formatCurrency(storageOverageChfPerGbMonth)} / GB / Monat
</div> </div>
</div> </div>
@ -140,7 +140,7 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, isCurrent, onActivate, activa
disabled={!!activatingPlanKey} disabled={!!activatingPlanKey}
style={{ style={{
marginTop: 'auto', padding: '8px 16px', borderRadius: '6px', border: 'none', marginTop: 'auto', padding: '8px 16px', borderRadius: '6px', border: 'none',
background: 'var(--color-primary, #3b82f6)', color: '#fff', fontWeight: 600, background: 'var(--primary-color, #F25843)', color: '#fff', fontWeight: 600,
cursor: activatingPlanKey ? 'wait' : 'pointer', cursor: activatingPlanKey ? 'wait' : 'pointer',
opacity: activatingPlanKey ? 0.6 : 1, opacity: activatingPlanKey ? 0.6 : 1,
}} }}
@ -177,15 +177,15 @@ const SubInfoCard: React.FC<SubInfoProps> = ({ sub, plan, label, onCancel, onRea
return ( return (
<div style={{ <div style={{
border: '1px solid var(--color-border, #333)', border: '1px solid var(--color-border, var(--border-color, #333))',
borderRadius: '8px', borderRadius: '8px',
padding: '1.25rem', padding: '1.25rem',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: '0.5rem', gap: '0.5rem',
background: 'var(--color-surface, #1a1a2e)', background: 'var(--surface-color, var(--bg-secondary, #1a1a2e))',
}}> }}>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary, #888)', textTransform: 'uppercase', letterSpacing: '0.05em' }}> <div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{label} {label}
</div> </div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
@ -229,7 +229,7 @@ const SubInfoCard: React.FC<SubInfoProps> = ({ sub, plan, label, onCancel, onRea
{!isPending && !isScheduled && ( {!isPending && !isScheduled && (
<div style={{ <div style={{
fontSize: '0.85rem', color: 'var(--text-secondary, #888)', fontSize: '0.85rem', color: 'var(--text-secondary)',
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.25rem 1rem', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.25rem 1rem',
}}> }}>
<span>Gestartet: {_formatDate(sub.startedAt)}</span> <span>Gestartet: {_formatDate(sub.startedAt)}</span>
@ -263,7 +263,7 @@ const SubInfoCard: React.FC<SubInfoProps> = ({ sub, plan, label, onCancel, onRea
disabled={reactivating} disabled={reactivating}
style={{ style={{
padding: '6px 14px', borderRadius: '6px', border: 'none', padding: '6px 14px', borderRadius: '6px', border: 'none',
background: 'var(--color-primary, #3b82f6)', color: '#fff', background: 'var(--primary-color, #F25843)', color: '#fff',
fontWeight: 600, cursor: reactivating ? 'wait' : 'pointer', fontSize: '0.85rem', fontWeight: 600, cursor: reactivating ? 'wait' : 'pointer', fontSize: '0.85rem',
}} }}
> >
@ -443,9 +443,9 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
{checkoutMessage && ( {checkoutMessage && (
<div style={{ <div style={{
marginBottom: '1rem', padding: '0.75rem 1rem', borderRadius: '6px', marginBottom: '1rem', padding: '0.75rem 1rem', borderRadius: '6px',
background: checkoutMessage.type === 'success' ? 'rgba(34,197,94,0.12)' : 'rgba(59,130,246,0.12)', background: checkoutMessage.type === 'success' ? 'rgba(34,197,94,0.12)' : 'var(--primary-dark-bg, rgba(242, 88, 67, 0.12))',
border: `1px solid ${checkoutMessage.type === 'success' ? '#22c55e' : '#3b82f6'}`, border: `1px solid ${checkoutMessage.type === 'success' ? '#22c55e' : 'var(--primary-color, #F25843)'}`,
color: checkoutMessage.type === 'success' ? '#22c55e' : '#3b82f6', color: checkoutMessage.type === 'success' ? '#22c55e' : 'var(--primary-light, #F25843)',
fontSize: '0.9rem', fontSize: '0.9rem',
}}> }}>
{checkoutMessage.text} {checkoutMessage.text}

View file

@ -125,7 +125,7 @@ export const ChatStream: React.FC<ChatStreamProps> = ({
</td> </td>
), ),
a: ({ href, children }) => ( a: ({ href, children }) => (
<a href={href} target="_blank" rel="noopener noreferrer" style={{ color: '#1976d2' }}> <a href={href} target="_blank" rel="noopener noreferrer" style={{ color: 'var(--primary-color, #F25843)' }}>
{children} {children}
</a> </a>
), ),
@ -222,7 +222,7 @@ export const ChatStream: React.FC<ChatStreamProps> = ({
onClick={onOpenEditor} onClick={onOpenEditor}
style={{ style={{
padding: '5px 14px', borderRadius: 4, border: 'none', padding: '5px 14px', borderRadius: 4, border: 'none',
background: 'var(--primary-color, #1976d2)', color: '#fff', background: 'var(--primary-color, #F25843)', color: '#fff',
cursor: 'pointer', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontSize: 12, fontWeight: 600,
}} }}
> >
@ -280,7 +280,7 @@ export const ChatStream: React.FC<ChatStreamProps> = ({
}}> }}>
<span className="workspace-spinner" style={{ <span className="workspace-spinner" style={{
display: 'inline-block', width: 12, height: 12, display: 'inline-block', width: 12, height: 12,
border: '2px solid #ccc', borderTopColor: '#1976d2', border: '2px solid var(--border-color, #ccc)', borderTopColor: 'var(--primary-color, #F25843)',
borderRadius: '50%', animation: 'workspace-spin 0.8s linear infinite', borderRadius: '50%', animation: 'workspace-spin 0.8s linear infinite',
}} /> }} />
Processing... Processing...
@ -373,7 +373,7 @@ function _FileCard({ doc }: { doc: MessageDocument }) {
{ext.toUpperCase()}{sizeLabel ? ` \u00b7 ${sizeLabel}` : ''} {ext.toUpperCase()}{sizeLabel ? ` \u00b7 ${sizeLabel}` : ''}
</div> </div>
</div> </div>
<span style={{ fontSize: 14, color: '#1976d2' }} title="Download">&#x2B07;</span> <span style={{ fontSize: 14, color: 'var(--primary-color, #F25843)' }} title="Download">&#x2B07;</span>
</div> </div>
); );
} }
@ -455,7 +455,7 @@ function _AudioPlayer({ url, language }: { url: string; language?: string; charC
onClick={_togglePlay} onClick={_togglePlay}
style={{ style={{
width: 32, height: 32, borderRadius: '50%', border: 'none', width: 32, height: 32, borderRadius: '50%', border: 'none',
background: 'var(--primary-color, #1976d2)', color: '#fff', background: 'var(--primary-color, #F25843)', color: '#fff',
cursor: 'pointer', fontSize: 14, cursor: 'pointer', fontSize: 14,
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0, flexShrink: 0,
@ -473,7 +473,7 @@ function _AudioPlayer({ url, language }: { url: string; language?: string; charC
}}> }}>
<div style={{ <div style={{
height: '100%', borderRadius: 2, height: '100%', borderRadius: 2,
background: 'var(--primary-color, #1976d2)', background: 'var(--primary-color, #F25843)',
width: `${progress * 100}%`, width: `${progress * 100}%`,
transition: 'width 0.2s', transition: 'width 0.2s',
}} /> }} />

View file

@ -236,7 +236,7 @@ const _EditorTab: React.FC<{
padding: '6px 16px', padding: '6px 16px',
fontSize: 13, fontSize: 13,
border: 'none', border: 'none',
borderBottom: isActive ? '2px solid var(--primary-color, #1976d2)' : '2px solid transparent', borderBottom: isActive ? '2px solid var(--primary-color, #F25843)' : '2px solid transparent',
background: isActive ? 'var(--bg-primary, #fff)' : 'transparent', background: isActive ? 'var(--bg-primary, #fff)' : 'transparent',
cursor: 'pointer', cursor: 'pointer',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',

View file

@ -324,8 +324,8 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
borderTop: '1px solid var(--border-color, #e0e0e0)', borderTop: '1px solid var(--border-color, #e0e0e0)',
position: 'relative', position: 'relative',
flexShrink: 0, flexShrink: 0,
outline: treeDropOver ? '2px dashed #1976d2' : 'none', outline: treeDropOver ? '2px dashed var(--primary-color, #F25843)' : 'none',
background: treeDropOver ? 'rgba(25, 118, 210, 0.04)' : undefined, background: treeDropOver ? 'var(--primary-dark-bg, rgba(242, 88, 67, 0.08))' : undefined,
transition: 'background 0.15s, outline 0.15s', transition: 'background 0.15s, outline 0.15s',
}} }}
onDragOver={_handlePromptDragOver} onDragOver={_handlePromptDragOver}
@ -534,7 +534,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
style={{ style={{
width: _controlSize, height: _controlSize, borderRadius: 8, border: '1px solid var(--border-color, #ddd)', width: _controlSize, height: _controlSize, borderRadius: 8, border: '1px solid var(--border-color, #ddd)',
background: 'var(--secondary-bg, #f5f5f5)', background: 'var(--secondary-bg, #f5f5f5)',
color: uploading ? '#1976d2' : '#666', color: uploading ? 'var(--primary-color, #F25843)' : 'var(--text-secondary, #666)',
cursor: uploading || isProcessing ? 'not-allowed' : 'pointer', cursor: uploading || isProcessing ? 'not-allowed' : 'pointer',
fontSize: 16, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 16, display: 'flex', alignItems: 'center', justifyContent: 'center',
opacity: isProcessing ? 0.5 : 1, opacity: isProcessing ? 0.5 : 1,
@ -706,10 +706,10 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
}} }}
style={{ style={{
padding: '8px 12px', cursor: 'pointer', fontSize: 13, padding: '8px 12px', cursor: 'pointer', fontSize: 13,
background: lang.code === voiceLanguage ? 'var(--primary-color, #1976d2)' : 'transparent', background: lang.code === voiceLanguage ? 'var(--primary-color, #F25843)' : 'transparent',
color: lang.code === voiceLanguage ? '#fff' : 'var(--text-primary, #333)', color: lang.code === voiceLanguage ? '#fff' : 'var(--text-primary, #333)',
}} }}
onMouseEnter={e => { if (lang.code !== voiceLanguage) e.currentTarget.style.background = '#f5f5f5'; }} onMouseEnter={e => { if (lang.code !== voiceLanguage) e.currentTarget.style.background = 'var(--bg-secondary, #f5f5f5)'; }}
onMouseLeave={e => { if (lang.code !== voiceLanguage) e.currentTarget.style.background = ''; }} onMouseLeave={e => { if (lang.code !== voiceLanguage) e.currentTarget.style.background = ''; }}
> >
{lang.label} ({lang.code}) {lang.label} ({lang.code})
@ -751,7 +751,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
disabled={!prompt.trim()} disabled={!prompt.trim()}
style={{ style={{
padding: '10px 20px', borderRadius: 8, border: 'none', padding: '10px 20px', borderRadius: 8, border: 'none',
background: prompt.trim() ? 'var(--primary-color, #1976d2)' : '#ccc', background: prompt.trim() ? 'var(--primary-color, #F25843)' : 'var(--color-gray-disabled, #ccc)',
color: '#fff', cursor: prompt.trim() ? 'pointer' : 'default', fontWeight: 600, color: '#fff', cursor: prompt.trim() ? 'pointer' : 'default', fontWeight: 600,
minWidth: isMobile ? 84 : undefined, minWidth: isMobile ? 84 : undefined,
}} }}

View file

@ -220,12 +220,12 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
flex: 1, flex: 1,
padding: '6px 0', padding: '6px 0',
border: 'none', border: 'none',
borderBottom: active ? '2px solid var(--primary-color, #1976d2)' : '2px solid transparent', borderBottom: active ? '2px solid var(--primary-color, #F25843)' : '2px solid transparent',
background: 'none', background: 'none',
cursor: 'pointer', cursor: 'pointer',
fontSize: 11, fontSize: 11,
fontWeight: active ? 600 : 400, fontWeight: active ? 600 : 400,
color: active ? 'var(--primary-color, #1976d2)' : '#888', color: active ? 'var(--primary-color, #F25843)' : 'var(--text-tertiary, #888)',
textTransform: 'uppercase' as const, textTransform: 'uppercase' as const,
}); });
@ -326,7 +326,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
<div <div
onMouseDown={e => _leftResize.onMouseDown(e, 1)} onMouseDown={e => _leftResize.onMouseDown(e, 1)}
style={{ width: 4, cursor: 'col-resize', background: 'transparent', flexShrink: 0 }} style={{ width: 4, cursor: 'col-resize', background: 'transparent', flexShrink: 0 }}
onMouseEnter={e => (e.currentTarget.style.background = '#1976d2')} onMouseEnter={e => (e.currentTarget.style.background = 'var(--primary-color, #F25843)')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
/> />
)} )}
@ -377,7 +377,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
padding: '6px 10px', padding: '6px 10px',
borderRadius: 8, borderRadius: 8,
border: '1px solid var(--border-color, #ddd)', border: '1px solid var(--border-color, #ddd)',
background: rightTab === 'activity' ? '#e8f3ff' : '#f7f7f7', background: rightTab === 'activity' ? 'var(--primary-dark-bg, rgba(242, 88, 67, 0.1))' : '#f7f7f7',
cursor: 'pointer', cursor: 'pointer',
fontSize: 12, fontSize: 12,
}} }}
@ -390,7 +390,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
padding: '6px 10px', padding: '6px 10px',
borderRadius: 8, borderRadius: 8,
border: '1px solid var(--border-color, #ddd)', border: '1px solid var(--border-color, #ddd)',
background: rightTab === 'preview' ? '#e8f3ff' : '#f7f7f7', background: rightTab === 'preview' ? 'var(--primary-dark-bg, rgba(242, 88, 67, 0.1))' : '#f7f7f7',
cursor: 'pointer', cursor: 'pointer',
fontSize: 12, fontSize: 12,
}} }}
@ -402,10 +402,10 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
{isDragOver && ( {isDragOver && (
<div style={{ <div style={{
position: 'absolute', inset: 0, zIndex: 100, position: 'absolute', inset: 0, zIndex: 100,
background: 'rgba(25, 118, 210, 0.08)', background: 'var(--primary-dark-bg, rgba(242, 88, 67, 0.1))',
border: '2px dashed #1976d2', borderRadius: 8, border: '2px dashed var(--primary-color, #F25843)', borderRadius: 8,
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 16, fontWeight: 600, color: '#1976d2', fontSize: 16, fontWeight: 600, color: 'var(--primary-color, #F25843)',
pointerEvents: 'none', pointerEvents: 'none',
}}> }}>
Dateien hier ablegen Dateien hier ablegen
@ -452,7 +452,7 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
<div <div
onMouseDown={e => _rightResize.onMouseDown(e, -1)} onMouseDown={e => _rightResize.onMouseDown(e, -1)}
style={{ width: 4, cursor: 'col-resize', background: 'transparent', flexShrink: 0 }} style={{ width: 4, cursor: 'col-resize', background: 'transparent', flexShrink: 0 }}
onMouseEnter={e => (e.currentTarget.style.background = '#1976d2')} onMouseEnter={e => (e.currentTarget.style.background = 'var(--primary-color, #F25843)')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
/> />
)} )}

View file

@ -46,14 +46,14 @@ export const WorkspaceSettingsPage: React.FC = () => {
padding: '10px 20px', padding: '10px 20px',
border: 'none', border: 'none',
borderBottom: activeTab === tab.key borderBottom: activeTab === tab.key
? '2px solid var(--primary-color, #1976d2)' ? '2px solid var(--primary-color, #F25843)'
: '2px solid transparent', : '2px solid transparent',
background: 'none', background: 'none',
cursor: 'pointer', cursor: 'pointer',
fontSize: 14, fontSize: 14,
fontWeight: activeTab === tab.key ? 600 : 400, fontWeight: activeTab === tab.key ? 600 : 400,
color: activeTab === tab.key color: activeTab === tab.key
? 'var(--primary-color, #1976d2)' ? 'var(--primary-color, #F25843)'
: 'var(--text-secondary, #888)', : 'var(--text-secondary, #888)',
}} }}
> >

View file

@ -82,6 +82,15 @@
/* Error color */ /* Error color */
--error-color: #dc2626; --error-color: #dc2626;
/* Legacy / inline-style aliases (override :root beige --color-primary) */
--color-primary: var(--primary-color);
--color-primary-hover: var(--primary-color-dark);
--color-primary-disabled: var(--primary-color-light);
--color-border: var(--border-color);
--bg-card: var(--bg-primary);
--bg-input: #ffffff;
--bg-hover: var(--hover-bg);
} }
/* ============================================== */ /* ============================================== */
@ -92,9 +101,9 @@
--color-surface: #1E1D1A; --color-surface: #1E1D1A;
--color-text: #E5E7EB; --color-text: #E5E7EB;
--color-primary: #C7C5B2; --color-primary: var(--primary-color);
--color-primary-hover: #E0DECC; --color-primary-hover: var(--primary-color-dark);
--color-primary-disabled: #59584F; --color-primary-disabled: rgba(242, 88, 67, 0.35);
--color-secondary: #F25843; --color-secondary: #F25843;
--color-secondary-hover: #FF715C; --color-secondary-hover: #FF715C;
@ -108,9 +117,10 @@
--color-secondary-red-hover: #E17683; --color-secondary-red-hover: #E17683;
--color-secondary-red-disabled: #70363C; --color-secondary-red-disabled: #70363C;
--color-gray: #181818; /* Readable neutrals on dark (was #181818 — same as bg, illegible) */
--color-gray-hover: #2E2E2E; --color-gray: #9ca3af;
--color-gray-disabled: #505050; --color-gray-hover: #d1d5db;
--color-gray-disabled: #57534e;
/* Background colors */ /* Background colors */
--bg-primary: #181818; --bg-primary: #181818;
@ -146,4 +156,9 @@
/* Error color */ /* Error color */
--error-color: #ef4444; --error-color: #ef4444;
--color-border: var(--border-color);
--bg-card: #252422;
--bg-input: #2c2926;
--bg-hover: rgba(255, 255, 255, 0.08);
} }