Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2a28f5782 | |||
| 4d2e309967 | |||
| 2f6c668ca4 | |||
| 42736fedf3 | |||
| ac84fef832 | |||
| 404eb79add | |||
| 14b52c684d |
+262
-332
@@ -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
@@ -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} />
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'))
|
||||
}
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user