many fixes

This commit is contained in:
2026-07-03 11:18:40 +07:00
parent d909618ced
commit 79fd200e57
12 changed files with 348 additions and 18 deletions
+94 -6
View File
@@ -245,8 +245,8 @@
}
.link-btn.danger:hover {
color: var(--danger, #ff5c5c);
border-bottom-color: var(--danger, #ff5c5c);
color: var(--fail);
border-bottom-color: var(--fail);
}
.link-btn:disabled {
@@ -272,8 +272,11 @@
.pbar-fill {
display: block;
height: 100%;
width: 100%;
transform: scaleX(0);
transform-origin: left;
background: var(--accent);
transition: width 0.3s ease-out;
transition: transform 0.3s ease-out;
}
.pmeta {
@@ -473,11 +476,32 @@
box-shadow: 0 0 16px -2px rgba(255, 178, 56, 0.6);
}
.btn:disabled {
.btn:disabled,
.btn.is-disabled {
opacity: 0.35;
cursor: not-allowed;
}
/* label styled as .btn (CSV upload) can't use :disabled — block interaction. */
.btn.is-disabled {
pointer-events: none;
}
/* ---------- keyboard focus ---------- */
/* Custom-styled controls drop the faint UA outline; restore a visible, on-brand
focus ring for keyboard users (mouse clicks stay ring-free via :focus-visible). */
.btn:focus-visible,
.link-btn:focus-visible,
.file-btn:focus-within,
.topnav a:focus-visible,
.crumb:focus-visible,
table.tbl a.rowlink:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 2px;
color: var(--accent-strong);
}
.btn-row {
display: flex;
gap: 10px;
@@ -533,14 +557,14 @@
.badge-pending {
color: var(--pending);
border-color: #4a4423;
border-color: var(--pending-dim);
background: rgba(240, 196, 25, 0.06);
}
.badge-pending .dot { background: var(--pending); }
.badge-info {
color: var(--info);
border-color: #234456;
border-color: var(--info-dim);
background: rgba(87, 194, 255, 0.06);
}
.badge-info .dot { background: var(--info); animation: pulse 1.4s ease-in-out infinite; }
@@ -784,3 +808,67 @@ table.tbl a.rowlink:hover {
color: var(--fg-faint);
font-size: 12px;
}
/* ---------- responsive ---------- */
/* The shell is desktop-first (density is a feature for operators). These
overrides keep the chrome usable on phones/tablets without thinning the
data-dense surfaces. Intrinsic grids (panel-grid auto-fit, stat-row/page-head
flex-wrap) already reflow; the topbar and paired fields need explicit help. */
@media (max-width: 640px) {
.topbar {
flex-wrap: wrap;
height: auto;
padding: 10px 16px;
gap: 12px 18px;
}
/* Secondary chrome — drop the ambient session pill to save the row on mobile. */
.session-indicator {
display: none;
}
.topnav {
order: 3;
flex-basis: 100%;
flex: 0 0 100%;
}
.main {
padding: 20px 16px 48px;
}
.page-title {
font-size: 26px;
}
/* Stacked src/dst fields — two password inputs side by side are too tight. */
.field-row {
grid-template-columns: 1fr;
}
}
/* ---------- reduced motion ---------- */
/* Neutralize the infinite pulse dots, modal entrance, and progress transition
for users who opt out. State stays legible through color + presence, not
movement. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Coarse pointers (touch) get larger hit areas; fine pointers keep the dense
inline layout. WCAG 2.5.8 target size (AA) is 24px minimum. */
@media (pointer: coarse) {
.link-btn {
display: inline-block;
padding-block: 6px;
min-height: 24px;
}
.topnav a {
padding: 12px 16px;
}
.btn-ghost {
padding: 12px 14px;
}
}
@@ -63,6 +63,7 @@ export function FolderMappingModal({ open, srcFolders, dstFolders, initialMappin
<span className="map-arrow"></span>
<select
className="map-select"
aria-label={`Destination folder for ${src}`}
value={val}
onChange={(e) => setChoice((c) => ({ ...c, [src]: e.target.value }))}
>
+4 -2
View File
@@ -12,8 +12,8 @@
--border: #23342b;
--border-bright: #3a5443;
--fg: #dbe8de;
--fg-dim: #6f8478;
--fg-faint: #4a5c50;
--fg-dim: #7a8f82;
--fg-faint: #6f8478;
--accent: #ffb238;
--accent-strong: #ffd27a;
--accent-dim: #7a5a26;
@@ -22,7 +22,9 @@
--fail: #ff5d5d;
--fail-dim: #4a2323;
--pending: #f0c419;
--pending-dim: #4a4423;
--info: #57c2ff;
--info-dim: #234456;
--font-display: 'Big Shoulders Display', 'Arial Narrow', sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
+1 -1
View File
@@ -111,7 +111,7 @@ export function Endpoints() {
</select>
</div>
</div>
{error && <div className="error-banner">{error}</div>}
{error && <div className="error-banner" role="alert">{error}</div>}
<div className="btn-row">
<button className="btn btn-primary" disabled={busy}>
{busy ? 'Saving…' : editingId !== null ? 'Save changes' : 'Add endpoint'}
+1 -1
View File
@@ -54,7 +54,7 @@ export function Login({ onSuccess }: { onSuccess: () => void }) {
<button className="btn btn-primary" style={{ width: '100%' }} disabled={busy || !user || !pass}>
{busy ? 'Authenticating…' : 'Sign in'}
</button>
<div className="login-error">{error}</div>
<div className="login-error" role="alert">{error}</div>
</form>
</div>
)
+14 -7
View File
@@ -330,7 +330,7 @@ export function TaskDetail({ id }: { id: number }) {
<div className="panel">
<span className="panel-label">Run control</span>
<div className="stat-row">
<div className="stat-row" aria-live="polite">
<div className="stat ok">
<span className="val mono-num">{totals.copied}</span>
<span className="lbl">copied</span>
@@ -348,7 +348,7 @@ export function TaskDetail({ id }: { id: number }) {
<span className="lbl">accounts</span>
</div>
</div>
{error && <div className="error-banner">{error}</div>}
{error && <div className="error-banner" role="alert">{error}</div>}
<div className="btn-row" style={{ marginTop: 16 }}>
<button className="btn" onClick={onTest} disabled={busy !== null || accounts.length === 0}>
{busy === 'test' ? 'Testing…' : 'Test connections'}
@@ -415,10 +415,10 @@ export function TaskDetail({ id }: { id: number }) {
<div className="divider-label">or bulk import</div>
<div className="upload-row">
<button className="btn file-btn" disabled={busy !== null}>
<label className={`btn file-btn${busy !== null ? ' is-disabled' : ''}`}>
{busy === 'import' ? 'Importing…' : 'Upload CSV'}
<input ref={fileInputRef} type="file" accept=".csv,text/csv" onChange={onFileChosen} disabled={busy !== null} />
</button>
</label>
<button type="button" className="link-btn" onClick={downloadExampleCSV}>
download example.csv
</button>
@@ -432,7 +432,7 @@ export function TaskDetail({ id }: { id: number }) {
clear log
</button>
)}
<div className="log-pane">
<div className="log-pane" role="log" aria-live="polite" aria-relevant="additions" aria-label="Event log">
{log.length === 0 ? (
<div className="log-empty">awaiting events over websocket</div>
) : (
@@ -501,8 +501,15 @@ export function TaskDetail({ id }: { id: number }) {
const scanning = lv.scanned != null && lv.scanTotal != null && lv.scanned < lv.scanTotal
return (
<div className="acct-progress">
<div className="pbar">
<span className="pbar-fill" style={{ width: `${pct}%` }} />
<div
className="pbar"
role="progressbar"
aria-valuenow={pct}
aria-valuemin={0}
aria-valuemax={100}
aria-label={`Copy progress${lv.folder ? `: ${lv.folder}` : ''}`}
>
<span className="pbar-fill" style={{ transform: `scaleX(${pct / 100})` }} />
</div>
<span className="pmeta mono-num">
{done}/{lv.total} ({pct}%) · {lv.speed >= 1 ? Math.round(lv.speed) : lv.speed.toFixed(1)}/s · ETA {fmtDuration(eta)}