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} -
{loadError}
+
{loadError}
{:else if file} -
+
- {file.path} @ {file.version} -
- 下载 - + {file.path} @ {file.version} +
+ 下载 + {#if canEdit} - + {/if} {#if onclose} - + {/if}
{#if file.encoding === "base64"} -
二进制文件({file.size} B),不支持在线编辑
+
二进制文件({file.size} B),不支持在线编辑
{:else} - + {/if} {#if canEdit && file.encoding !== "base64"}
- +
{/if} @@ -139,13 +140,13 @@
{@html renderDiff(conflict.diff)}
请人工合并后,以最新内容为全文重新提交(基版将更新为 {conflict.currentVersion})
- +
{/if}
{:else} -
加载中…
+
加载中…
{/if} {#if showHistory} @@ -159,7 +160,7 @@ {/each}
- +
{/if} diff --git a/hub/filelib-web/src/lib/FilesPanel.svelte b/hub/filelib-web/src/lib/FilesPanel.svelte index 8d3b7ca..0806afd 100644 --- a/hub/filelib-web/src/lib/FilesPanel.svelte +++ b/hub/filelib-web/src/lib/FilesPanel.svelte @@ -4,6 +4,7 @@ import { selectedFilePath, filesVersion } from "./browser.js"; import type { FileEntry, NodeDetail } from "./types.js"; import Modal from "./Modal.svelte"; + import Icon from "./Icon.svelte"; let { node }: { node: NodeDetail } = $props(); @@ -12,7 +13,8 @@ let showNewFile = $state(false); let newPath = $state(""); let newContent = $state(""); - let uploadInput: HTMLInputElement; + // bind:this 的目标要用 $state,否则 svelte 5 warn 不会正确更新。 + let uploadInput = $state(null); const canEdit = $derived(node.role !== "VIEW"); @@ -84,34 +86,34 @@ } -
-
-
项目文件({files?.length ?? 0})
+
+
+
项目文件({files?.length ?? 0})
{#if canEdit} -
- - +
+ +
{/if}
{#if files === null && loadError === null} -
加载中…
+
加载中…
{:else if loadError}
{loadError}
{:else if files && files.length === 0} -
空仓库 · 可新建或上传文件
+
空仓库 · 可新建或上传文件
{:else if files} - +
{#each files as f (f.path)} selectedFilePath.set(f.path)} > - - + + {/each} @@ -121,17 +123,17 @@ {#if showNewFile} (showNewFile = false)}> -
- - +
+ +
-
- - +
+ +
- - + +
{/if} diff --git a/hub/filelib-web/src/lib/Icon.svelte b/hub/filelib-web/src/lib/Icon.svelte new file mode 100644 index 0000000..7c98aed --- /dev/null +++ b/hub/filelib-web/src/lib/Icon.svelte @@ -0,0 +1,41 @@ + + + + +
{f.path}{f.size} B{f.path}{f.size} B