many fixes
This commit is contained in:
+94
-6
@@ -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
@@ -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;
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user