feat(portal): add light/dark theme sync and OA drive navigation
Memind CI / Test, build, and release guards (push) Successful in 4m31s

Unify chat and OA drive appearance with shared memind-theme storage, default light mode, and cross-frame sync so users can switch themes from chat or the drive shell.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-10 20:10:06 +08:00
parent 97a3e89ff7
commit 3e6b0c262a
10 changed files with 520 additions and 128 deletions
+149 -84
View File
@@ -1,21 +1,86 @@
:root {
--bg: #0f1117;
--panel: #171a22;
--border: #2a3140;
--text: #e8ecf4;
--muted: #8b95a8;
--accent: #5b8cff;
:root,
[data-theme='light'] {
color-scheme: light;
--bg: #f4f1ea;
--bg-gradient-start: #fffaf0;
--panel: #fffaf0;
--border: rgba(24, 33, 29, 0.12);
--text: #18211d;
--muted: #52605a;
--accent: #2f6f57;
--accent-2: #2f6f57;
--danger: #c4453d;
--drop: rgba(47, 111, 87, 0.12);
--warning: #eeb04e;
--overlay: rgba(244, 241, 234, 0.72);
--finder-bg: #fffaf0;
--finder-bg-head: #f0ebe0;
--finder-bg-body: #ffffff;
--finder-border: rgba(24, 33, 29, 0.14);
--finder-text: #18211d;
--finder-muted: #52605a;
--finder-subtle: #7a8780;
--finder-surface: #ffffff;
--finder-accent: #2f6f57;
--finder-hover: rgba(47, 111, 87, 0.08);
--finder-selected: rgba(47, 111, 87, 0.16);
--finder-drop: rgba(47, 111, 87, 0.12);
--finder-danger: #c4453d;
--finder-input-border: rgba(24, 33, 29, 0.18);
--finder-focus-ring: rgba(47, 111, 87, 0.15);
--finder-accent-ghost: rgba(47, 111, 87, 0.1);
--finder-chip-active-bg: rgba(47, 111, 87, 0.12);
--finder-chip-active-border: rgba(47, 111, 87, 0.35);
--finder-resizer: #7a8780;
--finder-row-border: rgba(24, 33, 29, 0.06);
--finder-bc-bg: #f4f1ea;
--finder-empty-bg: #faf8f4;
}
[data-theme='dark'] {
color-scheme: dark;
--bg: #0f1419;
--bg-gradient-start: #1a2236;
--panel: #121820;
--border: #243041;
--text: #e7ecf3;
--muted: #8fa3bf;
--accent: #3d8bfd;
--accent-2: #3dd68c;
--danger: #ff6b7a;
--drop: rgba(91, 140, 255, 0.12);
--warning: #ffb347;
--overlay: rgba(15, 17, 23, 0.72);
--finder-bg: #121820;
--finder-bg-head: #1c2735;
--finder-bg-body: #0f1419;
--finder-border: #243041;
--finder-text: #e7ecf3;
--finder-muted: #8fa3bf;
--finder-subtle: #6b829e;
--finder-surface: #1c2735;
--finder-accent: #3d8bfd;
--finder-hover: rgba(61, 139, 253, 0.12);
--finder-selected: rgba(61, 139, 253, 0.22);
--finder-drop: rgba(61, 139, 253, 0.18);
--finder-danger: #ff6b7a;
--finder-input-border: #2a3a50;
--finder-focus-ring: rgba(61, 139, 253, 0.25);
--finder-accent-ghost: rgba(61, 139, 253, 0.1);
--finder-chip-active-bg: rgba(61, 139, 253, 0.15);
--finder-chip-active-border: rgba(61, 139, 253, 0.45);
--finder-resizer: #6b829e;
--finder-row-border: #243041;
--finder-bc-bg: #1c2735;
--finder-empty-bg: #121820;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: radial-gradient(circle at top, #1a2236 0%, var(--bg) 45%);
background: radial-gradient(circle at top, var(--bg-gradient-start) 0%, var(--bg) 45%);
color: var(--text);
}
.wrap { max-width: min(1280px, 96vw); margin: 0 auto; padding: 10px 16px 20px; }
@@ -55,7 +120,7 @@ body {
white-space: nowrap; flex: none;
}
.link-chip:hover { border-color: var(--accent); }
.link-chip.active { border-color: var(--accent); background: #e8f0fe; color: #0066cc; font-weight: 600; }
.link-chip.active { border-color: var(--accent); background: var(--finder-chip-active-bg); color: var(--finder-accent); font-weight: 600; }
.top-actions { display: flex; gap: 4px; flex: none; }
.top-actions button { padding: 3px 8px; font-size: 11px; min-height: 0; line-height: 1.3; }
.top-actions button.primary { padding: 3px 9px; }
@@ -129,7 +194,7 @@ button:disabled { opacity: 0.45; cursor: not-allowed; filter: none; }
.drag-overlay {
position: fixed; inset: 0; z-index: 200; pointer-events: none;
display: none; align-items: center; justify-content: center;
background: rgba(15, 17, 23, 0.72); backdrop-filter: blur(4px);
background: var(--overlay); backdrop-filter: blur(4px);
}
.drag-overlay.show { display: flex; pointer-events: none; }
.drag-overlay-box {
@@ -144,8 +209,8 @@ button:disabled { opacity: 0.45; cursor: not-allowed; filter: none; }
.panel-shell { margin-top: 8px; position: relative; width: 100%; max-width: 100%; }
.panel {
display: flex; flex-direction: column;
background: #f5f5f7; border: 1px solid #d1d1d6;
border-radius: 10px; overflow: hidden; color: #1d1d1f;
background: var(--finder-bg); border: 1px solid var(--finder-border);
border-radius: 10px; overflow: hidden; color: var(--finder-text);
height: 520px; min-height: 280px; max-height: 85vh;
}
.panel-resizer {
@@ -154,57 +219,57 @@ button:disabled { opacity: 0.45; cursor: not-allowed; filter: none; }
}
.panel-resizer::after {
content: ''; position: absolute; right: 4px; bottom: 4px; width: 10px; height: 10px;
border-right: 2px solid #86868b; border-bottom: 2px solid #86868b;
border-right: 2px solid var(--finder-subtle); border-bottom: 2px solid var(--finder-subtle);
border-radius: 0 0 2px 0; opacity: 0.75; transition: opacity .15s;
}
.panel-resizer:hover::after,
.panel-shell-resizing .panel-resizer::after { opacity: 1; border-color: #0066cc; }
.panel-shell-resizing .panel-resizer::after { opacity: 1; border-color: var(--finder-accent); }
body.panel-shell-resizing { user-select: none; cursor: nwse-resize; }
/* Toolbar */
.panel-head {
display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px;
padding: 10px 14px; border-bottom: 1px solid #d1d1d6; font-weight: 600;
font-size: 13px; background: #ebebed; flex: none;
padding: 10px 14px; border-bottom: 1px solid var(--finder-border); font-weight: 600;
font-size: 13px; background: var(--finder-bg-head); flex: none;
}
.panel-head small { color: #6e6e73; font-weight: 400; }
.panel-head small { color: var(--finder-muted); font-weight: 400; }
.panel-head button {
background: #fff; border-color: #c7c7cc; color: #1d1d1f; padding: 4px 10px;
background: var(--finder-surface); border-color: var(--finder-input-border); color: var(--finder-text); padding: 4px 10px;
border-radius: 6px; font-size: 12px;
}
.panel-head .bulk-actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.panel-head .select-all-label {
display: inline-flex; align-items: center; gap: 4px; font-weight: 400;
font-size: 12px; color: #6e6e73; cursor: pointer; user-select: none;
font-size: 12px; color: var(--finder-muted); cursor: pointer; user-select: none;
}
.panel-head .select-all-label input { width: 14px; height: 14px; accent-color: #0066cc; cursor: pointer; }
.panel-head .select-all-label input { width: 14px; height: 14px; accent-color: var(--finder-accent); cursor: pointer; }
.panel-head .bulk-actions button.danger {
background: #fff; border-color: #ff3b30; color: #ff3b30;
background: var(--finder-surface); border-color: var(--finder-danger); color: var(--finder-danger);
}
.panel-head .bulk-actions button.danger:disabled { opacity: 0.45; cursor: not-allowed; }
.panel-head .bulk-actions button.danger:not(:disabled):hover { background: #ff3b30; color: #fff; }
.panel-head .bulk-actions button.danger:not(:disabled):hover { background: var(--finder-danger); color: #fff; }
/* Search bar */
.toolbar-search {
display: flex; align-items: center; gap: 6px; flex: 1; min-width: 140px; max-width: 220px;
}
.toolbar-search input {
flex: 1; min-width: 0; padding: 4px 8px; border: 1px solid #c7c7cc; border-radius: 6px;
font-size: 12px; background: #fff; color: #1d1d1f;
flex: 1; min-width: 0; padding: 4px 8px; border: 1px solid var(--finder-input-border); border-radius: 6px;
font-size: 12px; background: var(--finder-surface); color: var(--finder-text);
}
.toolbar-search input:focus { outline: none; border-color: #0066cc; box-shadow: 0 0 0 2px rgba(0,102,204,.15); }
.toolbar-search input::placeholder { color: #aeaeb2; }
.toolbar-search input:focus { outline: none; border-color: var(--finder-accent); box-shadow: 0 0 0 2px var(--finder-focus-ring); }
.toolbar-search input::placeholder { color: var(--finder-subtle); }
/* Conflict policy select */
.conflict-select {
padding: 4px 6px; border: 1px solid #c7c7cc; border-radius: 6px;
font-size: 11px; background: #fff; color: #1d1d1f; cursor: pointer;
padding: 4px 6px; border: 1px solid var(--finder-input-border); border-radius: 6px;
font-size: 11px; background: var(--finder-surface); color: var(--finder-text); cursor: pointer;
}
.conflict-select:focus { outline: none; border-color: #0066cc; }
.conflict-select:focus { outline: none; border-color: var(--finder-accent); }
/* Notification toggle */
.notify-btn { position: relative; }
.notify-btn.active { background: #e8f0fe; border-color: #0066cc; color: #0066cc; }
.notify-btn.active { background: var(--finder-hover); border-color: var(--finder-accent); color: var(--finder-accent); }
.notify-btn .badge {
position: absolute; top: -4px; right: -4px; width: 8px; height: 8px;
border-radius: 50%; background: var(--accent-2); display: none;
@@ -212,29 +277,29 @@ body.panel-shell-resizing { user-select: none; cursor: nwse-resize; }
.notify-btn.has-unread .badge { display: block; }
.view-toggle {
display: inline-flex; border: 1px solid #c7c7cc; border-radius: 6px; overflow: hidden; background: #fff;
display: inline-flex; border: 1px solid var(--finder-input-border); border-radius: 6px; overflow: hidden; background: var(--finder-surface);
}
.view-toggle button {
border: none; border-radius: 0; padding: 4px 8px; min-height: 0; background: #fff; color: #6e6e73;
border: none; border-radius: 0; padding: 4px 8px; min-height: 0; background: var(--finder-surface); color: var(--finder-muted);
font-size: 14px; line-height: 1;
}
.view-toggle button + button { border-left: 1px solid #c7c7cc; }
.view-toggle button.active { background: #e8f0fe; color: #0066cc; }
.view-toggle button:hover { filter: none; background: #f2f2f7; }
.view-toggle button.active:hover { background: #e8f0fe; }
.view-toggle button + button { border-left: 1px solid var(--finder-input-border); }
.view-toggle button.active { background: var(--finder-hover); color: var(--finder-accent); }
.view-toggle button:hover { filter: none; background: var(--finder-hover); }
.view-toggle button.active:hover { background: var(--finder-hover); }
.breadcrumb {
display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
padding: 8px 14px; border-bottom: 1px solid #d1d1d6; background: #fafafa;
font-size: 12px; color: #6e6e73; flex: none;
padding: 8px 14px; border-bottom: 1px solid var(--finder-border); background: var(--finder-bc-bg);
font-size: 12px; color: var(--finder-muted); flex: none;
}
.breadcrumb .bc-link {
background: none; border: none; padding: 0; color: #0066cc; cursor: pointer;
background: none; border: none; padding: 0; color: var(--finder-accent); cursor: pointer;
font-size: 12px; border-radius: 0; min-height: 0;
}
.breadcrumb .bc-link:hover { text-decoration: underline; filter: none; }
.breadcrumb .bc-sep { color: #aeaeb2; user-select: none; }
.breadcrumb .bc-current { color: #1d1d1f; font-weight: 600; }
.breadcrumb .bc-sep { color: var(--finder-subtle); user-select: none; }
.breadcrumb .bc-current { color: var(--finder-text); font-weight: 600; }
/* File list */
.file-list {
@@ -252,30 +317,30 @@ body.panel-shell-resizing { user-select: none; cursor: nwse-resize; }
}
.list-header .col-check input, .list-row .col-check input,
.list-row .col-check {
width: 14px; height: 14px; cursor: pointer; accent-color: #0066cc;
width: 14px; height: 14px; cursor: pointer; accent-color: var(--finder-accent);
}
.list-row .col-check { width: 28px; margin: 0; }
.list-header {
height: 28px; background: #ebebed; border-bottom: 1px solid #d1d1d6;
color: #6e6e73; font-size: 12px; user-select: none; flex: none;
height: 28px; background: var(--finder-bg-head); border-bottom: 1px solid var(--finder-border);
color: var(--finder-muted); font-size: 12px; user-select: none; flex: none;
}
.list-header .sortable {
cursor: pointer; display: inline-flex; align-items: center; gap: 4px;
}
.list-header .sortable:hover { color: #1d1d1f; }
.list-header .sortable.active { color: #1d1d1f; font-weight: 600; }
.list-header .sortable:hover { color: var(--finder-text); }
.list-header .sortable.active { color: var(--finder-text); font-weight: 600; }
.list-header .col-size, .list-row .col-size { text-align: right; font-variant-numeric: tabular-nums; }
.list-header .col-date, .list-row .col-date { color: #6e6e73; font-variant-numeric: tabular-nums; white-space: nowrap; }
.list-header .col-date, .list-row .col-date { color: var(--finder-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.list-body {
background: #fff; flex: 1; min-height: 0; overflow: auto; position: relative;
background: var(--finder-bg-body); flex: 1; min-height: 0; overflow: auto; position: relative;
}
.list-body.drop-target { background: #d6e8ff; outline: 1px dashed #0066cc; outline-offset: -2px; }
.list-body.drop-target { background: var(--finder-drop); outline: 1px dashed var(--finder-accent); outline-offset: -2px; }
.list-row {
min-height: 28px; border-bottom: 1px solid #ececf0; position: relative;
min-height: 28px; border-bottom: 1px solid var(--finder-row-border); position: relative;
}
.list-row:last-child { border-bottom: none; }
.list-row:hover { background: #e8f0fe; }
.list-row.selected { background: #cce4ff; }
.list-row:hover { background: var(--finder-hover); }
.list-row.selected { background: var(--finder-selected); }
.list-row.folder { cursor: default; }
.list-row[draggable="true"], .icon-item[draggable="true"] { cursor: grab; }
.list-row.dragging, .icon-item.dragging { opacity: 0.45; }
@@ -283,24 +348,24 @@ body.panel-shell-resizing { user-select: none; cursor: nwse-resize; }
display: flex; align-items: center; gap: 6px; min-width: 0;
}
.list-row .chevron {
width: 12px; flex: none; color: #86868b; font-size: 10px; text-align: center;
width: 12px; flex: none; color: var(--finder-subtle); font-size: 10px; text-align: center;
}
.list-row .chevron.hidden { visibility: hidden; }
/* Origin badge */
.origin-badge {
flex: none; font-size: 9px; padding: 1px 5px; border-radius: 999px;
background: #f2f2f7; border: 1px solid #d1d1d6; color: #6e6e73;
background: var(--finder-hover); border: 1px solid var(--finder-border); color: var(--finder-muted);
max-width: 72px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.origin-badge.self { background: #e8f0fe; border-color: #99caff; color: #0066cc; }
.origin-badge.self { background: var(--finder-hover); border-color: var(--finder-chip-active-border); color: var(--finder-accent); }
/* Inline rename */
.rename-input {
flex: 1; min-width: 0; padding: 1px 4px; border: 1px solid #0066cc; border-radius: 4px;
font-size: 13px; font-family: inherit; background: #fff; color: #1d1d1f;
flex: 1; min-width: 0; padding: 1px 4px; border: 1px solid var(--finder-accent); border-radius: 4px;
font-size: 13px; font-family: inherit; background: var(--finder-surface); color: var(--finder-text);
}
.rename-input:focus { outline: none; box-shadow: 0 0 0 2px rgba(0,102,204,.2); }
.rename-input:focus { outline: none; box-shadow: 0 0 0 2px var(--finder-focus-ring); }
.file-icon {
width: 16px; height: 16px; flex: none; background-size: contain; background-repeat: no-repeat;
@@ -325,51 +390,51 @@ body.panel-shell-resizing { user-select: none; cursor: nwse-resize; }
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect fill='%235b8cff' x='2' y='3' width='12' height='10' rx='1.5'/%3E%3Cpath fill='%23fff' d='M7 6.5v3l3-1.5z'/%3E%3C/svg%3E");
}
.list-row .name-link {
color: #1d1d1f; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
color: var(--finder-text); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.list-row .name-link.previewable:hover { color: #0066cc; text-decoration: underline; }
.list-row .name-link.previewable:hover { color: var(--finder-accent); text-decoration: underline; }
.list-row .name-text {
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #1d1d1f;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--finder-text);
}
.list-row .col-size { color: #6e6e73; }
.list-row .col-size { color: var(--finder-muted); }
.list-row .col-actions {
display: flex; justify-content: flex-end; gap: 4px; opacity: 0;
}
.list-row:hover .col-actions { opacity: 1; }
.list-row .col-actions button, .list-row .col-actions a {
background: transparent; border: none; color: #0066cc; padding: 2px 4px;
background: transparent; border: none; color: var(--finder-accent); padding: 2px 4px;
font-size: 12px; border-radius: 4px; min-height: 0;
}
.list-row .col-actions button:hover, .list-row .col-actions a:hover {
background: rgba(0,102,204,.1); filter: none;
background: var(--finder-accent-ghost); filter: none;
}
.list-row .col-actions button[data-del] { color: #ff3b30; }
.list-row .col-actions button[data-info] { color: #6e6e73; }
.list-row .col-actions button[data-del] { color: var(--finder-danger); }
.list-row .col-actions button[data-info] { color: var(--finder-muted); }
/* Empty + empty folder drop zone */
.empty {
padding: 40px 18px; text-align: center; color: #86868b; background: #fff; font-size: 13px;
padding: 40px 18px; text-align: center; color: var(--finder-subtle); background: var(--finder-bg-body); font-size: 13px;
}
.empty-drop-zone {
margin: 24px auto; max-width: 360px; padding: 32px 20px;
border: 2px dashed #c7c7cc; border-radius: 12px; background: #fafafa;
color: #6e6e73; transition: .15s;
border: 2px dashed var(--finder-input-border); border-radius: 12px; background: var(--finder-bc-bg);
color: var(--finder-muted); transition: .15s;
}
.empty-drop-zone.dragover {
border-color: #0066cc; background: #d6e8ff; color: #0066cc;
border-color: var(--finder-accent); background: var(--finder-drop); color: var(--finder-accent);
}
.empty-drop-zone .icon { font-size: 32px; margin-bottom: 8px; }
/* Load more */
.load-more-wrap { padding: 10px; text-align: center; background: #fff; border-top: 1px solid #ececf0; }
.load-more-wrap { padding: 10px; text-align: center; background: var(--finder-bg-body); border-top: 1px solid var(--finder-row-border); }
.load-more-btn {
background: #fff; border: 1px solid #c7c7cc; color: #0066cc; padding: 6px 16px;
background: var(--finder-surface); border: 1px solid var(--finder-input-border); color: var(--finder-accent); padding: 6px 16px;
border-radius: 6px; font-size: 12px; cursor: pointer;
}
.load-more-btn:hover { background: #e8f0fe; filter: none; }
.load-more-btn:hover { background: var(--finder-hover); filter: none; }
input[type=file] { display: none; }
.list-row.drop-target { background: #d6e8ff !important; }
.list-row.drop-target { background: var(--finder-drop) !important; }
/* Icon mode */
.file-list.icon-mode .list-header { display: none; }
@@ -383,16 +448,16 @@ input[type=file] { display: none; }
border: 1px solid transparent; min-width: 0; text-align: center;
}
.icon-item.folder { cursor: default; }
.icon-item:hover { background: #e8f0fe; border-color: #d1e3ff; }
.icon-item.selected { background: #cce4ff; border-color: #99caff; }
.icon-item.drop-target { background: #d6e8ff !important; border-color: #0066cc; }
.icon-item:hover { background: var(--finder-hover); border-color: var(--finder-chip-active-border); }
.icon-item.selected { background: var(--finder-selected); border-color: var(--finder-chip-active-border); }
.icon-item.drop-target { background: var(--finder-drop) !important; border-color: var(--finder-accent); }
.icon-item .item-check-wrap {
position: absolute; top: 4px; left: 4px; z-index: 2;
display: flex; align-items: center; justify-content: center;
width: 22px; height: 22px; cursor: pointer;
}
.icon-item .item-check {
width: 14px; height: 14px; accent-color: #0066cc; cursor: pointer; margin: 0;
width: 14px; height: 14px; accent-color: var(--finder-accent); cursor: pointer; margin: 0;
}
.icon-item .icon-thumb {
width: 56px; height: 56px; display: flex; align-items: center; justify-content: center; flex: none;
@@ -400,24 +465,24 @@ input[type=file] { display: none; }
.icon-item .icon-thumb .file-icon { width: 48px; height: 48px; }
.icon-item .icon-thumb-img {
width: 56px; height: 56px; object-fit: cover; border-radius: 6px;
border: 1px solid #ececf0; background: #fafafa;
border: 1px solid var(--finder-row-border); background: var(--finder-bc-bg);
}
.icon-item .icon-name {
width: 100%; font-size: 11px; line-height: 1.35; color: #1d1d1f;
width: 100%; font-size: 11px; line-height: 1.35; color: var(--finder-text);
overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
word-break: break-all;
}
.icon-item .icon-name a { color: inherit; text-decoration: none; }
.icon-item .icon-name a.previewable:hover { color: #0066cc; text-decoration: underline; }
.icon-item .icon-meta { font-size: 10px; color: #86868b; line-height: 1.2; }
.icon-item .icon-name a.previewable:hover { color: var(--finder-accent); text-decoration: underline; }
.icon-item .icon-meta { font-size: 10px; color: var(--finder-subtle); line-height: 1.2; }
.icon-item .icon-actions {
display: flex; gap: 6px; opacity: 0; font-size: 11px;
}
.icon-item:hover .icon-actions { opacity: 1; }
.icon-item .icon-actions a, .icon-item .icon-actions button {
background: none; border: none; color: #0066cc; padding: 0; font-size: 11px; min-height: 0;
background: none; border: none; color: var(--finder-accent); padding: 0; font-size: 11px; min-height: 0;
}
.icon-item .icon-actions button[data-del] { color: #ff3b30; }
.icon-item .icon-actions button[data-del] { color: var(--finder-danger); }
/* Modals (shared) */
.modal {
+101 -10
View File
@@ -1,6 +1,59 @@
(() => {
'use strict';
const THEME_STORAGE_KEY = 'memind-theme';
function getStoredTheme() {
const stored = localStorage.getItem(THEME_STORAGE_KEY);
return stored === 'dark' ? 'dark' : 'light';
}
function applyTheme(theme) {
const normalized = theme === 'dark' ? 'dark' : 'light';
document.documentElement.dataset.theme = normalized;
document.documentElement.style.colorScheme = normalized;
}
function updateThemeToggleLabel() {
const themeToggle = document.getElementById('themeToggle');
if (!themeToggle) return;
themeToggle.textContent = getStoredTheme() === 'light' ? '深色' : '浅色';
themeToggle.title = getStoredTheme() === 'light' ? '切换深色模式' : '切换浅色模式';
}
function toggleTheme() {
const next = getStoredTheme() === 'light' ? 'dark' : 'light';
localStorage.setItem(THEME_STORAGE_KEY, next);
applyTheme(next);
updateThemeToggleLabel();
window.dispatchEvent(new CustomEvent('memind-theme-change', { detail: { theme: next } }));
return next;
}
function initTheme() {
applyTheme(getStoredTheme());
updateThemeToggleLabel();
window.addEventListener('storage', (event) => {
if (event.key === THEME_STORAGE_KEY) {
applyTheme(getStoredTheme());
updateThemeToggleLabel();
}
});
window.addEventListener('memind-theme-change', (event) => {
const detail = event.detail;
if (detail?.theme) {
applyTheme(detail.theme);
updateThemeToggleLabel();
}
});
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
themeToggle.addEventListener('click', () => toggleTheme());
}
}
initTheme();
const API_BASE = '/api/mindspace/v1/oa/drive';
const fetchOpts = { credentials: 'same-origin' };
@@ -90,6 +143,7 @@
let pendingConflictResolve = null;
let uploadAbort = false;
let filesHydrated = false;
let currentUser = null;
let panelSize = (() => {
try {
@@ -172,6 +226,45 @@
statusEl.className = 'status ' + type;
}
function redirectToLogin() {
const returnTo = encodeURIComponent('/space/oa/drive');
const target = window.top && window.top !== window ? window.top : window;
target.location.href = `/?return_to=${returnTo}`;
}
async function ensureAuthenticated() {
const res = await fetch('/auth/status', fetchOpts);
if (!res.ok) {
redirectToLogin();
return null;
}
const data = await res.json();
if (!data.authenticated || !data.user?.id) {
redirectToLogin();
return null;
}
return data.user;
}
function renderUserBanner(user, quota) {
if (!linksEl || !user) return;
const name = user.displayName || user.username || user.slug || '用户';
const chips = [`<span class="link-chip active">${escapeHtml(name)} 的 OA 网盘</span>`];
if (quota) {
chips.push(`<span class="link-chip">剩余 ${fmtSize(quota.availableBytes)} / ${fmtSize(quota.quotaBytes)}</span>`);
}
linksEl.innerHTML = chips.join('');
}
async function driveFetch(url, opts = {}) {
const res = await fetch(url, { ...fetchOpts, ...opts });
if (res.status === 401 || res.status === 403) {
redirectToLogin();
throw new Error('未登录');
}
return res;
}
function viewUrl(name) {
return API_BASE + '/view/' + encodeURIComponent(name).replace(/%2F/g, '/');
}
@@ -734,23 +827,16 @@
/* ── API: load info ── */
async function loadInfo() {
const res = await fetch(API_BASE + '/info', fetchOpts);
const res = await driveFetch(API_BASE + '/info');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
serverInfo = await res.json();
displayLimit = serverInfo.pageSize || 100;
if (linksEl) {
const quota = serverInfo.quota;
if (quota) {
linksEl.innerHTML = `<span class="link-chip active">剩余 ${fmtSize(quota.availableBytes)} / ${fmtSize(quota.quotaBytes)}</span>`;
} else {
linksEl.innerHTML = '';
}
}
renderUserBanner(currentUser, serverInfo.quota);
}
async function refreshFiles() {
try {
const res = await fetch(API_BASE + '/files', fetchOpts);
const res = await driveFetch(API_BASE + '/files');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
applyFilePayload({ ...data, delta: false });
@@ -1754,6 +1840,11 @@
initPanelResize();
setViewMode(viewMode);
void (async () => {
currentUser = await ensureAuthenticated();
if (!currentUser) {
setStatus('未登录,正在跳转…', 'err');
return;
}
await loadInfo();
await refreshFiles();
connectEvents();
+1
View File
@@ -14,6 +14,7 @@
<span class="top-sub">云端同步 · 拖放/<kbd>⌘V</kbd>粘贴上传</span>
<div class="links" id="links"></div>
<div class="top-actions">
<button type="button" id="themeToggle" title="切换主题">深色</button>
<button type="button" class="primary" id="pickFiles">选择文件</button>
<button type="button" id="pickFolder">选择文件夹</button>
</div>
+2
View File
@@ -91,6 +91,7 @@ function AuthenticatedApp({
}
: undefined
}
onOpenOaDrive={user ? () => navigate('/space/oa/drive') : undefined}
onOpenPage={user ? (pageId) => navigate(`/space/page/${pageId}`) : undefined}
onOpenAdmin={
user?.role === 'admin'
@@ -121,6 +122,7 @@ function AuthenticatedApp({
element={<FeedbackRoutes user={user} onLogout={handleLogout} />}
/>
<Route path="/health" element={<Navigate to="/" replace />} />
<Route path="/oa-drive" element={<Navigate to="/space/oa/drive" replace />} />
<Route path="/" element={chatElement} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
+18
View File
@@ -18,6 +18,7 @@ import { WechatBindPrompt } from './WechatBindPrompt';
import { WechatAccountButton } from './WechatAccountButton';
import { ChatHeaderMoreMenu } from './ChatHeaderMoreMenu';
import { NotificationCenter } from './NotificationCenter';
import { ThemeToggle } from './ThemeToggle';
import { GoalRunAwaitingBanner } from './GoalRunAwaitingBanner';
import { HealthChannelBanner } from './HealthChannelBanner';
import { HealthAlertsPanel } from './HealthAlertsPanel';
@@ -173,6 +174,7 @@ export function ChatView({
onGrantedSkillsUpdate,
onLogout,
onOpenSpace,
onOpenOaDrive,
onOpenPage,
onOpenAdmin,
onOpenFeedback,
@@ -184,6 +186,7 @@ export function ChatView({
onGrantedSkillsUpdate?: (skills: string[]) => void;
onLogout?: () => void;
onOpenSpace?: (target?: { categoryCode?: MindSpaceSaveCategory; pageId?: string }) => void;
onOpenOaDrive?: () => void;
onOpenPage?: (pageId: string) => void;
onOpenAdmin?: () => void;
onOpenFeedback?: () => void;
@@ -327,6 +330,9 @@ export function ChatView({
: pendingSessionTitle;
const moreMenuItems = [
...(onOpenOaDrive
? [{ id: 'oa-drive', label: 'OA 网盘', onClick: () => onOpenOaDrive() }]
: []),
...(onOpenAdmin
? [{ id: 'admin', label: '管理', onClick: () => onOpenAdmin() }]
: []),
@@ -427,6 +433,16 @@ export function ChatView({
</button>
)}
{onOpenOaDrive && (
<button
type="button"
className="ghost-btn"
data-analytics-action="open_oa_drive"
onClick={() => onOpenOaDrive()}
>
OA
</button>
)}
{onOpenSpace && (
<button
ref={spaceButtonRef}
@@ -438,6 +454,7 @@ export function ChatView({
</button>
)}
<ThemeToggle />
{user && <WechatAccountButton returnTo={window.location.pathname} />}
{onLogout && (
<button type="button" className="ghost-btn logout-btn" data-analytics-action="logout" onClick={onLogout}>
@@ -501,6 +518,7 @@ export function ChatView({
</svg>
</button>
)}
<ThemeToggle className="ghost-btn theme-toggle-btn theme-toggle-btn-mobile" />
<ChatHeaderMoreMenu
items={moreMenuItems}
returnTo={window.location.pathname}
+32
View File
@@ -0,0 +1,32 @@
import { useEffect, useState } from 'react';
import { getStoredTheme, toggleTheme, type MemindTheme } from '../utils/theme';
export function ThemeToggle({
className = 'ghost-btn theme-toggle-btn',
}: {
className?: string;
}) {
const [theme, setTheme] = useState<MemindTheme>(() => getStoredTheme());
useEffect(() => {
const onChange = (event: Event) => {
const detail = (event as CustomEvent<{ theme: MemindTheme }>).detail;
if (detail?.theme) setTheme(detail.theme);
};
window.addEventListener('memind-theme-change', onChange);
return () => window.removeEventListener('memind-theme-change', onChange);
}, []);
return (
<button
type="button"
className={className}
data-analytics-action="toggle_theme"
aria-label={theme === 'light' ? '切换深色模式' : '切换浅色模式'}
title={theme === 'light' ? '切换深色模式' : '切换浅色模式'}
onClick={() => setTheme(toggleTheme())}
>
{theme === 'light' ? '深色' : '浅色'}
</button>
);
}
+167 -31
View File
@@ -1,7 +1,54 @@
:root {
color-scheme: dark;
:root,
[data-theme='light'] {
color-scheme: light;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--color-bg-base: #f4f1ea;
--color-bg-surface: #fffaf0;
--color-bg-elevated: #ffffff;
--color-bg-active: #e8f0ea;
--color-bg-code: #f0ebe0;
--color-bg-warning: #fff6e5;
--color-bg-error: #fff0f0;
--color-bg-info: #eef6ff;
--color-text-primary: #18211d;
--color-text-secondary: #3d4a44;
--color-text-muted: #52605a;
--color-text-faint: #6b7872;
--color-text-meta: #7a8780;
--color-text-link: #2f6f57;
--color-text-error: #b42318;
--color-text-info: #2458a6;
--color-border: rgba(24, 33, 29, 0.12);
--color-border-input: rgba(24, 33, 29, 0.18);
--color-border-warning: rgba(238, 176, 78, 0.45);
--color-border-input-focus: #2f6f57;
--color-accent: #2f6f57;
--color-accent-user: #2458a6;
--color-danger: #c4453d;
--radius-sm: 8px;
--radius-xs: 4px;
--radius-md: 10px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-pill: 999px;
--space-xs: 4px;
--space-sm: 8px;
--space-md: 12px;
--space-lg: 16px;
background: var(--color-bg-base);
color: var(--color-text-primary);
}
[data-theme='dark'] {
color-scheme: dark;
--color-bg-base: #0f1419;
--color-bg-surface: #121820;
--color-bg-elevated: #1c2735;
@@ -28,21 +75,6 @@
--color-accent: #3d8bfd;
--color-accent-user: #2458a6;
--color-danger: #c4453d;
--radius-sm: 8px;
--radius-xs: 4px;
--radius-md: 10px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-pill: 999px;
--space-xs: 4px;
--space-sm: 8px;
--space-md: 12px;
--space-lg: 16px;
background: var(--color-bg-base);
color: var(--color-text-primary);
}
* {
@@ -76,11 +108,9 @@ body,
position: relative;
overflow: hidden;
background:
radial-gradient(circle at 50% 22%, rgba(86, 128, 255, 0.12), transparent 18%),
radial-gradient(circle at 50% 56%, rgba(107, 138, 209, 0.08), transparent 34%),
radial-gradient(circle at 12% 92%, rgba(67, 107, 180, 0.08), transparent 28%),
radial-gradient(circle at 88% 8%, rgba(160, 97, 255, 0.05), transparent 22%),
linear-gradient(180deg, #08101d 0%, #0a1220 44%, #0c1320 100%);
radial-gradient(circle at 50% 22%, rgba(47, 111, 87, 0.08), transparent 18%),
radial-gradient(circle at 50% 56%, rgba(238, 176, 78, 0.06), transparent 34%),
linear-gradient(180deg, #fffaf0 0%, #f4f1ea 44%, #ece6d9 100%);
}
.app-home::before {
@@ -88,6 +118,21 @@ body,
position: absolute;
inset: 0;
pointer-events: none;
background:
linear-gradient(90deg, rgba(24, 33, 29, 0.02), transparent 18%, transparent 82%, rgba(24, 33, 29, 0.02)),
radial-gradient(circle at center, transparent 0%, rgba(244, 241, 234, 0.18) 72%, rgba(244, 241, 234, 0.42) 100%);
}
[data-theme='dark'] .app-home {
background:
radial-gradient(circle at 50% 22%, rgba(86, 128, 255, 0.12), transparent 18%),
radial-gradient(circle at 50% 56%, rgba(107, 138, 209, 0.08), transparent 34%),
radial-gradient(circle at 12% 92%, rgba(67, 107, 180, 0.08), transparent 28%),
radial-gradient(circle at 88% 8%, rgba(160, 97, 255, 0.05), transparent 22%),
linear-gradient(180deg, #08101d 0%, #0a1220 44%, #0c1320 100%);
}
[data-theme='dark'] .app-home::before {
background:
linear-gradient(90deg, rgba(255, 255, 255, 0.015), transparent 18%, transparent 82%, rgba(255, 255, 255, 0.015)),
radial-gradient(circle at center, transparent 0%, rgba(2, 8, 18, 0.18) 72%, rgba(2, 8, 18, 0.42) 100%);
@@ -897,16 +942,32 @@ body,
.header-home .ghost-btn,
.header-home .header-icon-btn,
.header-home .header-menu-btn {
background: rgba(24, 33, 29, 0.04);
color: rgba(24, 33, 29, 0.82);
border: 1px solid rgba(24, 33, 29, 0.12);
}
.header-home .header-title {
color: rgba(24, 33, 29, 0.96);
}
.header-home .header-sub {
color: rgba(24, 33, 29, 0.62);
}
[data-theme='dark'] .header-home .ghost-btn,
[data-theme='dark'] .header-home .header-icon-btn,
[data-theme='dark'] .header-home .header-menu-btn {
background: rgba(255, 255, 255, 0.04);
color: rgba(220, 231, 248, 0.82);
border: 1px solid rgba(103, 123, 156, 0.22);
}
.header-home .header-title {
[data-theme='dark'] .header-home .header-title {
color: rgba(238, 243, 255, 0.96);
}
.header-home .header-sub {
[data-theme='dark'] .header-home .header-sub {
color: rgba(180, 193, 219, 0.74);
}
@@ -3188,8 +3249,18 @@ body,
}
.chat-input-shell-home {
border: 1px solid rgba(96, 118, 153, 0.38);
border: 1px solid rgba(24, 33, 29, 0.14);
border-radius: 18px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 250, 240, 0.96)),
rgba(255, 255, 255, 0.94);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.6),
0 16px 36px rgba(24, 33, 29, 0.08);
}
[data-theme='dark'] .chat-input-shell-home {
border: 1px solid rgba(96, 118, 153, 0.38);
background:
linear-gradient(180deg, rgba(18, 27, 42, 0.94), rgba(13, 20, 32, 0.96)),
rgba(10, 18, 30, 0.94);
@@ -3216,12 +3287,20 @@ body,
padding-top: 14px;
padding-bottom: 14px;
font-size: 14px;
color: #edf4ff;
color: var(--color-text-primary);
background: transparent;
border-color: transparent;
}
.chat-input-row-home textarea.input.chat-input-field::placeholder {
color: var(--color-text-muted);
}
[data-theme='dark'] .chat-input-row-home textarea.input.chat-input-field {
color: #edf4ff;
}
[data-theme='dark'] .chat-input-row-home textarea.input.chat-input-field::placeholder {
color: rgba(175, 191, 216, 0.72);
}
@@ -3367,10 +3446,18 @@ body,
}
.chat-input-row-home .chat-input-task-status-text {
color: rgba(175, 191, 216, 0.72);
color: var(--color-text-muted);
}
.chat-input-row-home .chat-input-task-status-dots span {
background: var(--color-text-muted);
}
[data-theme='dark'] .chat-input-row-home .chat-input-task-status-text {
color: rgba(175, 191, 216, 0.72);
}
[data-theme='dark'] .chat-input-row-home .chat-input-task-status-dots span {
background: rgba(175, 191, 216, 0.72);
}
@@ -3766,6 +3853,13 @@ body,
.chat-input-row-home .chat-image-upload-trigger,
.chat-input-row-home .voice-btn {
border-color: var(--color-border-input);
background: var(--color-bg-elevated);
color: var(--color-text-secondary);
}
[data-theme='dark'] .chat-input-row-home .chat-image-upload-trigger,
[data-theme='dark'] .chat-input-row-home .voice-btn {
border-color: rgba(110, 132, 168, 0.3);
background: rgba(170, 192, 228, 0.08);
color: #d7e4fb;
@@ -12910,19 +13004,61 @@ body,
display: flex;
flex-direction: column;
height: 100dvh;
background: #f4f1ea;
background: var(--color-bg-base);
color: var(--color-text-primary);
}
.oa-drive-toolbar {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 16px;
border-bottom: 1px solid rgba(24, 33, 29, 0.08);
background: #fffaf0;
border-bottom: 1px solid var(--color-border);
background: var(--color-bg-surface);
}
.oa-drive-toolbar-start {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.oa-drive-toolbar-secondary {
opacity: 0.88;
}
.oa-drive-toolbar-end {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.oa-drive-toolbar-label {
color: var(--color-text-muted);
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.oa-drive-frame {
flex: 1 1 auto;
width: 100%;
border: 0;
background: #fff;
background: var(--color-bg-base);
}
.theme-toggle-btn {
min-width: 44px;
padding-inline: 12px;
}
.theme-toggle-btn-mobile {
min-width: 0;
padding: 8px 10px;
font-size: 12px;
}
+3
View File
@@ -2,8 +2,11 @@ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { App } from './App';
import { initTheme } from './utils/theme';
import './index.css';
initTheme();
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
+21 -3
View File
@@ -1,6 +1,11 @@
import { Navigate, useNavigate } from 'react-router-dom';
import { ThemeToggle } from '../components/ThemeToggle';
import type { PortalUser } from '../types';
function resolveUserLabel(user: PortalUser) {
return user.displayName?.trim() || user.username?.trim() || user.slug?.trim() || '用户';
}
export function OaDriveRoute({
user,
}: {
@@ -15,9 +20,22 @@ export function OaDriveRoute({
return (
<div className="oa-drive-shell">
<div className="oa-drive-toolbar">
<button type="button" className="mindspace-inline-back" onClick={() => navigate('/space?category=oa')}>
OA
</button>
<div className="oa-drive-toolbar-start">
<button type="button" className="mindspace-inline-back" onClick={() => navigate('/')}>
</button>
<button
type="button"
className="mindspace-inline-back oa-drive-toolbar-secondary"
onClick={() => navigate('/space?category=oa')}
>
OA
</button>
</div>
<div className="oa-drive-toolbar-end">
<span className="oa-drive-toolbar-label">{resolveUserLabel(user)} OA </span>
<ThemeToggle />
</div>
</div>
<iframe
className="oa-drive-frame"
+26
View File
@@ -0,0 +1,26 @@
export type MemindTheme = 'light' | 'dark';
export const THEME_STORAGE_KEY = 'memind-theme';
export function getStoredTheme(): MemindTheme {
const stored = localStorage.getItem(THEME_STORAGE_KEY);
return stored === 'dark' ? 'dark' : 'light';
}
export function applyTheme(theme: MemindTheme) {
const normalized: MemindTheme = theme === 'dark' ? 'dark' : 'light';
document.documentElement.dataset.theme = normalized;
document.documentElement.style.colorScheme = normalized;
localStorage.setItem(THEME_STORAGE_KEY, normalized);
window.dispatchEvent(new CustomEvent('memind-theme-change', { detail: { theme: normalized } }));
}
export function initTheme() {
applyTheme(getStoredTheme());
}
export function toggleTheme(): MemindTheme {
const next: MemindTheme = getStoredTheme() === 'light' ? 'dark' : 'light';
applyTheme(next);
return next;
}