diff --git a/hub/filelib-web/src/app.css b/hub/filelib-web/src/app.css
index 1beee79..526846b 100644
--- a/hub/filelib-web/src/app.css
+++ b/hub/filelib-web/src/app.css
@@ -23,8 +23,7 @@
}
html,
-body,
-#app {
+body {
height: 100%;
}
@@ -47,6 +46,204 @@ body {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
+/* 共享组件层(ADR-0029)。
+ *
+ * 从已删除的 hub/src/database/routes/uiTheme.ts 原样搬来。组件只带布局工具类,
+ * 不重述这里的组件样式 —— 第一版迁移只搬了上面的 @theme 令牌,把按钮/输入框/
+ * 面板样式在每个组件里内联重写了一遍,后台随即明显退化。 */
+@layer components {
+ .btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ padding: 6px 14px;
+ border-radius: 8px;
+ border: 1px solid var(--color-line);
+ background: var(--color-panel);
+ color: var(--color-ink);
+ font-size: 12.5px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 120ms ease;
+ white-space: nowrap;
+ }
+ .btn:hover {
+ background: var(--color-hover);
+ }
+ .btn-sm {
+ padding: 4px 9px;
+ font-size: 11.5px;
+ }
+ .btn-primary {
+ background: var(--color-accent);
+ border-color: var(--color-accent);
+ color: #fff;
+ }
+ .btn-primary:hover {
+ background: var(--color-accent-hover);
+ border-color: var(--color-accent-hover);
+ }
+ .btn-danger {
+ border-color: transparent;
+ background: transparent;
+ color: var(--color-danger);
+ }
+ .btn-danger:hover {
+ background: color-mix(in srgb, var(--color-danger) 7%, transparent);
+ }
+
+ .panel {
+ background: var(--color-panel);
+ border: 1px solid var(--color-line-soft);
+ border-radius: 10px;
+ padding: 20px 22px;
+ }
+
+ .tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ font-size: 10.5px;
+ font-weight: 500;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ padding: 2px 8px;
+ border-radius: 999px;
+ border: 1px solid var(--color-line-soft);
+ color: var(--color-ink-3);
+ background: var(--color-panel);
+ }
+
+ .input,
+ .select,
+ .textarea {
+ width: 100%;
+ padding: 7px 11px;
+ border-radius: 8px;
+ border: 1px solid var(--color-line);
+ background: var(--color-panel);
+ font-size: 13px;
+ color: var(--color-ink);
+ font-family: inherit;
+ outline: none;
+ transition: border-color 120ms ease;
+ }
+ .input:focus,
+ .select:focus,
+ .textarea:focus {
+ border-color: var(--color-accent);
+ }
+ .textarea {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ font-size: 12.5px;
+ line-height: 1.75;
+ resize: vertical;
+ }
+ .form-label {
+ display: block;
+ font-size: 11.5px;
+ color: var(--color-ink-3);
+ margin-bottom: 4px;
+ }
+ .form-row {
+ margin-bottom: 12px;
+ }
+
+ table.list {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 13px;
+ }
+ table.list th {
+ text-align: left;
+ font-size: 11.5px;
+ font-weight: 500;
+ color: var(--color-ink-3);
+ padding: 4px 0;
+ }
+ table.list td {
+ padding: 8px 0;
+ border-top: 1px solid var(--color-line-soft);
+ }
+ table.list tr:first-child td {
+ border-top: none;
+ }
+
+ .quiet {
+ color: var(--color-ink-3);
+ font-size: 12.5px;
+ }
+ .section-title {
+ font-size: 13px;
+ font-weight: 600;
+ }
+ .section-note {
+ font-size: 11.5px;
+ color: var(--color-ink-3);
+ }
+ .file-meta {
+ font-size: 11px;
+ color: var(--color-ink-3);
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ }
+ .link-danger {
+ color: var(--color-danger);
+ font-size: 12.5px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ }
+ .link-danger:hover {
+ text-decoration: underline;
+ }
+
+ /* 开关。真 checkbox 藏在下面 —— 保留键盘可达与 :checked 语义,不做 div 假开关。 */
+ .switch {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+ user-select: none;
+ }
+ .switch > input {
+ position: absolute;
+ opacity: 0;
+ width: 0;
+ height: 0;
+ }
+ .switch > span {
+ position: relative;
+ flex-shrink: 0;
+ width: 30px;
+ height: 17px;
+ border-radius: 999px;
+ background: var(--color-line);
+ transition: background 0.16s;
+ }
+ .switch > span::after {
+ content: "";
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 13px;
+ height: 13px;
+ border-radius: 50%;
+ background: #fff;
+ transition: transform 0.16s;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
+ }
+ .switch > input:checked + span {
+ background: var(--color-accent);
+ }
+ .switch > input:checked + span::after {
+ transform: translateX(13px);
+ }
+ .switch > input:focus-visible + span {
+ outline: 2px solid var(--color-accent);
+ outline-offset: 2px;
+ }
+}
+
/* diff 渲染 */
pre.diff {
white-space: pre-wrap;
diff --git a/hub/filelib-web/src/lib/Avatar.svelte b/hub/filelib-web/src/lib/Avatar.svelte
new file mode 100644
index 0000000..fd1ad7c
--- /dev/null
+++ b/hub/filelib-web/src/lib/Avatar.svelte
@@ -0,0 +1,31 @@
+
+
+{#if avatarUrl}
+
+{:else}
+
+{/if}
diff --git a/hub/filelib-web/src/lib/FileEditor.svelte b/hub/filelib-web/src/lib/FileEditor.svelte
index a046e1e..8339366 100644
--- a/hub/filelib-web/src/lib/FileEditor.svelte
+++ b/hub/filelib-web/src/lib/FileEditor.svelte
@@ -3,6 +3,7 @@
import { toastOk, toastErr, toast } from "./stores.js";
import type { FileContent, VersionInfo, Role } from "./types.js";
import Modal from "./Modal.svelte";
+ import Icon from "./Icon.svelte";
let { projectId, path, role, onchanged, onclose }: { projectId: string; path: string; role: Role; onchanged: () => void; onclose?: () => void } = $props();
@@ -104,32 +105,32 @@
{#if loadError}
-
{@html renderDiff(conflict.diff)}
| {f.path} | -{f.size} B | +{f.path} | +{f.size} B |