Compare commits

...

7 Commits

Author SHA1 Message Date
elpatron b2a28f5782 chore: release v0.1.0.3 2026-05-29 17:36:48 +02:00
elpatron 4d2e309967 fix: Einstellungs-Dropdowns durch ThemedSelect mit lesbarem Kontrast ersetzen.
Native Select-Optionen waren in Light/Dark Mode schlecht lesbar; ein eigenes Dropdown steuert Hintergrund und Textfarbe zuverlässig.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 17:35:57 +02:00
elpatron 2f6c668ca4 feat: Light Mode mit System-Erkennung und konfigurierbarem Erscheinungsbild.
Stellt hell/dunkel für Ocean, Material und Cupertino bereit, migriert die Kern-UI auf CSS-Variablen und ergänzt die Einstellungen inkl. i18n und Select-Kontrast.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 17:26:50 +02:00
elpatron 42736fedf3 fix: Schiffs-Maße als Zahlen statt Strings speichern
Länge, Tiefgang und Höhe werden beim Speichern geparst und numerisch persistiert; Legacy-String-Werte beim Laden weiter unterstützt.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 17:10:26 +02:00
elpatron ac84fef832 fix: Auto-Accept-Retry bei fehlendem Logbuch-Schlüssel ermöglichen
autoAcceptStarted wird zurückgesetzt, wenn logbookKey oder logbookId fehlen, damit der Einladungsflow erneut starten kann.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 17:09:11 +02:00
elpatron 404eb79add feat: Schiffs-Stammdaten erweitern und Ablenkungstabelle ausblenden
Neue Felder für Yachttyp, Länge, Tiefgang und Höhe; Compass Deviation Table ist für Freizeit-Skipper vorerst aus der Navigation entfernt.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 17:08:21 +02:00
elpatron 14b52c684d fix: Einladungs-Auto-Accept, isShared-Cache und Recovery-Validierung
Auto-Accept kann nach Session-Verlust erneut starten, isShared wird offline in Dexie persistiert, und leere Recovery-Benutzernamen werden abgefangen.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 17:00:49 +02:00
15 changed files with 1048 additions and 387 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.0.3
0.1.0.4
+262 -332
View File
@@ -1,10 +1,11 @@
/* Kapteins Daagbok App styling */
body {
background: radial-gradient(circle at center, #1b264f 0%, #0b0c10 100%);
background: var(--app-body-bg);
min-height: 100vh;
margin: 0;
font-family: system-ui, -apple-system, sans-serif;
color: var(--app-text);
}
#root:has(.auth-screen) {
@@ -27,15 +28,15 @@ body {
/* Glassmorphism Auth Card */
.auth-card {
background: rgba(11, 12, 16, 0.75);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(212, 175, 55, 0.25);
border-radius: 20px;
background: var(--app-surface);
backdrop-filter: var(--app-backdrop);
-webkit-backdrop-filter: var(--app-backdrop);
border: 1px solid var(--app-border);
border-radius: var(--app-radius-card);
padding: 40px;
width: 450px;
max-width: 90%;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6), inset 0 0 0 1px rgba(255, 255, 255, 0.05);
box-shadow: var(--app-shadow);
display: flex;
flex-direction: column;
align-items: center;
@@ -65,8 +66,8 @@ body {
.auth-brand h1 {
font-size: 32px;
font-weight: 700;
color: #f8fafc;
background: linear-gradient(135deg, #fef08a 0%, #d97706 100%);
color: var(--app-text-heading);
background: var(--app-accent-gradient);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
@@ -77,7 +78,7 @@ body {
.auth-brand .tagline {
font-size: 14.5px;
color: #94a3b8;
color: var(--app-text-muted);
margin: 0;
line-height: 1.45;
}
@@ -96,31 +97,135 @@ body {
.input-text {
width: 100%;
background: rgba(11, 12, 16, 0.85);
border: 1px solid rgba(148, 163, 184, 0.25);
border-radius: 10px;
background: var(--app-input-bg);
border: 1px solid var(--app-input-border);
border-radius: var(--app-radius-input);
padding: 14px 16px;
font-size: 16px;
color: #f1f5f9;
color: var(--app-input-text);
outline: none;
box-sizing: border-box;
transition: all 0.3s ease;
}
.input-text:focus {
border-color: #d97706;
box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.2);
background: #0b0c10;
border-color: var(--app-accent);
box-shadow: 0 0 0 3px var(--app-accent-focus-ring);
background: var(--app-input-bg-focus);
}
select.input-text {
color-scheme: inherit;
cursor: pointer;
}
.themed-select {
position: relative;
width: 100%;
}
.themed-select-trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
text-align: left;
width: 100%;
}
.themed-select-chevron {
flex-shrink: 0;
opacity: 0.75;
transition: transform 0.2s ease;
}
.themed-select.is-open .themed-select-chevron {
transform: rotate(180deg);
}
.themed-select-menu {
position: absolute;
z-index: 120;
top: calc(100% + 4px);
left: 0;
right: 0;
margin: 0;
padding: 4px;
list-style: none;
border: 1px solid var(--app-input-border);
border-radius: var(--app-radius-input);
box-shadow: var(--app-card-shadow);
max-height: 240px;
overflow-y: auto;
isolation: isolate;
}
html.scheme-light .themed-select-menu {
background: #ffffff;
color: #0f172a;
}
html.scheme-dark .themed-select-menu {
background: #1c1c1e;
color: #f8fafc;
}
.themed-select-option {
padding: 10px 12px;
border-radius: calc(var(--app-radius-input) - 2px);
cursor: pointer;
font-size: 16px;
font-weight: 500;
line-height: 1.4;
}
html.scheme-light .themed-select-option {
color: #0f172a;
-webkit-text-fill-color: #0f172a;
}
html.scheme-dark .themed-select-option {
color: #f8fafc;
-webkit-text-fill-color: #f8fafc;
}
.themed-select-option:hover {
background: var(--app-accent-bg);
}
html.scheme-light .themed-select-option:hover {
color: var(--app-accent);
-webkit-text-fill-color: var(--app-accent);
}
html.scheme-dark .themed-select-option:hover {
color: var(--app-accent-light);
-webkit-text-fill-color: var(--app-accent-light);
}
.themed-select-option.is-selected {
background: var(--app-accent-bg);
font-weight: 600;
}
html.scheme-light .themed-select-option.is-selected {
color: var(--app-accent);
-webkit-text-fill-color: var(--app-accent);
}
html.scheme-dark .themed-select-option.is-selected {
color: var(--app-accent-light);
-webkit-text-fill-color: var(--app-accent-light);
}
.input-textarea {
width: 100%;
background: rgba(11, 12, 16, 0.85);
border: 1px solid rgba(148, 163, 184, 0.25);
border-radius: 10px;
background: var(--app-input-bg);
border: 1px solid var(--app-input-border);
border-radius: var(--app-radius-input);
padding: 14px;
font-size: 15px;
color: #f1f5f9;
color: var(--app-input-text);
outline: none;
box-sizing: border-box;
resize: none;
@@ -129,9 +234,9 @@ body {
}
.input-textarea:focus {
border-color: #d97706;
box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.2);
background: #0b0c10;
border-color: var(--app-accent);
box-shadow: 0 0 0 3px var(--app-accent-focus-ring);
background: var(--app-input-bg-focus);
}
.auth-submit-actions {
@@ -148,7 +253,7 @@ body {
font-size: 16px;
font-weight: 600;
border: none;
border-radius: 10px;
border-radius: var(--app-radius-btn);
cursor: pointer;
transition: all 0.25s ease;
display: flex;
@@ -158,14 +263,15 @@ body {
}
.btn.primary {
background: linear-gradient(135deg, #fbbf24 0%, #d97706 100%);
color: #0b0c10;
box-shadow: 0 4px 15px rgba(217, 119, 6, 0.3);
background-color: var(--app-accent);
background-image: var(--app-accent-gradient);
color: var(--app-btn-primary-text);
box-shadow: 0 4px 15px var(--app-accent-focus-ring);
}
.btn.primary:hover:not(:disabled) {
transform: translateY(-1.5px);
box-shadow: 0 6px 20px rgba(217, 119, 6, 0.45);
filter: brightness(1.05);
}
.btn.primary:active:not(:disabled) {
@@ -173,14 +279,13 @@ body {
}
.btn.secondary {
background: rgba(255, 255, 255, 0.05);
color: #e2e8f0;
border: 1px solid rgba(255, 255, 255, 0.12);
background: var(--app-btn-secondary-bg);
color: var(--app-btn-secondary-text);
border: 1px solid var(--app-btn-secondary-border);
}
.btn.secondary:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.08);
color: #ffffff;
background: var(--app-btn-secondary-hover-bg);
}
.btn:disabled {
@@ -213,15 +318,15 @@ body {
.auth-card h2 {
font-size: 24px;
font-weight: 600;
color: #f8fafc;
color: var(--app-text-heading);
margin: 0;
}
.recovery-warning {
font-size: 13.5px;
color: #f43f5e;
background: rgba(244, 63, 94, 0.08);
border: 1px solid rgba(244, 63, 94, 0.2);
color: var(--app-warning-text);
background: var(--app-warning-bg);
border: 1px solid var(--app-warning-border);
border-radius: 8px;
padding: 12px 14px;
line-height: 145%;
@@ -238,18 +343,18 @@ body {
}
.phrase-word {
background: rgba(11, 12, 16, 0.9);
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--app-input-bg);
border: 1px solid var(--app-border-subtle);
border-radius: 6px;
padding: 12px 8px;
font-size: 14.5px;
color: #f1f5f9;
color: var(--app-text);
text-align: center;
font-family: ui-monospace, monospace;
}
.word-num {
color: #d97706;
color: var(--app-accent);
font-size: 11px;
margin-right: 4px;
}
@@ -262,9 +367,9 @@ body {
.auth-error {
width: 100%;
background: rgba(244, 63, 94, 0.08);
color: #fda4af;
border-left: 3px solid #f43f5e;
background: var(--app-error-bg);
color: var(--app-error-text);
border-left: 3px solid var(--app-error-border);
padding: 10px 12px;
font-size: 14px;
border-radius: 4px;
@@ -275,7 +380,7 @@ body {
.auth-footer {
margin-top: 30px;
width: 100%;
border-top: 1px solid rgba(255, 255, 255, 0.06);
border-top: 1px solid var(--app-divider);
padding-top: 20px;
display: flex;
justify-content: space-between;
@@ -285,7 +390,7 @@ body {
.btn-icon-text {
background: none;
border: none;
color: #94a3b8;
color: var(--app-text-muted);
font-size: 13.5px;
display: flex;
align-items: center;
@@ -298,16 +403,16 @@ body {
}
.btn-icon-text:hover {
color: #d97706;
background: rgba(217, 119, 6, 0.06);
color: var(--app-accent);
background: var(--app-accent-bg);
}
.btn-icon-text.link-sec {
color: #4b5563;
color: var(--app-text-subtle);
}
.btn-icon-text.link-sec:hover {
color: #9ca3af;
color: var(--app-text-muted);
}
/* Dashboard Mock Layout (for verified state) */
@@ -346,7 +451,7 @@ body {
flex-direction: column;
padding: 24px;
box-sizing: border-box;
color: #f1f5f9;
color: var(--app-text);
}
.dashboard-header {
@@ -355,7 +460,7 @@ body {
align-items: center;
margin-bottom: 32px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(212, 175, 55, 0.15);
border-bottom: 1px solid var(--app-header-border);
}
.header-brand {
@@ -365,15 +470,15 @@ body {
}
.header-logo {
color: #fbbf24;
filter: drop-shadow(0 0 8px rgba(251, 191, 36, 0.4));
color: var(--app-accent-light);
filter: drop-shadow(0 0 8px var(--app-accent-focus-ring));
}
.header-brand h1 {
font-size: 24px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, #fef08a 0%, #d97706 100%);
background: var(--app-accent-gradient);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
@@ -381,7 +486,7 @@ body {
.header-brand .subtitle {
font-size: 12px;
color: #94a3b8;
color: var(--app-text-muted);
margin: 2px 0 0 0;
}
@@ -420,15 +525,15 @@ body {
font-size: 13px;
padding: 6px 12px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #e2e8f0;
background: var(--app-btn-secondary-bg);
border: 1px solid var(--app-btn-secondary-border);
color: var(--app-btn-secondary-text);
}
.btn-icon {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #94a3b8;
background: var(--app-icon-btn-bg);
border: 1px solid var(--app-icon-btn-border);
color: var(--app-text-muted);
width: 36px;
height: 36px;
border-radius: 50%;
@@ -440,9 +545,9 @@ body {
}
.btn-icon:hover {
background: rgba(217, 119, 6, 0.1);
border-color: #d97706;
color: #fbbf24;
background: var(--app-accent-bg);
border-color: var(--app-accent);
color: var(--app-accent-light);
}
.btn-icon.logout:hover {
@@ -511,11 +616,11 @@ body {
}
.create-section {
background: rgba(11, 12, 16, 0.6);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(212, 175, 55, 0.2);
border-radius: 16px;
background: var(--app-surface-alt);
backdrop-filter: var(--app-backdrop);
-webkit-backdrop-filter: var(--app-backdrop);
border: 1px solid var(--app-border-muted);
border-radius: var(--app-radius-card);
padding: 24px;
box-sizing: border-box;
}
@@ -524,7 +629,7 @@ body {
font-size: 18px;
margin-top: 0;
margin-bottom: 20px;
color: #f8fafc;
color: var(--app-text-heading);
}
.dashboard-form {
@@ -574,13 +679,13 @@ body {
.section-title-bar h2 {
font-size: 20px;
margin: 0;
color: #f8fafc;
color: var(--app-text-heading);
}
.btn-refresh {
background: none;
border: none;
color: #94a3b8;
color: var(--app-text-muted);
cursor: pointer;
padding: 6px;
border-radius: 4px;
@@ -591,7 +696,7 @@ body {
}
.btn-refresh:hover {
color: #fbbf24;
color: var(--app-accent-light);
}
.spin {
@@ -619,11 +724,11 @@ body {
}
.logbook-card {
background: rgba(11, 12, 16, 0.6);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
background: var(--app-surface-alt);
backdrop-filter: var(--app-backdrop);
-webkit-backdrop-filter: var(--app-backdrop);
border: 1px solid var(--app-border-subtle);
border-radius: var(--app-radius-card);
padding: 20px;
display: flex;
align-items: center;
@@ -635,14 +740,14 @@ body {
.logbook-card:hover {
transform: translateY(-2px);
border-color: rgba(212, 175, 55, 0.4);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
background: rgba(11, 12, 16, 0.75);
border-color: var(--app-border);
box-shadow: var(--app-card-shadow);
background: var(--app-surface-hover);
}
.card-icon {
background: rgba(217, 119, 6, 0.1);
color: #fbbf24;
background: var(--app-accent-bg);
color: var(--app-accent-light);
width: 48px;
height: 48px;
border-radius: 10px;
@@ -650,7 +755,7 @@ body {
align-items: center;
justify-content: center;
flex-shrink: 0;
border: 1px solid rgba(217, 119, 6, 0.2);
border: 1px solid var(--app-accent-border);
}
.card-info {
@@ -662,7 +767,7 @@ body {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #f1f5f9;
color: var(--app-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -693,7 +798,7 @@ body {
}
.date-badge {
color: #64748b;
color: var(--app-text-subtle);
}
.btn-delete {
@@ -757,7 +862,7 @@ body {
flex-direction: column;
padding: 24px;
box-sizing: border-box;
color: #f1f5f9;
color: var(--app-text);
}
.app-header {
@@ -766,7 +871,7 @@ body {
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(212, 175, 55, 0.15);
border-bottom: 1px solid var(--app-header-border);
}
.app-header-left {
@@ -776,9 +881,9 @@ body {
}
.btn-back {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #fbbf24;
background: var(--app-btn-secondary-bg);
border: 1px solid var(--app-btn-secondary-border);
color: var(--app-accent-light);
padding: 8px 16px;
border-radius: 8px;
display: flex;
@@ -790,19 +895,19 @@ body {
}
.btn-back:hover {
background: rgba(217, 119, 6, 0.1);
border-color: #d97706;
background: var(--app-accent-bg);
border-color: var(--app-accent);
}
.app-title-area h2 {
font-size: 20px;
margin: 0;
color: #f8fafc;
color: var(--app-text-heading);
}
.app-title-area .app-subtitle {
font-size: 12px;
color: #94a3b8;
color: var(--app-text-muted);
margin: 2px 0 0 0;
}
@@ -823,10 +928,10 @@ body {
display: flex;
flex-direction: column;
gap: 8px;
background: rgba(11, 12, 16, 0.6);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
background: var(--app-surface-alt);
backdrop-filter: var(--app-backdrop);
border: 1px solid var(--app-border-subtle);
border-radius: var(--app-radius-card);
padding: 16px;
box-sizing: border-box;
}
@@ -834,7 +939,7 @@ body {
.sidebar-btn {
background: none;
border: none;
color: #94a3b8;
color: var(--app-text-muted);
padding: 12px 16px;
border-radius: 8px;
display: flex;
@@ -849,21 +954,21 @@ body {
}
.sidebar-btn:hover {
background: rgba(255, 255, 255, 0.04);
color: #f1f5f9;
background: var(--app-surface-inset);
color: var(--app-text);
}
.sidebar-btn.active {
background: rgba(217, 119, 6, 0.1);
border: 1px solid rgba(217, 119, 6, 0.2);
color: #fbbf24;
background: var(--app-sidebar-active-bg);
border: 1px solid var(--app-accent-border);
color: var(--app-sidebar-active-text);
}
.app-content {
background: rgba(11, 12, 16, 0.6);
backdrop-filter: blur(20px);
border: 1px solid rgba(212, 175, 55, 0.2);
border-radius: 16px;
background: var(--app-surface-alt);
backdrop-filter: var(--app-backdrop);
border: 1px solid var(--app-border-muted);
border-radius: var(--app-radius-card);
padding: 32px;
min-height: 400px;
box-sizing: border-box;
@@ -877,12 +982,12 @@ body {
justify-content: center;
min-height: 300px;
text-align: center;
color: #94a3b8;
color: var(--app-text-muted);
}
.tab-placeholder h3 {
font-size: 24px;
color: #fbbf24;
color: var(--app-accent-light);
margin-top: 16px;
margin-bottom: 8px;
}
@@ -895,11 +1000,11 @@ body {
/* Form and Editor layout structures */
.form-card {
background: rgba(11, 12, 16, 0.6);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(212, 175, 55, 0.2);
border-radius: 16px;
background: var(--app-surface-alt);
backdrop-filter: var(--app-backdrop);
-webkit-backdrop-filter: var(--app-backdrop);
border: 1px solid var(--app-border-muted);
border-radius: var(--app-radius-card);
padding: 32px;
box-sizing: border-box;
width: 100%;
@@ -915,14 +1020,14 @@ body {
.form-header h2 {
font-size: 20px;
margin: 0;
color: #fbbf24;
color: var(--app-accent-light);
min-width: 0;
flex-shrink: 1;
word-break: break-word;
}
.form-icon {
color: #fbbf24;
color: var(--app-accent-light);
}
.vessel-form {
@@ -951,7 +1056,7 @@ body {
.vessel-form label {
display: block;
font-size: 13.5px;
color: #94a3b8;
color: var(--app-text-muted);
margin-bottom: 6px;
font-weight: 500;
}
@@ -986,9 +1091,9 @@ body {
}
.member-editor-card {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(212, 175, 55, 0.15);
border-radius: 12px;
background: var(--app-surface-inset);
border: 1px solid var(--app-border-muted);
border-radius: var(--app-radius-card);
padding: 24px;
box-sizing: border-box;
}
@@ -1002,7 +1107,7 @@ body {
.editor-header h3 {
margin: 0;
font-size: 16px;
color: #fbbf24;
color: var(--app-accent-light);
}
.editor-actions {
@@ -1187,7 +1292,7 @@ body {
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, #fbbf24, #d97706, #fbbf24);
background: var(--app-progress-bar);
background-size: 200% 100%;
animation: sync-slide 1.5s infinite linear;
z-index: 9999;
@@ -1228,232 +1333,57 @@ body {
/* ========================================== */
/* PHASE 4: OS-ADAPTIVE UI THEMES */
/* Color tokens: themes.css · classes on html */
/* ========================================== */
/* Body Overrides via :has() */
body:has(.theme-material) {
background: #121212 !important;
}
body:has(.theme-cupertino) {
background: #000000 !important;
html.theme-material {
font-family: 'Roboto', 'Noto Sans', system-ui, -apple-system, sans-serif;
}
/* --- MATERIAL THEME (ANDROID/LINUX) --- */
.theme-material {
font-family: 'Roboto', 'Noto Sans', system-ui, -apple-system, sans-serif !important;
html.theme-cupertino {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
/* Color & Text overrides */
.theme-material h1,
.theme-material h2,
.theme-material h3,
.theme-material h4,
.theme-material .form-header h2,
.theme-material .form-icon,
.theme-material .header-logo,
.theme-material .card-icon,
.theme-material .btn-back,
.theme-material .cell-label {
color: #00adb5 !important;
html.theme-material .sidebar-btn {
border-radius: 0;
border-left: 4px solid transparent;
}
/* Card Overrides */
.theme-material .auth-card,
.theme-material .form-card,
.theme-material .create-section,
.theme-material .app-sidebar,
.theme-material .app-content,
.theme-material .logbook-card,
.theme-material .crew-member-card,
.theme-material .member-editor-card {
background: #1e1e1e !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
border: 1px solid #2d2d2d !important;
border-radius: 4px !important;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3) !important;
html.theme-material .sidebar-btn.active {
border-left: 4px solid var(--app-sidebar-active-border);
border-top: none;
border-right: none;
border-bottom: none;
}
/* Input Overrides */
.theme-material .input-text,
.theme-material .input-textarea,
.theme-material select.input-text {
background: #2a2a2a !important;
border: 1px solid #3d3d3d !important;
border-radius: 4px !important;
color: #f1f5f9 !important;
}
.theme-material .input-text:focus,
.theme-material .input-textarea:focus {
border-color: #00adb5 !important;
box-shadow: 0 0 0 2px rgba(0, 173, 181, 0.2) !important;
html.theme-material .btn.primary {
background-image: none;
}
/* Button Overrides */
.theme-material .btn.primary {
background: #00adb5 !important;
color: #ffffff !important;
border-radius: 4px !important;
box-shadow: none !important;
}
.theme-material .btn.primary:hover {
background: #008f95 !important;
transform: none !important;
}
.theme-material .btn.secondary,
.theme-material .btn-back {
background: #2a2a2a !important;
border: 1px solid #3d3d3d !important;
color: #f1f5f9 !important;
border-radius: 4px !important;
}
.theme-material .btn.secondary:hover,
.theme-material .btn-back:hover {
background: #333333 !important;
html.theme-cupertino .btn.primary,
html.theme-cupertino .btn.secondary,
html.theme-cupertino .btn-back {
border-radius: var(--app-radius-btn);
}
/* Sidebar Overrides */
.theme-material .sidebar-btn {
border-radius: 0 !important;
border-left: 4px solid transparent !important;
}
.theme-material .sidebar-btn.active {
background: rgba(0, 173, 181, 0.08) !important;
border-left: 4px solid #00adb5 !important;
color: #00adb5 !important;
border-top: none !important;
border-right: none !important;
border-bottom: none !important;
html.theme-material .events-table th,
html.theme-cupertino .events-table th {
color: var(--app-accent);
border-bottom-color: var(--app-table-border);
}
/* Header Overrides */
.theme-material .app-header {
border-bottom: 1px solid #2d2d2d !important;
html.theme-material .events-table td,
html.theme-cupertino .events-table td {
border-bottom-color: var(--app-table-border);
}
/* Tables and Grids */
.theme-material .events-table th {
color: #00adb5 !important;
border-bottom: 2px solid #2d2d2d !important;
}
.theme-material .events-table td {
border-bottom: 1px solid #2d2d2d !important;
}
.theme-material .events-scroll-container {
border: 1px solid #2d2d2d !important;
html.theme-material .events-scroll-container,
html.theme-cupertino .events-scroll-container {
border-color: var(--app-table-border);
}
/* Progress bar override for Material */
.theme-material ~ .sync-progress-bar {
background: linear-gradient(90deg, #00adb5, #008f95, #00adb5) !important;
}
/* --- CUPERTINO THEME (iOS/macOS) --- */
.theme-cupertino {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Helvetica, Arial, sans-serif !important;
}
/* Color & Text overrides */
.theme-cupertino h1,
.theme-cupertino h2,
.theme-cupertino h3,
.theme-cupertino h4,
.theme-cupertino .form-header h2,
.theme-cupertino .form-icon,
.theme-cupertino .header-logo,
.theme-cupertino .card-icon,
.theme-cupertino .btn-back,
.theme-cupertino .cell-label {
color: #0a84ff !important;
}
/* Card Overrides */
.theme-cupertino .auth-card,
.theme-cupertino .form-card,
.theme-cupertino .create-section,
.theme-cupertino .app-sidebar,
.theme-cupertino .app-content,
.theme-cupertino .logbook-card,
.theme-cupertino .crew-member-card,
.theme-cupertino .member-editor-card {
background: rgba(28, 28, 30, 0.7) !important;
backdrop-filter: blur(25px) !important;
-webkit-backdrop-filter: blur(25px) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
border-radius: 12px !important;
box-shadow: none !important;
}
/* Input Overrides */
.theme-cupertino .input-text,
.theme-cupertino .input-textarea,
.theme-cupertino select.input-text {
background: rgba(255, 255, 255, 0.05) !important;
border: 1px solid rgba(255, 255, 255, 0.12) !important;
border-radius: 8px !important;
color: #ffffff !important;
}
.theme-cupertino .input-text:focus,
.theme-cupertino .input-textarea:focus {
border-color: #0a84ff !important;
box-shadow: 0 0 0 2px rgba(10, 132, 255, 0.25) !important;
}
/* Button Overrides */
.theme-cupertino .btn.primary {
background: #0a84ff !important;
color: #ffffff !important;
border-radius: 9999px !important;
box-shadow: none !important;
}
.theme-cupertino .btn.primary:hover {
background: #007aff !important;
transform: none !important;
}
.theme-cupertino .btn.secondary,
.theme-cupertino .btn-back {
background: rgba(255, 255, 255, 0.08) !important;
border: 1px solid rgba(255, 255, 255, 0.12) !important;
color: #ffffff !important;
border-radius: 9999px !important;
}
.theme-cupertino .btn.secondary:hover,
.theme-cupertino .btn-back:hover {
background: rgba(255, 255, 255, 0.12) !important;
}
/* Sidebar Overrides */
.theme-cupertino .sidebar-btn {
border-radius: 8px !important;
}
.theme-cupertino .sidebar-btn.active {
background: rgba(10, 132, 255, 0.12) !important;
color: #0a84ff !important;
border: 1px solid rgba(10, 132, 255, 0.2) !important;
}
/* Header Overrides */
.theme-cupertino .app-header {
border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important;
}
/* Tables and Grids */
.theme-cupertino .events-table th {
color: #0a84ff !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.15) !important;
}
.theme-cupertino .events-table td {
border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important;
}
.theme-cupertino .events-scroll-container {
border: 1px solid rgba(255, 255, 255, 0.1) !important;
background: rgba(28, 28, 30, 0.5) !important;
}
/* Progress bar override for Cupertino */
.theme-cupertino ~ .sync-progress-bar {
background: linear-gradient(90deg, #0a84ff, #007aff, #0a84ff) !important;
html.theme-cupertino .events-scroll-container {
background: var(--app-surface-inset);
}
+27 -28
View File
@@ -5,18 +5,25 @@ import AuthOnboarding from './components/AuthOnboarding.tsx'
import LogbookDashboard from './components/LogbookDashboard.tsx'
import VesselForm from './components/VesselForm.tsx'
import CrewForm from './components/CrewForm.tsx'
import DeviationForm from './components/DeviationForm.tsx'
// Compass Deviation Table — für Freizeit-Skipper vorerst deaktiviert (Komponente bleibt erhalten)
// import DeviationForm from './components/DeviationForm.tsx'
import LogEntriesList from './components/LogEntriesList.tsx'
import SettingsForm from './components/SettingsForm.tsx'
import InvitationAcceptance from './components/InvitationAcceptance.tsx'
import { getActiveMasterKey, logoutUser } from './services/auth.js'
import {
applyAppearanceToDocument,
resolveAppTheme,
resolveColorScheme,
subscribeToSystemColorScheme
} from './services/appearance.js'
import { startBackgroundSync, stopBackgroundSync, syncAllLogbooks, subscribeToSyncState } from './services/sync.js'
import ReadOnlyViewer from './components/ReadOnlyViewer.tsx'
import PwaInstallPrompt from './components/PwaInstallPrompt.tsx'
import AppFooter from './components/AppFooter.tsx'
import { db } from './services/db.js'
import { useLiveQuery } from 'dexie-react-hooks'
import { Ship, LogOut, ChevronLeft, Users, Compass, FileText, Settings, Wifi, WifiOff } from 'lucide-react'
import { Ship, LogOut, ChevronLeft, Users, FileText, Settings, Wifi, WifiOff } from 'lucide-react'
import { useTranslation } from 'react-i18next'
function App() {
@@ -24,10 +31,9 @@ function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [activeLogbookId, setActiveLogbookId] = useState<string | null>(null)
const [activeLogbookTitle, setActiveLogbookTitle] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<'vessel' | 'crew' | 'deviation' | 'logs' | 'settings'>('logs')
const [activeTab, setActiveTab] = useState<'vessel' | 'crew' | 'logs' | 'settings'>('logs')
const [online, setOnline] = useState(navigator.onLine)
const [isSyncing, setIsSyncing] = useState(false)
const [appliedTheme, setAppliedTheme] = useState<'ocean' | 'material' | 'cupertino'>('ocean')
const [isAcceptingInvite, setIsAcceptingInvite] = useState(false)
// Viewer mode for read-only shared links
@@ -40,27 +46,16 @@ function App() {
[activeLogbookId]
)
const updateAppliedTheme = () => {
const configTheme = localStorage.getItem('active_theme') || 'auto'
if (configTheme === 'auto') {
const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera
if (/iPad|iPhone|iPod|Macintosh/.test(userAgent)) {
setAppliedTheme('cupertino')
} else if (/Android|Linux/.test(userAgent)) {
setAppliedTheme('material')
} else {
setAppliedTheme('ocean')
}
} else {
setAppliedTheme(configTheme as 'ocean' | 'material' | 'cupertino')
}
}
useEffect(() => {
updateAppliedTheme()
window.addEventListener('theme-changed', updateAppliedTheme)
const syncAppearance = () => {
applyAppearanceToDocument(resolveAppTheme(), resolveColorScheme())
}
syncAppearance()
window.addEventListener('appearance-changed', syncAppearance)
const unsubscribeSystem = subscribeToSystemColorScheme(syncAppearance)
return () => {
window.removeEventListener('theme-changed', updateAppliedTheme)
window.removeEventListener('appearance-changed', syncAppearance)
unsubscribeSystem()
}
}, [])
@@ -158,7 +153,7 @@ function App() {
if (isViewerMode) {
return (
<div className={`theme-${appliedTheme}`} style={{ display: 'contents' }}>
<div style={{ display: 'contents' }}>
<ReadOnlyViewer token={shareToken} hexKey={shareKey} />
</div>
)
@@ -166,7 +161,7 @@ function App() {
if (isAcceptingInvite) {
return (
<div className={`theme-${appliedTheme} auth-screen`}>
<div className="auth-screen">
<InvitationAcceptance
onAccepted={(logbookId, title) => {
setIsAuthenticated(true)
@@ -186,7 +181,7 @@ function App() {
if (!isAuthenticated) {
return (
<div className={`theme-${appliedTheme} auth-screen`}>
<div className="auth-screen">
<AuthOnboarding onAuthenticated={handleAuthenticated} />
</div>
)
@@ -196,7 +191,7 @@ function App() {
if (!activeLogbookId) {
return (
<div className={`theme-${appliedTheme}`} style={{ display: 'contents' }}>
<div style={{ display: 'contents' }}>
{pwaInstallBanner}
<LogbookDashboard
onSelectLogbook={handleSelectLogbook}
@@ -207,7 +202,7 @@ function App() {
}
return (
<div className={`theme-${appliedTheme}`} style={{ display: 'contents' }}>
<div style={{ display: 'contents' }}>
{pwaInstallBanner}
{isSyncing && <div className="sync-progress-bar" />}
<div className="app-layout">
@@ -271,6 +266,7 @@ function App() {
{t('nav.crew')}
</button>
{/* Compass Deviation Table — für Freizeit-Skipper vorerst ausgeblendet
<button
className={`sidebar-btn ${activeTab === 'deviation' ? 'active' : ''}`}
onClick={() => setActiveTab('deviation')}
@@ -278,6 +274,7 @@ function App() {
<Compass size={18} />
{t('nav.deviation')}
</button>
*/}
<button
className={`sidebar-btn ${activeTab === 'settings' ? 'active' : ''}`}
@@ -302,9 +299,11 @@ function App() {
<CrewForm logbookId={activeLogbookId} />
)}
{/* Compass Deviation Table — für Freizeit-Skipper vorerst deaktiviert
{activeTab === 'deviation' && (
<DeviationForm logbookId={activeLogbookId} />
)}
*/}
{activeTab === 'settings' && (
<SettingsForm logbookId={activeLogbookId} />
+19 -4
View File
@@ -137,13 +137,17 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
const masterKey = getActiveMasterKey()
const activeUserId = localStorage.getItem('active_userid')
if (!masterKey || !activeUserId) {
autoAcceptStarted.current = false
setError(isDe
? 'Sitzung unvollständig — bitte erneut anmelden (Benutzer-ID fehlt).'
: 'Incomplete session — please log in again (user ID missing).')
setIsLoggedIn(false)
return
}
if (!logbookKey || !logbookId) return
if (!logbookKey || !logbookId) {
autoAcceptStarted.current = false
return
}
setAccepting(true)
setError(null)
@@ -184,7 +188,8 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
id: logbookId,
encryptedTitle: rawEncryptedTitle,
updatedAt: new Date().toISOString(),
isSynced: 1
isSynced: 1,
isShared: 1
})
}
@@ -202,7 +207,10 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
useEffect(() => {
if (loading || accepting || autoAcceptStarted.current) return
if (!isLoggedIn || !logbookId || !logbookKey || !token) return
if (!sessionReady()) return
if (!sessionReady()) {
autoAcceptStarted.current = false
return
}
autoAcceptStarted.current = true
void handleAccept()
@@ -240,10 +248,17 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
e.preventDefault()
if (!recoveryInput.trim() || !encryptedPayloads) return
const resolvedUser = (username.trim() || encryptedPayloads.username || '').trim()
if (!resolvedUser) {
setAuthError(isDe
? 'Benutzername konnte nicht ermittelt werden — bitte erneut anmelden.'
: 'Could not determine username — please try logging in again.')
return
}
setLoading(true)
setAuthError(null)
try {
const resolvedUser = username.trim() || encryptedPayloads.username
const success = await completeLoginWithRecovery(resolvedUser, recoveryInput.trim(), encryptedPayloads)
if (success) {
setShowRecoveryFallback(false)
+52 -15
View File
@@ -5,6 +5,8 @@ import { ensureLogbookKey } from '../services/logbookKeys.js'
import AccountDangerZone from './AccountDangerZone.tsx'
import PwaInstallPrompt from './PwaInstallPrompt.tsx'
import { useDialog } from './ModalDialog.tsx'
import { notifyAppearanceChanged } from '../services/appearance.js'
import ThemedSelect from './ThemedSelect.tsx'
interface SettingsFormProps {
logbookId?: string | null
@@ -30,6 +32,7 @@ export default function SettingsForm({ logbookId }: SettingsFormProps) {
const { showConfirm, showAlert } = useDialog()
const [apiKey, setApiKey] = useState(localStorage.getItem('owm_api_key') || '')
const [theme, setTheme] = useState(localStorage.getItem('active_theme') || 'auto')
const [colorScheme, setColorScheme] = useState(localStorage.getItem('active_color_scheme') || 'auto')
const [saving, setSaving] = useState(false)
const [success, setSuccess] = useState(false)
@@ -245,17 +248,29 @@ export default function SettingsForm({ logbookId }: SettingsFormProps) {
}
}
const persistAppearance = (nextTheme: string, nextColorScheme: string) => {
localStorage.setItem('active_theme', nextTheme)
localStorage.setItem('active_color_scheme', nextColorScheme)
notifyAppearanceChanged()
}
const handleThemeChange = (nextTheme: string) => {
setTheme(nextTheme)
persistAppearance(nextTheme, colorScheme)
}
const handleColorSchemeChange = (nextColorScheme: string) => {
setColorScheme(nextColorScheme)
persistAppearance(theme, nextColorScheme)
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
setSaving(true)
setSuccess(false)
// Save to localStorage
localStorage.setItem('owm_api_key', apiKey.trim())
localStorage.setItem('active_theme', theme)
// Notify App of theme change
window.dispatchEvent(new Event('theme-changed'))
persistAppearance(theme, colorScheme)
setSaving(false)
setSuccess(true)
@@ -312,19 +327,41 @@ export default function SettingsForm({ logbookId }: SettingsFormProps) {
</p>
<div className="input-group">
<select
<ThemedSelect
id="app-theme"
className="input-text"
value={theme}
onChange={(e) => setTheme(e.target.value)}
disabled={saving}
style={{ background: 'rgba(11, 12, 16, 0.85)', color: '#f1f5f9' }}
>
<option value="auto">{t('settings.theme_auto')}</option>
<option value="ocean">{t('settings.theme_ocean')}</option>
<option value="material">{t('settings.theme_material')}</option>
<option value="cupertino">{t('settings.theme_cupertino')}</option>
</select>
onChange={handleThemeChange}
options={[
{ value: 'auto', label: t('settings.theme_auto') },
{ value: 'ocean', label: t('settings.theme_ocean') },
{ value: 'material', label: t('settings.theme_material') },
{ value: 'cupertino', label: t('settings.theme_cupertino') }
]}
/>
</div>
</div>
<div className="member-editor-card glass mt-4">
<h3 style={{ marginTop: 0, marginBottom: '12px', color: 'var(--app-accent-light)', fontSize: '16px' }}>
{t('settings.color_scheme_title')}
</h3>
<p className="text-muted" style={{ fontSize: '13.5px', lineHeight: '145%', margin: '0 0 16px 0' }}>
{t('settings.color_scheme_label')}
</p>
<div className="input-group">
<ThemedSelect
id="app-color-scheme"
value={colorScheme}
disabled={saving}
onChange={handleColorSchemeChange}
options={[
{ value: 'auto', label: t('settings.color_scheme_auto') },
{ value: 'light', label: t('settings.color_scheme_light') },
{ value: 'dark', label: t('settings.color_scheme_dark') }
]}
/>
</div>
</div>
+86
View File
@@ -0,0 +1,86 @@
import { useEffect, useRef, useState } from 'react'
import { ChevronDown } from 'lucide-react'
export interface ThemedSelectOption {
value: string
label: string
}
interface ThemedSelectProps {
id?: string
value: string
options: ThemedSelectOption[]
onChange: (value: string) => void
disabled?: boolean
}
export default function ThemedSelect({
id,
value,
options,
onChange,
disabled = false
}: ThemedSelectProps) {
const [open, setOpen] = useState(false)
const rootRef = useRef<HTMLDivElement>(null)
const selected = options.find((option) => option.value === value)
useEffect(() => {
if (!open) return
const closeOnOutsideClick = (event: MouseEvent) => {
if (rootRef.current && !rootRef.current.contains(event.target as Node)) {
setOpen(false)
}
}
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') setOpen(false)
}
document.addEventListener('mousedown', closeOnOutsideClick)
document.addEventListener('keydown', closeOnEscape)
return () => {
document.removeEventListener('mousedown', closeOnOutsideClick)
document.removeEventListener('keydown', closeOnEscape)
}
}, [open])
const selectOption = (nextValue: string) => {
onChange(nextValue)
setOpen(false)
}
return (
<div className={`themed-select${open ? ' is-open' : ''}`} ref={rootRef}>
<button
type="button"
id={id}
className="themed-select-trigger input-text"
disabled={disabled}
aria-haspopup="listbox"
aria-expanded={open}
onClick={() => !disabled && setOpen((current) => !current)}
>
<span>{selected?.label ?? value}</span>
<ChevronDown size={16} className="themed-select-chevron" aria-hidden="true" />
</button>
{open && (
<ul className="themed-select-menu" role="listbox" aria-labelledby={id}>
{options.map((option) => (
<li
key={option.value}
role="option"
aria-selected={option.value === value}
className={`themed-select-option${option.value === value ? ' is-selected' : ''}`}
onClick={() => selectOption(option.value)}
>
{option.label}
</li>
))}
</ul>
)}
</div>
)
}
+99
View File
@@ -13,9 +13,30 @@ interface VesselFormProps {
preloadedData?: any
}
function metricInputFromStored(value: unknown): string {
if (value == null || value === '') return ''
if (typeof value === 'number' && Number.isFinite(value)) return String(value)
if (typeof value === 'string') return value.trim()
return ''
}
function parseOptionalMetricMeters(input: string): number | undefined {
const trimmed = input.trim().replace(',', '.')
if (!trimmed) return undefined
const parsed = Number(trimmed)
if (!Number.isFinite(parsed) || parsed < 0) {
throw new Error('invalid_metric')
}
return parsed
}
export default function VesselForm({ logbookId, readOnly = false, preloadedData }: VesselFormProps) {
const { t } = useTranslation()
const [name, setName] = useState('')
const [vesselType, setVesselType] = useState<'sailing' | 'motor' | ''>('')
const [lengthM, setLengthM] = useState('')
const [draftM, setDraftM] = useState('')
const [airDraftM, setAirDraftM] = useState('')
const [homePort, setHomePort] = useState('')
const [charterCompany, setCharterCompany] = useState('')
const [owner, setOwner] = useState('')
@@ -43,6 +64,10 @@ export default function VesselForm({ logbookId, readOnly = false, preloadedData
try {
if (readOnly && preloadedData) {
setName(preloadedData.name || '')
setVesselType(preloadedData.vesselType || '')
setLengthM(metricInputFromStored(preloadedData.lengthM))
setDraftM(metricInputFromStored(preloadedData.draftM))
setAirDraftM(metricInputFromStored(preloadedData.airDraftM))
setHomePort(preloadedData.homePort || '')
setCharterCompany(preloadedData.charterCompany || '')
setOwner(preloadedData.owner || '')
@@ -64,6 +89,10 @@ export default function VesselForm({ logbookId, readOnly = false, preloadedData
const decrypted = await decryptJson(local.encryptedData, local.iv, local.tag, masterKey)
if (decrypted) {
setName(decrypted.name || '')
setVesselType(decrypted.vesselType || '')
setLengthM(metricInputFromStored(decrypted.lengthM))
setDraftM(metricInputFromStored(decrypted.draftM))
setAirDraftM(metricInputFromStored(decrypted.airDraftM))
setHomePort(decrypted.homePort || '')
setCharterCompany(decrypted.charterCompany || '')
setOwner(decrypted.owner || '')
@@ -168,8 +197,25 @@ export default function VesselForm({ logbookId, readOnly = false, preloadedData
const masterKey = await getLogbookKey(logbookId) || getActiveMasterKey()
if (!masterKey) throw new Error('Encryption key not found. Please log in.')
let parsedLengthM: number | undefined
let parsedDraftM: number | undefined
let parsedAirDraftM: number | undefined
try {
parsedLengthM = parseOptionalMetricMeters(lengthM)
parsedDraftM = parseOptionalMetricMeters(draftM)
parsedAirDraftM = parseOptionalMetricMeters(airDraftM)
} catch {
setError(t('vessel.invalid_metric'))
setSaving(false)
return
}
const yachtData = {
name: name.trim(),
vesselType: vesselType || undefined,
lengthM: parsedLengthM,
draftM: parsedDraftM,
airDraftM: parsedAirDraftM,
homePort: homePort.trim(),
charterCompany: charterCompany.trim(),
owner: owner.trim(),
@@ -302,6 +348,59 @@ export default function VesselForm({ logbookId, readOnly = false, preloadedData
/>
</div>
<div className="input-group">
<label>{t('vessel.type')}</label>
<select
className="input-text"
value={vesselType}
onChange={(e) => setVesselType(e.target.value as 'sailing' | 'motor' | '')}
disabled={saving || readOnly}
>
<option value="">{t('vessel.type_unset')}</option>
<option value="sailing">{t('vessel.type_sailing')}</option>
<option value="motor">{t('vessel.type_motor')}</option>
</select>
</div>
<div className="input-group">
<label>{t('vessel.length_m')}</label>
<input
type="text"
inputMode="decimal"
className="input-text"
value={lengthM}
onChange={(e) => setLengthM(e.target.value)}
disabled={saving || readOnly}
placeholder="0.00"
/>
</div>
<div className="input-group">
<label>{t('vessel.draft_m')}</label>
<input
type="text"
inputMode="decimal"
className="input-text"
value={draftM}
onChange={(e) => setDraftM(e.target.value)}
disabled={saving || readOnly}
placeholder="0.00"
/>
</div>
<div className="input-group">
<label>{t('vessel.air_draft_m')}</label>
<input
type="text"
inputMode="decimal"
className="input-text"
value={airDraftM}
onChange={(e) => setAirDraftM(e.target.value)}
disabled={saving || readOnly}
placeholder="0.00"
/>
</div>
<div className="input-group">
<label>{t('vessel.port')}</label>
<input
+13
View File
@@ -76,6 +76,14 @@
"vessel": {
"title": "Schiffs-Stammdaten",
"name": "Yachtname",
"type": "Yachttyp",
"type_unset": "— nicht angegeben —",
"type_sailing": "Segelyacht",
"type_motor": "Motoryacht",
"length_m": "Länge (m)",
"draft_m": "Tiefgang (m)",
"air_draft_m": "Höhe (m)",
"invalid_metric": "Ungültiger Zahlenwert — bitte Meter als Dezimalzahl eingeben (z. B. 12,5).",
"port": "Heimathafen",
"owner": "Eigner",
"charter": "Charterfirma",
@@ -278,6 +286,11 @@
"theme_ocean": "Ocean (Glassmorphismus)",
"theme_material": "Material (Android)",
"theme_cupertino": "Cupertino (iOS)",
"color_scheme_title": "Erscheinungsbild",
"color_scheme_label": "Hell- oder Dunkelmodus (Standard: Systemeinstellung)",
"color_scheme_auto": "Automatisch (System)",
"color_scheme_light": "Hell",
"color_scheme_dark": "Dunkel",
"share_title": "Logbuch teilen (Schreibgeschützt)",
"share_desc": "Aktivieren Sie diese Option, um einen öffentlichen, schreibgeschützten Link zu erstellen. Jeder mit dem Link kann Ihre Reisen, Yacht-Profile und Besatzung ansehen. Die Verschlüsselungsschlüssel werden niemals an den Server übertragen (sie bleiben im Hash-Teil der URL).",
"share_enable": "Öffentlichen Link aktivieren",
+13
View File
@@ -76,6 +76,14 @@
"vessel": {
"title": "Vessel Master Data",
"name": "Yacht Name",
"type": "Vessel Type",
"type_unset": "— not specified —",
"type_sailing": "Sailing yacht",
"type_motor": "Motor yacht",
"length_m": "Length (m)",
"draft_m": "Draft (m)",
"air_draft_m": "Air draft (m)",
"invalid_metric": "Invalid number — please enter meters as a decimal (e.g. 12.5).",
"port": "Home Port",
"owner": "Owner",
"charter": "Charter Company",
@@ -278,6 +286,11 @@
"theme_ocean": "Ocean (Glassmorphism)",
"theme_material": "Material (Android)",
"theme_cupertino": "Cupertino (iOS)",
"color_scheme_title": "Appearance",
"color_scheme_label": "Light or dark mode (default: follow system)",
"color_scheme_auto": "Auto (System)",
"color_scheme_light": "Light",
"color_scheme_dark": "Dark",
"share_title": "Share Logbook (Read-Only)",
"share_desc": "Enable this to generate a public, read-only link. Anyone with the link can view your travels, yacht profile, and crew members. Decryption keys are never transmitted to the server (they stay in the hash part of the URL).",
"share_enable": "Enable Public Link",
+4
View File
@@ -1,9 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import 'leaflet/dist/leaflet.css'
import './themes.css'
import './index.css'
import App from './App.tsx'
import './i18n'
import { applyAppearanceToDocument } from './services/appearance.ts'
applyAppearanceToDocument()
createRoot(document.getElementById('root')!).render(
<StrictMode>
+53
View File
@@ -0,0 +1,53 @@
export type ColorSchemePreference = 'auto' | 'light' | 'dark'
export type ResolvedColorScheme = 'light' | 'dark'
export type AppTheme = 'ocean' | 'material' | 'cupertino'
const THEME_CLASSES = ['theme-ocean', 'theme-material', 'theme-cupertino'] as const
const SCHEME_CLASSES = ['scheme-light', 'scheme-dark'] as const
export function getColorSchemePreference(): ColorSchemePreference {
const stored = localStorage.getItem('active_color_scheme')
if (stored === 'light' || stored === 'dark' || stored === 'auto') return stored
return 'auto'
}
export function resolveColorScheme(pref?: ColorSchemePreference): ResolvedColorScheme {
const preference = pref ?? getColorSchemePreference()
if (preference === 'light') return 'light'
if (preference === 'dark') return 'dark'
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
export function resolveAppTheme(): AppTheme {
const configTheme = localStorage.getItem('active_theme') || 'auto'
if (configTheme === 'material' || configTheme === 'cupertino' || configTheme === 'ocean') {
return configTheme
}
const userAgent = navigator.userAgent || navigator.vendor || ''
if (/iPad|iPhone|iPod|Macintosh/.test(userAgent)) return 'cupertino'
if (/Android|Linux/.test(userAgent)) return 'material'
return 'ocean'
}
export function applyAppearanceToDocument(
theme: AppTheme = resolveAppTheme(),
scheme: ResolvedColorScheme = resolveColorScheme()
): void {
const root = document.documentElement
root.classList.remove(...THEME_CLASSES, ...SCHEME_CLASSES)
root.classList.add(`theme-${theme}`, `scheme-${scheme}`)
root.style.colorScheme = scheme
}
export function subscribeToSystemColorScheme(onChange: () => void): () => void {
const media = window.matchMedia('(prefers-color-scheme: dark)')
const handler = () => {
if (getColorSchemePreference() === 'auto') onChange()
}
media.addEventListener('change', handler)
return () => media.removeEventListener('change', handler)
}
export function notifyAppearanceChanged(): void {
window.dispatchEvent(new Event('appearance-changed'))
}
+12
View File
@@ -5,6 +5,7 @@ export interface LocalLogbook {
encryptedTitle: string
updatedAt: string
isSynced: number // 1 = yes, 0 = pending local modifications
isShared?: number // 1 = collaborator copy, 0 or unset = owned
}
export interface LocalYacht {
@@ -120,6 +121,17 @@ class DaagboxDatabase extends Dexie {
gpsTracks: 'entryId, logbookId, updatedAt',
logbookKeys: 'logbookId'
})
this.version(4).stores({
logbooks: 'id, encryptedTitle, updatedAt, isSynced, isShared',
yachts: 'logbookId, updatedAt',
crews: 'payloadId, logbookId, updatedAt',
deviations: 'logbookId, updatedAt',
entries: 'payloadId, logbookId, updatedAt',
syncQueue: '++id, action, type, payloadId, logbookId',
photos: 'payloadId, entryId, logbookId, updatedAt',
gpsTracks: 'entryId, logbookId, updatedAt',
logbookKeys: 'logbookId'
})
}
}
+7 -7
View File
@@ -43,8 +43,6 @@ export async function fetchLogbooks(): Promise<DecryptedLogbook[]> {
throw new Error('Master key not found. User must log in.')
}
const sharedLogbookIds = new Set<string>()
if (navigator.onLine) {
try {
const response = await fetch(API_BASE, {
@@ -61,7 +59,6 @@ export async function fetchLogbooks(): Promise<DecryptedLogbook[]> {
// Decrypt and save logbook keys locally if they exist
for (const lb of serverLogbooks) {
const isShared = lb.userId !== userId
if (isShared) sharedLogbookIds.add(lb.id)
const encryptedKeyStr = isShared
? lb.collaborators?.[0]?.encryptedLogbookKey
@@ -105,7 +102,8 @@ export async function fetchLogbooks(): Promise<DecryptedLogbook[]> {
id: lb.id,
encryptedTitle: lb.encryptedTitle,
updatedAt: lb.updatedAt || new Date().toISOString(),
isSynced: 1
isSynced: 1,
isShared: lb.userId !== userId ? 1 : 0
}))
// Clear existing cache for this user and insert new ones
@@ -128,7 +126,7 @@ export async function fetchLogbooks(): Promise<DecryptedLogbook[]> {
title,
updatedAt: lb.updatedAt,
isSynced: lb.isSynced === 1,
isShared: sharedLogbookIds.has(lb.id)
isShared: lb.isShared === 1
})
}
@@ -195,7 +193,8 @@ export async function createLogbook(title: string): Promise<DecryptedLogbook> {
id: serverLb.id,
encryptedTitle: serverLb.encryptedTitle,
updatedAt: serverLb.updatedAt,
isSynced: 1
isSynced: 1,
isShared: 0
})
return {
@@ -216,7 +215,8 @@ export async function createLogbook(title: string): Promise<DecryptedLogbook> {
id: localId,
encryptedTitle: encryptedTitleStr,
updatedAt: now,
isSynced: 0
isSynced: 0,
isShared: 0
})
await db.syncQueue.put({
+398
View File
@@ -0,0 +1,398 @@
/**
* Appearance tokens: scheme (light/dark) × theme (ocean/material/cupertino)
* Applied on document.documentElement via appearance.ts
*/
/* Fallback before JS hydrates (ocean · dark) */
html {
color-scheme: dark;
--app-body-bg: radial-gradient(circle at center, #1b264f 0%, #0b0c10 100%);
--app-text: #f1f5f9;
--app-text-heading: #f8fafc;
--app-text-muted: #94a3b8;
--app-text-subtle: #64748b;
--app-surface: rgba(11, 12, 16, 0.75);
--app-surface-alt: rgba(11, 12, 16, 0.6);
--app-surface-hover: rgba(11, 12, 16, 0.85);
--app-surface-inset: rgba(255, 255, 255, 0.02);
--app-border: rgba(212, 175, 55, 0.25);
--app-border-subtle: rgba(255, 255, 255, 0.08);
--app-border-muted: rgba(212, 175, 55, 0.15);
--app-input-bg: rgba(11, 12, 16, 0.85);
--app-input-bg-focus: #0b0c10;
--app-input-border: rgba(148, 163, 184, 0.25);
--app-input-text: #f1f5f9;
--app-accent: #d97706;
--app-accent-light: #fbbf24;
--app-accent-gradient: linear-gradient(135deg, #fef08a 0%, #d97706 100%);
--app-accent-bg: rgba(217, 119, 6, 0.1);
--app-accent-border: rgba(217, 119, 6, 0.2);
--app-accent-focus-ring: rgba(217, 119, 6, 0.2);
--app-btn-primary-text: #0b0c10;
--app-btn-secondary-bg: rgba(255, 255, 255, 0.05);
--app-btn-secondary-border: rgba(255, 255, 255, 0.12);
--app-btn-secondary-text: #e2e8f0;
--app-btn-secondary-hover-bg: rgba(255, 255, 255, 0.08);
--app-icon-btn-bg: rgba(255, 255, 255, 0.05);
--app-icon-btn-border: rgba(255, 255, 255, 0.1);
--app-divider: rgba(255, 255, 255, 0.06);
--app-shadow: 0 20px 50px rgba(0, 0, 0, 0.6), inset 0 0 0 1px rgba(255, 255, 255, 0.05);
--app-card-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
--app-error-bg: rgba(244, 63, 94, 0.08);
--app-error-text: #fda4af;
--app-error-border: #f43f5e;
--app-warning-text: #f43f5e;
--app-warning-bg: rgba(244, 63, 94, 0.08);
--app-warning-border: rgba(244, 63, 94, 0.2);
--app-empty-border: rgba(255, 255, 255, 0.08);
--app-empty-bg: rgba(255, 255, 255, 0.02);
--app-sidebar-active-bg: rgba(217, 119, 6, 0.12);
--app-sidebar-active-border: #d97706;
--app-sidebar-active-text: #fbbf24;
--app-header-border: rgba(212, 175, 55, 0.15);
--app-table-border: rgba(255, 255, 255, 0.08);
--app-progress-bar: linear-gradient(90deg, #d97706, #fbbf24, #d97706);
--app-backdrop: blur(20px);
--app-radius-card: 16px;
--app-radius-input: 10px;
--app-radius-btn: 10px;
}
/* ===== OCEAN · DARK (default) ===== */
html.scheme-dark.theme-ocean {
color-scheme: dark;
--app-body-bg: radial-gradient(circle at center, #1b264f 0%, #0b0c10 100%);
--app-text: #f1f5f9;
--app-text-heading: #f8fafc;
--app-text-muted: #94a3b8;
--app-text-subtle: #64748b;
--app-surface: rgba(11, 12, 16, 0.75);
--app-surface-alt: rgba(11, 12, 16, 0.6);
--app-surface-hover: rgba(11, 12, 16, 0.85);
--app-surface-inset: rgba(255, 255, 255, 0.02);
--app-border: rgba(212, 175, 55, 0.25);
--app-border-subtle: rgba(255, 255, 255, 0.08);
--app-border-muted: rgba(212, 175, 55, 0.15);
--app-input-bg: rgba(11, 12, 16, 0.85);
--app-input-bg-focus: #0b0c10;
--app-input-border: rgba(148, 163, 184, 0.25);
--app-input-text: #f1f5f9;
--app-accent: #d97706;
--app-accent-light: #fbbf24;
--app-accent-gradient: linear-gradient(135deg, #fef08a 0%, #d97706 100%);
--app-accent-bg: rgba(217, 119, 6, 0.1);
--app-accent-border: rgba(217, 119, 6, 0.2);
--app-accent-focus-ring: rgba(217, 119, 6, 0.2);
--app-btn-primary-text: #0b0c10;
--app-btn-secondary-bg: rgba(255, 255, 255, 0.05);
--app-btn-secondary-border: rgba(255, 255, 255, 0.12);
--app-btn-secondary-text: #e2e8f0;
--app-btn-secondary-hover-bg: rgba(255, 255, 255, 0.08);
--app-icon-btn-bg: rgba(255, 255, 255, 0.05);
--app-icon-btn-border: rgba(255, 255, 255, 0.1);
--app-divider: rgba(255, 255, 255, 0.06);
--app-shadow: 0 20px 50px rgba(0, 0, 0, 0.6), inset 0 0 0 1px rgba(255, 255, 255, 0.05);
--app-card-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
--app-error-bg: rgba(244, 63, 94, 0.08);
--app-error-text: #fda4af;
--app-error-border: #f43f5e;
--app-warning-text: #f43f5e;
--app-warning-bg: rgba(244, 63, 94, 0.08);
--app-warning-border: rgba(244, 63, 94, 0.2);
--app-empty-border: rgba(255, 255, 255, 0.08);
--app-empty-bg: rgba(255, 255, 255, 0.02);
--app-sidebar-active-bg: rgba(217, 119, 6, 0.12);
--app-sidebar-active-border: #d97706;
--app-sidebar-active-text: #fbbf24;
--app-header-border: rgba(212, 175, 55, 0.15);
--app-table-border: rgba(255, 255, 255, 0.08);
--app-progress-bar: linear-gradient(90deg, #d97706, #fbbf24, #d97706);
--app-backdrop: blur(20px);
--app-radius-card: 16px;
--app-radius-input: 10px;
--app-radius-btn: 10px;
}
/* ===== OCEAN · LIGHT ===== */
html.scheme-light.theme-ocean {
color-scheme: light;
--app-body-bg: linear-gradient(165deg, #dbeafe 0%, #f8fafc 42%, #e2e8f0 100%);
--app-text: #1e293b;
--app-text-heading: #0f172a;
--app-text-muted: #475569;
--app-text-subtle: #64748b;
--app-surface: rgba(255, 255, 255, 0.88);
--app-surface-alt: rgba(255, 255, 255, 0.78);
--app-surface-hover: rgba(255, 255, 255, 0.96);
--app-surface-inset: rgba(15, 23, 42, 0.03);
--app-border: rgba(217, 119, 6, 0.28);
--app-border-subtle: rgba(15, 23, 42, 0.1);
--app-border-muted: rgba(217, 119, 6, 0.18);
--app-input-bg: #ffffff;
--app-input-bg-focus: #ffffff;
--app-input-border: rgba(100, 116, 139, 0.35);
--app-input-text: #0f172a;
--app-accent: #b45309;
--app-accent-light: #d97706;
--app-accent-gradient: linear-gradient(135deg, #fcd34d 0%, #b45309 100%);
--app-accent-bg: rgba(217, 119, 6, 0.12);
--app-accent-border: rgba(217, 119, 6, 0.25);
--app-accent-focus-ring: rgba(217, 119, 6, 0.25);
--app-btn-primary-text: #0b0c10;
--app-btn-secondary-bg: rgba(15, 23, 42, 0.04);
--app-btn-secondary-border: rgba(15, 23, 42, 0.12);
--app-btn-secondary-text: #334155;
--app-btn-secondary-hover-bg: rgba(15, 23, 42, 0.07);
--app-icon-btn-bg: rgba(15, 23, 42, 0.04);
--app-icon-btn-border: rgba(15, 23, 42, 0.1);
--app-divider: rgba(15, 23, 42, 0.08);
--app-shadow: 0 16px 40px rgba(15, 23, 42, 0.12), inset 0 0 0 1px rgba(255, 255, 255, 0.6);
--app-card-shadow: 0 8px 24px rgba(15, 23, 42, 0.1);
--app-error-bg: rgba(244, 63, 94, 0.08);
--app-error-text: #be123c;
--app-error-border: #e11d48;
--app-warning-text: #be123c;
--app-warning-bg: rgba(244, 63, 94, 0.06);
--app-warning-border: rgba(244, 63, 94, 0.2);
--app-empty-border: rgba(15, 23, 42, 0.12);
--app-empty-bg: rgba(15, 23, 42, 0.02);
--app-sidebar-active-bg: rgba(217, 119, 6, 0.1);
--app-sidebar-active-border: #d97706;
--app-sidebar-active-text: #b45309;
--app-header-border: rgba(217, 119, 6, 0.2);
--app-table-border: rgba(15, 23, 42, 0.1);
--app-progress-bar: linear-gradient(90deg, #d97706, #fbbf24, #d97706);
--app-backdrop: blur(20px);
--app-radius-card: 16px;
--app-radius-input: 10px;
--app-radius-btn: 10px;
}
/* ===== MATERIAL · DARK ===== */
html.scheme-dark.theme-material {
color-scheme: dark;
--app-body-bg: #121212;
--app-text: #f1f5f9;
--app-text-heading: #f8fafc;
--app-text-muted: #94a3b8;
--app-text-subtle: #64748b;
--app-surface: #1e1e1e;
--app-surface-alt: #1e1e1e;
--app-surface-hover: #252525;
--app-surface-inset: #2a2a2a;
--app-border: #2d2d2d;
--app-border-subtle: #2d2d2d;
--app-border-muted: #2d2d2d;
--app-input-bg: #2a2a2a;
--app-input-bg-focus: #2a2a2a;
--app-input-border: #3d3d3d;
--app-input-text: #f1f5f9;
--app-accent: #00adb5;
--app-accent-light: #00adb5;
--app-accent-gradient: linear-gradient(135deg, #00adb5 0%, #008f95 100%);
--app-accent-bg: rgba(0, 173, 181, 0.12);
--app-accent-border: rgba(0, 173, 181, 0.3);
--app-accent-focus-ring: rgba(0, 173, 181, 0.2);
--app-btn-primary-text: #ffffff;
--app-btn-secondary-bg: #2a2a2a;
--app-btn-secondary-border: #3d3d3d;
--app-btn-secondary-text: #f1f5f9;
--app-btn-secondary-hover-bg: #333333;
--app-icon-btn-bg: #2a2a2a;
--app-icon-btn-border: #3d3d3d;
--app-divider: #2d2d2d;
--app-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
--app-card-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
--app-error-bg: rgba(244, 63, 94, 0.08);
--app-error-text: #fda4af;
--app-error-border: #f43f5e;
--app-warning-text: #f43f5e;
--app-warning-bg: rgba(244, 63, 94, 0.08);
--app-warning-border: rgba(244, 63, 94, 0.2);
--app-empty-border: #2d2d2d;
--app-empty-bg: #1a1a1a;
--app-sidebar-active-bg: rgba(0, 173, 181, 0.08);
--app-sidebar-active-border: #00adb5;
--app-sidebar-active-text: #00adb5;
--app-header-border: #2d2d2d;
--app-table-border: #2d2d2d;
--app-progress-bar: linear-gradient(90deg, #00adb5, #008f95, #00adb5);
--app-backdrop: none;
--app-radius-card: 4px;
--app-radius-input: 4px;
--app-radius-btn: 4px;
}
/* ===== MATERIAL · LIGHT ===== */
html.scheme-light.theme-material {
color-scheme: light;
--app-body-bg: #fafafa;
--app-text: #212121;
--app-text-heading: #111827;
--app-text-muted: #616161;
--app-text-subtle: #757575;
--app-surface: #ffffff;
--app-surface-alt: #ffffff;
--app-surface-hover: #f5f5f5;
--app-surface-inset: #f5f5f5;
--app-border: #e0e0e0;
--app-border-subtle: #eeeeee;
--app-border-muted: #e0e0e0;
--app-input-bg: #ffffff;
--app-input-bg-focus: #ffffff;
--app-input-border: #bdbdbd;
--app-input-text: #212121;
--app-accent: #00838f;
--app-accent-light: #00838f;
--app-accent-gradient: linear-gradient(135deg, #00838f 0%, #006064 100%);
--app-accent-bg: rgba(0, 131, 143, 0.1);
--app-accent-border: rgba(0, 131, 143, 0.25);
--app-accent-focus-ring: rgba(0, 131, 143, 0.2);
--app-btn-primary-text: #ffffff;
--app-btn-secondary-bg: #f5f5f5;
--app-btn-secondary-border: #e0e0e0;
--app-btn-secondary-text: #424242;
--app-btn-secondary-hover-bg: #eeeeee;
--app-icon-btn-bg: #f5f5f5;
--app-icon-btn-border: #e0e0e0;
--app-divider: #e0e0e0;
--app-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
--app-card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
--app-error-bg: rgba(244, 63, 94, 0.08);
--app-error-text: #be123c;
--app-error-border: #e11d48;
--app-warning-text: #be123c;
--app-warning-bg: rgba(244, 63, 94, 0.06);
--app-warning-border: rgba(244, 63, 94, 0.2);
--app-empty-border: #e0e0e0;
--app-empty-bg: #fafafa;
--app-sidebar-active-bg: rgba(0, 131, 143, 0.08);
--app-sidebar-active-border: #00838f;
--app-sidebar-active-text: #00838f;
--app-header-border: #e0e0e0;
--app-table-border: #e0e0e0;
--app-progress-bar: linear-gradient(90deg, #00838f, #00adb5, #00838f);
--app-backdrop: none;
--app-radius-card: 4px;
--app-radius-input: 4px;
--app-radius-btn: 4px;
}
/* ===== CUPERTINO · DARK ===== */
html.scheme-dark.theme-cupertino {
color-scheme: dark;
--app-body-bg: #000000;
--app-text: #ffffff;
--app-text-heading: #ffffff;
--app-text-muted: #aeaeb2;
--app-text-subtle: #8e8e93;
--app-surface: rgba(28, 28, 30, 0.72);
--app-surface-alt: rgba(28, 28, 30, 0.72);
--app-surface-hover: rgba(44, 44, 46, 0.85);
--app-surface-inset: rgba(255, 255, 255, 0.05);
--app-border: rgba(255, 255, 255, 0.1);
--app-border-subtle: rgba(255, 255, 255, 0.1);
--app-border-muted: rgba(255, 255, 255, 0.08);
--app-input-bg: rgba(255, 255, 255, 0.05);
--app-input-bg-focus: rgba(255, 255, 255, 0.07);
--app-input-border: rgba(255, 255, 255, 0.12);
--app-input-text: #ffffff;
--app-accent: #0a84ff;
--app-accent-light: #0a84ff;
--app-accent-gradient: linear-gradient(135deg, #0a84ff 0%, #007aff 100%);
--app-accent-bg: rgba(10, 132, 255, 0.12);
--app-accent-border: rgba(10, 132, 255, 0.3);
--app-accent-focus-ring: rgba(10, 132, 255, 0.25);
--app-btn-primary-text: #ffffff;
--app-btn-secondary-bg: rgba(255, 255, 255, 0.08);
--app-btn-secondary-border: rgba(255, 255, 255, 0.12);
--app-btn-secondary-text: #ffffff;
--app-btn-secondary-hover-bg: rgba(255, 255, 255, 0.12);
--app-icon-btn-bg: rgba(255, 255, 255, 0.08);
--app-icon-btn-border: rgba(255, 255, 255, 0.12);
--app-divider: rgba(255, 255, 255, 0.08);
--app-shadow: none;
--app-card-shadow: none;
--app-error-bg: rgba(255, 69, 58, 0.12);
--app-error-text: #ff6961;
--app-error-border: #ff453a;
--app-warning-text: #ff6961;
--app-warning-bg: rgba(255, 69, 58, 0.12);
--app-warning-border: rgba(255, 69, 58, 0.25);
--app-empty-border: rgba(255, 255, 255, 0.1);
--app-empty-bg: rgba(255, 255, 255, 0.04);
--app-sidebar-active-bg: rgba(10, 132, 255, 0.15);
--app-sidebar-active-border: #0a84ff;
--app-sidebar-active-text: #0a84ff;
--app-header-border: rgba(255, 255, 255, 0.1);
--app-table-border: rgba(255, 255, 255, 0.1);
--app-progress-bar: linear-gradient(90deg, #0a84ff, #007aff, #0a84ff);
--app-backdrop: blur(25px);
--app-radius-card: 12px;
--app-radius-input: 8px;
--app-radius-btn: 9999px;
}
/* ===== CUPERTINO · LIGHT ===== */
html.scheme-light.theme-cupertino {
color-scheme: light;
--app-body-bg: #f2f2f7;
--app-text: #1c1c1e;
--app-text-heading: #000000;
--app-text-muted: #636366;
--app-text-subtle: #8e8e93;
--app-surface: rgba(255, 255, 255, 0.82);
--app-surface-alt: rgba(255, 255, 255, 0.82);
--app-surface-hover: rgba(255, 255, 255, 0.95);
--app-surface-inset: rgba(0, 0, 0, 0.03);
--app-border: rgba(0, 0, 0, 0.08);
--app-border-subtle: rgba(0, 0, 0, 0.06);
--app-border-muted: rgba(0, 0, 0, 0.08);
--app-input-bg: #ffffff;
--app-input-bg-focus: #ffffff;
--app-input-border: rgba(0, 0, 0, 0.12);
--app-input-text: #1c1c1e;
--app-accent: #007aff;
--app-accent-light: #007aff;
--app-accent-gradient: linear-gradient(135deg, #007aff 0%, #0a84ff 100%);
--app-accent-bg: rgba(0, 122, 255, 0.1);
--app-accent-border: rgba(0, 122, 255, 0.25);
--app-accent-focus-ring: rgba(0, 122, 255, 0.2);
--app-btn-primary-text: #ffffff;
--app-btn-secondary-bg: rgba(0, 0, 0, 0.05);
--app-btn-secondary-border: rgba(0, 0, 0, 0.08);
--app-btn-secondary-text: #1c1c1e;
--app-btn-secondary-hover-bg: rgba(0, 0, 0, 0.08);
--app-icon-btn-bg: rgba(0, 0, 0, 0.05);
--app-icon-btn-border: rgba(0, 0, 0, 0.08);
--app-divider: rgba(0, 0, 0, 0.08);
--app-shadow: none;
--app-card-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
--app-error-bg: rgba(255, 59, 48, 0.1);
--app-error-text: #d70015;
--app-error-border: #ff3b30;
--app-warning-text: #d70015;
--app-warning-bg: rgba(255, 59, 48, 0.08);
--app-warning-border: rgba(255, 59, 48, 0.2);
--app-empty-border: rgba(0, 0, 0, 0.08);
--app-empty-bg: rgba(0, 0, 0, 0.02);
--app-sidebar-active-bg: rgba(0, 122, 255, 0.1);
--app-sidebar-active-border: #007aff;
--app-sidebar-active-text: #007aff;
--app-header-border: rgba(0, 0, 0, 0.08);
--app-table-border: rgba(0, 0, 0, 0.08);
--app-progress-bar: linear-gradient(90deg, #007aff, #0a84ff, #007aff);
--app-backdrop: blur(25px);
--app-radius-card: 12px;
--app-radius-input: 8px;
--app-radius-btn: 9999px;
}
/* Utility classes for inline-style migration */
.text-muted { color: var(--app-text-muted); }
.text-subtle { color: var(--app-text-subtle); }
.text-heading { color: var(--app-text-heading); }
html.scheme-light #root {
border-inline-color: var(--app-border-subtle);
}
+2
View File
@@ -61,6 +61,7 @@ async function getLogbookWithAccess(logbookId: string, userId: string) {
}
function hasWriteAccess(access: { isOwner: boolean; collaboration?: { role: string } | null }) {
// Intentional (HYBRID-ELECTRONIC-SIGNATURES.md §2.1): owner OR WRITE collaborator may sign entries.
return access.isOwner || access.collaboration?.role === 'WRITE'
}
@@ -106,6 +107,7 @@ async function isAuthorizedSigner(
role: 'skipper' | 'crew'
): Promise<boolean> {
if (role === 'skipper') {
// Skipper signing: owner or WRITE collaborator (design §2.1), using their own passkey.
if (signerUserId === ownerUserId) return true
const collaboration = await prisma.collaboration.findUnique({
where: {