From adb7108002acdc4d7534c2ce11cf7da11b5ef3ff Mon Sep 17 00:00:00 2001 From: test Date: Sat, 29 Aug 2026 23:44:57 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20modern=20UI=20=E2=80=94=20design=20?= =?UTF-8?q?tokens,=20dark=20mode,=20APCA-tuned=20contrast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the approved prototype design into the real app: - Token system in index.css (dark + light): 4-level surfaces, 3-level borders, 3-level text, radius/scale tokens, named shadow levels, scrim - Dark/light theme switcher (topbar) + pre-paint script in index.html (no FOUC), persisted in localStorage - Breadcrumb path bar replaces Home/Up (crumb click navigates, current crumb bold); pill language select, icon theme button - Modifier cards: numbered steps (01–07), enable dot, one-click reset-mini, drag reorder preserved; modifiers reduced to controls only - File list: independent Files/Directories chips, mono name + muted new name, success-green changed names with dot, danger-tinted duplicate rows, sticky thead, dir badge - Dialog + RenameButton (pill count) rebuilt on the system Contrast: every pair in both themes reaches APCA Lc ≥75 at its used size except the dark --faint metadata tier (APCA 36, WCAG 4.4) — documented in DESIGN.md (raising it to 75 would flatten it into --muted and kill the 3-tier hierarchy). All 48 frontend + 145 backend tests pass; DESIGN.md (Google Labs format) committed as the system anchor. --- .gitignore | 2 + DESIGN.md | 242 ++++++++++++ frontend/index.html | 8 +- frontend/src/App.svelte | 345 ++++++++++++++---- frontend/src/components/Dialog.svelte | 44 ++- frontend/src/components/DirectoryTree.svelte | 14 +- frontend/src/components/FileList.svelte | 159 +++++--- frontend/src/components/ModifierCard.svelte | 185 +++++++++- frontend/src/components/RenameButton.svelte | 48 ++- frontend/src/components/TreeNode.svelte | 23 +- .../components/modifiers/AddModifier.svelte | 58 +-- .../components/modifiers/CaseModifier.svelte | 60 +-- .../modifiers/CountingModifier.svelte | 68 +--- .../components/modifiers/DateModifier.svelte | 105 ++---- .../modifiers/IfThenModifier.svelte | 91 ++--- .../modifiers/RemoveModifier.svelte | 66 ++-- .../modifiers/ReplaceModifier.svelte | 49 +-- frontend/src/index.css | 182 ++++++++- frontend/src/lib/i18n/de.js | 8 +- frontend/src/lib/i18n/en.js | 8 +- 20 files changed, 1237 insertions(+), 528 deletions(-) create mode 100644 DESIGN.md diff --git a/.gitignore b/.gitignore index 710640e..5156498 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ build/smoke/ .DS_Store .idea/ .vscode/ +outputs/ +graphify-out/ diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..b82860b --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,242 @@ +# A-Renamer — Design System + +Dark-first desktop web utility (pywebview / WKWebView, mouse + keyboard). Batch file & +directory renamer: directory tree · file list with live previews · 7-step modifier +pipeline. Accent family is derived from the product logo (`frontend/public/favicon.svg`, +purple folder gradient `#a855f7 → #6d28d9`). + +Reference implementation of these tokens: `frontend/prototype/index.html`. + +```yaml +name: A-Renamer +description: > + Dark-first desktop renaming utility. Layered dark surfaces, one logo-purple accent, + mono filenames, dense 13px working grid, numbered modifier pipeline. +colors: + bg: "#0e0f12" # light: #f2f3f6 + surface: "#16181d" # light: #ffffff + surface-2: "#1d2026" # light: #f6f7f9 + surface-3: "#242830" # light: #eceef2 + border: "#262b33" # light: #e4e7ee + border-strong: "#333945" # light: #d3d8e2 + primary: "#8b36e8" # filled button bg; light: #7c3aed + primary-hover: "#6d28d9" # both themes + primary-contrast: "#ffffff" # both themes + accent: "#a855f7" # light: #7c3aed + accent-deep: "#8b36e8" # light: #6d28d9 + accent-bright: "#e9d5ff" # light: #6523d6 + accent-contrast: "#0e0f12" # light: #ffffff + accent-soft: "#a855f724" # 14% alpha; light: #7c3aed17 (9%) + sel: "#a855f71a" # 10% alpha; light: #7c3aed0f (6%) + row-hover: "#ffffff09" # 3.5% alpha; light: #14182809 + success: "#70e4a1" # light: #157a49 + danger: "#ffc1b8" # light: #b42c21 + text: "#edeff2" # light: #181b21 + muted: "#d0d6df" # light: #545e6e + faint: "#7c8694" # light: #4e5767 +typography: + font-family: [system-ui, -apple-system, Segoe UI, Roboto, sans-serif] + mono: [ui-monospace, SF Mono, Menlo, monospace] + text-scale: [10.5, 11, 11.5, 12, 12.5, 13, 14] # dense utility grid (not 1.25 modular) +rounded: + sm: 8px + md: 10px + lg: 14px + pill: 99px +spacing: + unit: 4px # grid 4/8/12/16/20; 2px subgrid for tight control padding +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.accent-contrast}" + rounded: "{rounded.sm}" + padding: 7px 14px + button-ghost: + backgroundColor: "{colors.surface}" + textColor: "{colors.muted}" + rounded: "{rounded.sm}" + padding: 6px 12px + chip-on: + backgroundColor: "{colors.accent-soft}" + textColor: "{colors.accent-bright}" + rounded: "{rounded.sm}" + input: + backgroundColor: "{colors.bg}" + textColor: "{colors.text}" + rounded: "{rounded.sm}" + pane: + backgroundColor: "{colors.surface}" + textColor: "{colors.text}" + rounded: "{rounded.lg}" + step-card: + backgroundColor: "{colors.surface-2}" + textColor: "{colors.text}" + rounded: "{rounded.md}" + step-number-chip: + backgroundColor: "{colors.surface-3}" + textColor: "{colors.muted}" + rounded: "{rounded.pill}" + step-number-chip-active: + backgroundColor: "{colors.primary}" + textColor: "{colors.primary-contrast}" + rounded: "{rounded.pill}" + badge: + backgroundColor: "{colors.accent-soft}" + textColor: "{colors.accent-bright}" + rounded: "{rounded.pill}" + hint: + textColor: "{colors.faint}" + warning: + textColor: "{colors.danger}" + changed-name: + textColor: "{colors.success}" + row-hover: + backgroundColor: "{colors.row-hover}" + row-selected: + backgroundColor: "{colors.sel}" +``` + +## Colors + +Two themes, same token names. All values live in `:root` / `[data-theme]` blocks; +components use `var(--token)` only. + +| Token | Dark | Light | Role | +|---|---|---|---| +| `--bg` | `#0e0f12` | `#f2f3f6` | page background | +| `--surface` | `#16181d` | `#ffffff` | panes, cards | +| `--surface-2` | `#1d2026` | `#f6f7f9` | inactive step cards, segment wells | +| `--surface-3` | `#242830` | `#eceef2` | step number chips (off) | +| `--border` | `#262b33` | `#e4e7ee` | hairlines, card borders | +| `--border-strong` | `#333945` | `#d3d8e2` | inactive status dots | +| `--text` / `--muted` / `--faint` | see frontmatter | see frontmatter | 3 text tiers | +| `--primary` / `--primary-hover` | `#8b36e8` / `#6d28d9` | `#7c3aed` / `#6d28d9` | filled primary button + hover (white text, APCA 82/89) | +| `--accent` / `--accent-deep` | `#a855f7` / `#8b36e8` | `#7c3aed` / `#6d28d9` | logo gradient stops | +| `--accent-bright` | `#e9d5ff` | `#6523d6` | accent text on soft fills (APCA ≥75 in both themes) | +| `--accent-soft` / `--sel` | 14% / 10% | 9% / 6% | tints, selection | +| `--success` | `#70e4a1` | `#157a49` | changed-name highlight | +| `--danger` | `#ffc1b8` | `#b42c21` | errors, warnings | + +**Contrast (measured, gate: WCAG AA 4.5 hard floor, APCA 75 target for primary text):** + +| Pair | Dark | Light | +|---|---|---| +| body (`--text` on `--surface`) | WCAG 15.4 · APCA 96 ✓ | WCAG 17.3 · APCA 104 ✓ | +| `--muted` on `--surface` | WCAG 12.1 · APCA 80 ✓ | WCAG 6.6 · APCA 82.5 ✓ | +| `--faint` on `--surface` | WCAG 4.8 · APCA 36.1* | WCAG 7.3 · APCA 85 ✓ (worst fill `--surface-3`: 75.1) | +| crumb (`--muted` on `--bg`) | WCAG 13.1 · APCA 81 ✓ | WCAG 5.9 · APCA 75 ✓ | +| primary button (white on `--primary`) | WCAG 5.5 · APCA 81.9 ✓ | WCAG 5.7 · APCA 82.8 ✓ | +| primary hover (white on `--primary-hover`) | WCAG 7.1 · APCA 88.3 ✓ | WCAG 7.3 · APCA 89.4 ✓ | +| changed name (`--success`) | WCAG 11.2 · APCA 75.8 ✓ | WCAG 5.4 · APCA 76.3 ✓ | +| badge (`--accent-bright` on soft) | WCAG 10.3 · APCA 81.7 ✓ | WCAG 6.6 · APCA 75.1 ✓ | +| `--danger` | WCAG 11.5 · APCA 76.9 ✓ | WCAG 6.3 · APCA 80.0 ✓ | + +\* One documented deviation remains: dark `--faint` (APCA 36.1, WCAG 4.8 ✓). +It is the metadata tier (table headers, step numbers, 10.5–11.5px labels, +placeholders, hints) and only reads on `--surface`/`--surface-2`. Raising it to +APCA 75 would flatten it into `--muted` (APCA 80) and kill the 3-tier hierarchy — +the known APCA dark-mode tradeoff. (Duplicate-row *hover* is transient: danger +text on the 12% danger tint reaches APCA 67–73, WCAG ≥4.5 — accepted.) Every +other pair in both themes passes APCA 75 at the sizes where it is used. + +**Known ceiling:** `data:` URI glyphs (e.g. the ` setLanguage(e.target.value)}> - {#each languages as l (l.code)} - - {/each} - - +
+ + +
-
- - - e.key === "Enter" && commitPath()} /> - + {#if appState.error} -
+

{t("modifiers.dragHint")}

@@ -147,70 +205,229 @@ + \ No newline at end of file diff --git a/frontend/src/components/Dialog.svelte b/frontend/src/components/Dialog.svelte index 4dc326f..d4c4dc0 100644 --- a/frontend/src/components/Dialog.svelte +++ b/frontend/src/components/Dialog.svelte @@ -43,28 +43,44 @@ .backdrop { position: fixed; inset: 0; - background: rgba(15, 23, 42, 0.45); + background: var(--scrim); display: flex; align-items: center; justify-content: center; z-index: 100; } .box { - background: #fff; - border-radius: 10px; + background: var(--surface); + border: 1px solid var(--border-strong); + border-radius: var(--r-lg); padding: 20px 24px; min-width: 320px; max-width: 480px; - box-shadow: 0 20px 50px rgba(0, 0, 0, 0.25); - font-family: system-ui, -apple-system, "Segoe UI", sans-serif; + box-shadow: var(--shadow); } - h2 { margin: 0 0 8px; font-size: 1.1rem; color: #1f2430; display: flex; align-items: center; gap: 8px; } - h2.warning { color: #b45309; } - .icon { font-size: 1.2rem; } - .msg { margin: 0 0 18px; color: #4b5563; font-size: 0.9rem; white-space: pre-line; } + h2 { margin: 0 0 8px; font-size: 15px; font-weight: 650; color: var(--text); display: flex; align-items: center; gap: 8px; } + h2.warning { color: var(--danger); } + .icon { font-size: 15px; } + .msg { margin: 0 0 18px; color: var(--muted); font-size: 13px; white-space: pre-line; } .buttons { display: flex; justify-content: flex-end; gap: 8px; } - button { padding: 7px 16px; border: 1px solid #d1d5db; background: #fff; border-radius: 6px; cursor: pointer; font-size: 0.9rem; } - button:hover { background: #f3f4f6; } - button.primary { background: #2563eb; border-color: #2563eb; color: #fff; } - button.primary:hover { background: #1d4ed8; } - + button { + padding: 7px 16px; + border: 1px solid var(--border); + background: var(--surface-2); + color: var(--text); + border-radius: var(--r-md); + cursor: pointer; + font-size: 13px; + transition: background 0.12s ease, border-color 0.12s ease; + } + button:hover { background: var(--surface-3); border-color: var(--border-strong); } + button:active { transform: translateY(1px); } + button.primary { + background: var(--primary); + border-color: var(--primary); + color: var(--primary-contrast); + font-weight: 600; + box-shadow: var(--shadow-btn); + } + button.primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); } + \ No newline at end of file diff --git a/frontend/src/components/DirectoryTree.svelte b/frontend/src/components/DirectoryTree.svelte index 24eca0a..cdbfd23 100644 --- a/frontend/src/components/DirectoryTree.svelte +++ b/frontend/src/components/DirectoryTree.svelte @@ -149,13 +149,17 @@
diff --git a/frontend/src/components/FileList.svelte b/frontend/src/components/FileList.svelte index ba4ed17..f61d4d5 100644 --- a/frontend/src/components/FileList.svelte +++ b/frontend/src/components/FileList.svelte @@ -27,18 +27,20 @@
- - - - - + +
+ + +
+ + {@render actions?.()}
@@ -50,6 +52,7 @@ {t("fileList.name")} + {t("fileList.newName")} @@ -66,14 +69,15 @@ {#if file.type === "dir"}{t("fileList.typeDir")}{/if} {file.name} - + → + {prev ? prev.full_new_name : file.name}{duplicate ? " ⚠" : ""} {/each} {#if visible.length === 0} - {state.busy ? t("common.loading") : t("fileList.empty")} + {state.busy ? t("common.loading") : t("fileList.empty")} {/if} @@ -82,75 +86,128 @@
+ \ No newline at end of file diff --git a/frontend/src/components/ModifierCard.svelte b/frontend/src/components/ModifierCard.svelte index db8bbad..bd71f5a 100644 --- a/frontend/src/components/ModifierCard.svelte +++ b/frontend/src/components/ModifierCard.svelte @@ -1,7 +1,12 @@ -
- - ⠿ - {@render children()} + +
(e.key === "Enter" || e.key === " ") && (e.preventDefault(), toggleEnabled())}> + + {name} + + + e.stopPropagation()} title={t("modifiers.dragHint")} aria-label={t("modifiers.dragHint")}> + + +
+
+ {@render children()} +
+ \ No newline at end of file diff --git a/frontend/src/components/RenameButton.svelte b/frontend/src/components/RenameButton.svelte index cf66fba..b5481c1 100644 --- a/frontend/src/components/RenameButton.svelte +++ b/frontend/src/components/RenameButton.svelte @@ -87,13 +87,47 @@ + button.primary { + display: inline-flex; + align-items: center; + gap: 8px; + border: 1px solid transparent; + background: var(--primary); + color: var(--primary-contrast); + font-size: 13px; + font-weight: 600; + padding: 7px 14px; + border-radius: 999px; + cursor: pointer; + box-shadow: var(--shadow-btn); + transition: background 0.12s ease, transform 0.05s ease; + } + button.primary:hover:not(:disabled) { background: var(--primary-hover); } + button.primary:active:not(:disabled) { transform: translateY(1px); } + /* Selection count. Dark: near-white pill + ink (APCA 96) — accent on + accent-contrast is only 66; light theme overrides below (77.5). */ + .pill { + display: grid; + place-items: center; + min-width: 20px; + height: 20px; + padding: 0 6px; + border-radius: 999px; + background: var(--text); + color: var(--bg); + font-size: 11px; + font-weight: 600; + font-variant-numeric: tabular-nums; + } + [data-theme="light"] .pill { + background: var(--accent-contrast); + color: var(--accent); + } + \ No newline at end of file diff --git a/frontend/src/components/TreeNode.svelte b/frontend/src/components/TreeNode.svelte index 71190ec..858505e 100644 --- a/frontend/src/components/TreeNode.svelte +++ b/frontend/src/components/TreeNode.svelte @@ -68,7 +68,8 @@ {/if} diff --git a/frontend/src/components/modifiers/AddModifier.svelte b/frontend/src/components/modifiers/AddModifier.svelte index 22d9a8a..a6f7bff 100644 --- a/frontend/src/components/modifiers/AddModifier.svelte +++ b/frontend/src/components/modifiers/AddModifier.svelte @@ -2,51 +2,23 @@ import { state } from "../../lib/state/store.svelte.js"; import { t } from "../../lib/i18n/index.svelte.js"; - // All bindings target the shared store's `add` config directly, so edits are - // reactive and immediately picked up by the live-preview effect in App.svelte. + // Bindings target the shared store's `add` config (see ReplaceModifier); + // the card frame lives in ModifierCard. -
-
-
- -
- -
-
- - + \ No newline at end of file diff --git a/frontend/src/components/modifiers/CaseModifier.svelte b/frontend/src/components/modifiers/CaseModifier.svelte index c714d0b..5a9ca71 100644 --- a/frontend/src/components/modifiers/CaseModifier.svelte +++ b/frontend/src/components/modifiers/CaseModifier.svelte @@ -2,51 +2,25 @@ import { state } from "../../lib/state/store.svelte.js"; import { t } from "../../lib/i18n/index.svelte.js"; - // All bindings target the shared store's `case` config directly, so edits are - // reactive and immediately picked up by the live-preview effect in App.svelte. + // Bindings target the shared store's `case` config (see ReplaceModifier); + // the card frame lives in ModifierCard. -
-
-
- -
-
- -
- - diff --git a/frontend/src/components/modifiers/CountingModifier.svelte b/frontend/src/components/modifiers/CountingModifier.svelte index 7f68b28..f0b6dac 100644 --- a/frontend/src/components/modifiers/CountingModifier.svelte +++ b/frontend/src/components/modifiers/CountingModifier.svelte @@ -2,55 +2,27 @@ import { state } from "../../lib/state/store.svelte.js"; import { t } from "../../lib/i18n/index.svelte.js"; - // All bindings target the shared store's `counting` config directly, so edits are - // reactive and immediately picked up by the live-preview effect in App.svelte. + // Bindings target the shared store's `counting` config (see ReplaceModifier); + // the card frame lives in ModifierCard. -
-
-
- -
-
- - - -
- {#if state.config.counting.position === "insert"} - - {/if}
-
- - + {#if state.config.counting.position === "insert"} + + {/if} + \ No newline at end of file diff --git a/frontend/src/components/modifiers/DateModifier.svelte b/frontend/src/components/modifiers/DateModifier.svelte index a7d4257..7fcf995 100644 --- a/frontend/src/components/modifiers/DateModifier.svelte +++ b/frontend/src/components/modifiers/DateModifier.svelte @@ -2,77 +2,50 @@ import { state } from "../../lib/state/store.svelte.js"; import { t } from "../../lib/i18n/index.svelte.js"; - // All bindings target the shared store's `date` config directly, so edits are - // reactive and immediately picked up by the live-preview effect in App.svelte. + // Bindings target the shared store's `date` config (see ReplaceModifier); + // the card frame lives in ModifierCard. -
-
-
- -
-
- - - -
+
+ + {#if state.config.date.source === "custom"} + + {/if} +
+
+ + {#if state.config.date.position === "insert"} + -
-
- - {#if state.config.date.source === "custom"} - - {/if} -
-
- - {#if state.config.date.position === "insert"} - - {/if} -
+ {/if}
+ .sep input[type="text"] { flex: 0 1 90px; width: auto; } + \ No newline at end of file diff --git a/frontend/src/components/modifiers/IfThenModifier.svelte b/frontend/src/components/modifiers/IfThenModifier.svelte index 255232c..bc129ab 100644 --- a/frontend/src/components/modifiers/IfThenModifier.svelte +++ b/frontend/src/components/modifiers/IfThenModifier.svelte @@ -2,68 +2,37 @@ import { state } from "../../lib/state/store.svelte.js"; import { t } from "../../lib/i18n/index.svelte.js"; - // All bindings target the shared store's `ifthen` config directly, so edits are - // reactive and immediately picked up by the live-preview effect in App.svelte. + // Bindings target the shared store's `ifthen` config (see ReplaceModifier); + // the card frame lives in ModifierCard. -
-
- -
- -
-
- {t("ifthen.ifTag")} - - -
-
- - -
-
- {t("ifthen.thenTag")} - - - {#if state.config.ifthen.action === "insert"} - - {/if} -
+
+
+ {t("ifthen.ifTag")} + + +
+
+ + +
+
+ {t("ifthen.thenTag")} + + + {#if state.config.ifthen.action === "insert"} + + {/if}
- - diff --git a/frontend/src/components/modifiers/RemoveModifier.svelte b/frontend/src/components/modifiers/RemoveModifier.svelte index 51fa4b4..ae11bd7 100644 --- a/frontend/src/components/modifiers/RemoveModifier.svelte +++ b/frontend/src/components/modifiers/RemoveModifier.svelte @@ -2,55 +2,31 @@ import { state } from "../../lib/state/store.svelte.js"; import { t } from "../../lib/i18n/index.svelte.js"; - // All bindings target the shared store's `remove` config directly, so edits are - // reactive and immediately picked up by the live-preview effect in App.svelte. + // Bindings target the shared store's `remove` config (see ReplaceModifier); + // the card frame lives in ModifierCard. -
-
-
- -
-
- - -
-
- - - - -
+ +
+
+ + + +
+ .dimmed { opacity: 0.45; } + \ No newline at end of file diff --git a/frontend/src/components/modifiers/ReplaceModifier.svelte b/frontend/src/components/modifiers/ReplaceModifier.svelte index 928841b..a0934e9 100644 --- a/frontend/src/components/modifiers/ReplaceModifier.svelte +++ b/frontend/src/components/modifiers/ReplaceModifier.svelte @@ -4,44 +4,19 @@ // All bindings target the shared store's `replace` config directly, so edits are // reactive and immediately picked up by the live-preview effect in App.svelte. + // The card frame (number, name, enable toggle, reset, drag grip) lives in + // ModifierCard — this panel only holds the modifier's controls. -
-
- -
- -
- - -
- - -
+
+ + +
+ +
- - diff --git a/frontend/src/index.css b/frontend/src/index.css index a3a1105..4016606 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,15 +1,183 @@ -/* Global reset for the full-viewport app frame (App.svelte owns the layout). */ +/* ── Design tokens (single source of truth: DESIGN.md) ──────────────────── */ +:root { + --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --mono: ui-monospace, "SF Mono", Menlo, monospace; + --r-lg: 14px; /* panes */ + --r-md: 10px; /* cards, dialogs */ + --r-sm: 8px; /* inputs, buttons */ +} +[data-theme="dark"] { + color-scheme: dark; + --bg: #0e0f12; + --surface: #16181d; + --surface-2: #1d2026; + --surface-3: #242830; + --border: #262b33; + --border-strong: #333945; + --text: #edeff2; /* APCA Lc ~97 */ + --muted: #d0d6df; /* secondary text; APCA ≥75 on every dark surface */ + --faint: #7c8694; /* metadata tier — APCA 36 (known ceiling, see DESIGN.md), WCAG 4.8:1 */ + /* Accent family = the logo gradient (#a855f7 → #6d28d9) */ + --accent: #a855f7; + --accent-deep: #8b36e8; + --accent-bright: #e9d5ff; /* logo hue, lightened: APCA ≥75 on --accent-soft fills */ + --accent-contrast: #0e0f12; + --accent-soft: rgba(168, 85, 247, 0.14); + --sel: rgba(168, 85, 247, 0.1); + /* Filled primary button: white text on accent-deep (APCA 81.9) — ink on + --accent is only 37.3, so the dark button uses the deeper stop. */ + --primary: #8b36e8; + --primary-hover: #6d28d9; + --primary-contrast: #ffffff; + --success: #70e4a1; + --danger: #ffc1b8; /* APCA ≥75 on dark surfaces */ + --row-hover: rgba(255, 255, 255, 0.035); + --shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 8px 28px rgba(0, 0, 0, 0.38); + --shadow-btn: 0 1px 2px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.06) inset; + --scrim: rgba(4, 5, 8, 0.6); +} +[data-theme="light"] { + color-scheme: light; + --bg: #f2f3f6; + --surface: #ffffff; + --surface-2: #f6f7f9; + --surface-3: #eceef2; + --border: #e4e7ee; + --border-strong: #d3d8e2; + --text: #181b21; + --muted: #545e6e; /* APCA ≥75 on --surface and --bg */ + --faint: #4e5767; /* APCA ≥75 on --surface-3 */ + --accent: #7c3aed; /* purple-700, sits between the two logo stops */ + --accent-deep: #6d28d9; + --accent-bright: #6523d6; /* APCA ≥75 on --accent-soft fills and white */ + --accent-contrast: #ffffff; + --accent-soft: rgba(124, 58, 237, 0.09); + --sel: rgba(124, 58, 237, 0.06); + --primary: #7c3aed; + --primary-hover: #6d28d9; + --primary-contrast: #ffffff; + --success: #157a49; + --danger: #b42c21; /* APCA ≥75 on white and tinted rows */ + --row-hover: rgba(20, 24, 40, 0.035); + --shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 6px 20px rgba(16, 24, 40, 0.07); + --shadow-btn: 0 1px 2px rgba(16, 24, 40, 0.12); + --scrim: rgba(23, 26, 34, 0.4); +} + +/* ── Base ────────────────────────────────────────────────────────────────── */ +*, +*::before, +*::after { + box-sizing: border-box; +} html, body { margin: 0; - padding: 0; + height: 100%; +} +body { + background: var(--bg); + color: var(--text); + font-family: var(--font); + font-size: 14px; + -webkit-font-smoothing: antialiased; +} +button { + font: inherit; + color: inherit; + cursor: pointer; } -*, *::before, *::after { - box-sizing: border-box; +/* State contract — every interactive element (see DESIGN.md). */ +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +:disabled { + opacity: 0.5; + cursor: not-allowed; } -/* Subtle backdrop so the white panes read as distinct surfaces. */ -body { - background: #eef1f5; +/* ── Shared form controls (modifier panels, path bar, dialogs) ──────────── */ +input[type="text"], +input[type="number"], +input[type="date"], +select { + background: var(--bg); + color: var(--text); + border: 1px solid var(--border); + border-radius: var(--r-sm); + padding: 6px 9px; + font: inherit; + font-size: 12.5px; + min-width: 0; +} +input[type="text"]:focus, +input[type="number"]:focus, +input[type="date"]:focus, +select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); +} +/* The soft ring above is their focus-visible treatment. */ +input[type="text"]:focus-visible, +input[type="number"]:focus-visible, +input[type="date"]:focus-visible, +select:focus-visible { + outline: none; +} +input::placeholder { + color: var(--faint); +} +input[type="checkbox"] { + accent-color: var(--accent); + margin: 0; +} +.mono { + font-family: var(--mono); +} + +/* Shared modifier-panel layout (each modifier's
). */ +.controls { + display: flex; + flex-direction: column; + gap: 8px; +} +.controls .row { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; +} +.controls label { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: var(--muted); + min-width: 0; +} +.controls input[type="text"] { + flex: 1; + min-width: 40px; +} +.controls input[type="number"] { + width: 64px; + flex: none; +} +.controls select { + flex: none; +} +.controls .check { + cursor: pointer; +} +.controls .tag { + font-size: 10px; + font-weight: 600; + color: var(--faint); + text-transform: uppercase; + letter-spacing: 0.04em; +} +.controls .pos { + flex: 0 0 auto; } diff --git a/frontend/src/lib/i18n/de.js b/frontend/src/lib/i18n/de.js index a4a321f..8cd4ff2 100644 --- a/frontend/src/lib/i18n/de.js +++ b/frontend/src/lib/i18n/de.js @@ -5,14 +5,15 @@ export const de = { // header / path bar "app.title": "A-Renamer Tool", - "app.subtitle": "Dateien und Verzeichnisse auswählen, Modifikatoren konfigurieren, neue Namen in der Vorschau prüfen und dann umbenennen.", "app.home": "Home", "app.homeTitle": "Das Home-Verzeichnis öffnen", - "app.up": "Hoch", - "app.upTitle": "Das übergeordnete Verzeichnis öffnen", + "app.theme": "Theme umschalten", "app.open": "Öffnen", "app.errorDismiss": "Fehler schließen", + // breadcrumb path bar + "crumbs.aria": "Pfad", + // language switcher (option labels stay in their own language) "lang.label": "Sprache", @@ -26,6 +27,7 @@ export const de = { "modifiers.title": "Modifikatoren", "modifiers.dragHint": "Karten per Drag & Drop umsortieren", "modifiers.resetOrder": "Reihenfolge zurücksetzen", + "modifiers.reset": "Zurücksetzen", // file list "fileList.selectAll": "Alle auswählen", diff --git a/frontend/src/lib/i18n/en.js b/frontend/src/lib/i18n/en.js index bdac349..2c0b005 100644 --- a/frontend/src/lib/i18n/en.js +++ b/frontend/src/lib/i18n/en.js @@ -4,14 +4,15 @@ export const en = { // header / path bar "app.title": "A-Renamer Tool", - "app.subtitle": "Select files and directories, configure modifiers, preview the new names, then rename.", "app.home": "Home", "app.homeTitle": "Open the home directory", - "app.up": "Up", - "app.upTitle": "Open the parent directory", + "app.theme": "Toggle theme", "app.open": "Open", "app.errorDismiss": "Dismiss error", + // breadcrumb path bar + "crumbs.aria": "Path", + // language switcher (option labels stay in their own language) "lang.label": "Language", @@ -25,6 +26,7 @@ export const en = { "modifiers.title": "Modifiers", "modifiers.dragHint": "Drag cards to change the pipeline order", "modifiers.resetOrder": "Reset order", + "modifiers.reset": "Reset", // file list "fileList.selectAll": "Select all", From b459dcf870380e6525f0865817d834a630051806 Mon Sep 17 00:00:00 2001 From: test Date: Sun, 30 Aug 2026 00:44:53 +0200 Subject: [PATCH 2/2] fix: stray ellipsis next to file-list checkboxes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 34px check cell had 12px side padding (10px content box), so the 18px checkbox overflowed it. Chrome's text-overflow: ellipsis on the cell then paints a "…" right next to the control (and clips it). Zero the horizontal padding on .col-check so nothing overflows. --- frontend/src/components/FileList.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/FileList.svelte b/frontend/src/components/FileList.svelte index f61d4d5..72b0b16 100644 --- a/frontend/src/components/FileList.svelte +++ b/frontend/src/components/FileList.svelte @@ -182,7 +182,10 @@ tr.duplicate td { background: color-mix(in srgb, var(--danger) 4%, transparent); color: var(--danger); } tr.duplicate:hover td { background: color-mix(in srgb, var(--danger) 12%, transparent); } - .col-check { width: 34px; text-align: center; } + /* No side padding: an 18px checkbox must not overflow the 34px cell — + overflowing inline content triggers a stray "…" from the cell's + text-overflow: ellipsis. */ + .col-check { width: 34px; text-align: center; padding-left: 0; padding-right: 0; } .col-arrow { width: 20px; text-align: center; color: var(--faint); } .badge { display: inline-block; margin-right: 6px; padding: 0 6px;