Release/v0.4.0 - #22
Merged
Merged
Conversation
The drag-over highlight outlined the whole target card, leaving it ambiguous whether the dragged card would slot in before or after it. Hovering now shows an insertion line in the gap before or after the target card (chosen by pointer position relative to the card's midpoint; vertical edges on the narrow-screen grid), and reorderModifier takes an insertion slot instead of a card index, so the drop does exactly what the marker shows.
.modifiers is the scroll container (overflow-y: auto), which clips the insertion line drawn above the first card (and the vertical markers at the left edge on narrow screens). Add padding so the edge markers fit inside the scroll area.
Adds a small button in the modifier pane header that restores the canonical pipeline order. It only appears while the current order differs from the default (orderChanged $derived), and resetting re-runs the live preview via the existing config $effect. Also fixes the drag indicator on very narrow screens: 'side by side' now compares the card width to the container instead of the grid display, so stacked (single-column) cards still get horizontal above/below markers.
Feat/flexible modifier order
Add vitest, @testing-library/svelte, happy-dom as dev deps plus test/test:watch scripts and a separate vitest.config.js (Svelte plugin, happy-dom, src/**/*.test.js). Includes a sanity test for defaultConfig to prove the toolchain runs; real coverage lands in later milestones.
config.test.js: defaultConfig shape/freshness plus sanitizeConfig coercion (null -> 0, string numbers, garbage, clamping, non-numeric passthrough, no input mutation). languages.test.js: en/de identical key sets, non-empty values, locale/saved-choice detection, t() translation and {var} interpolation, raw-key fallback, setLanguage persistence.
store.svelte.test.js: selection (toggleSelect/selectAll/clearSelection), pipeline order (canonical default, reorderModifier permutation, resetModifierOrder), showDialog (open flag, fields, info default) and api-backed flows with vi.mock of lib/api.js (loadDir happy + error, refreshPreview request payload, checkDuplicates, performRename). Fresh module per test via resetModules + dynamic import.
components.smoke.test.js renders the core components against the real module-level store: FileList rows + click-to-select, Dialog open/close + button dismiss, RenameButton check->confirm flow and blocking duplicate warning (mocked api). The file opts into jsdom (happy-dom crashes on Svelte 5 checkbox mounting), and vitest.config.js aliases bare 'svelte' to its client entry so mount() resolves under Vitest's SSR-style transform.
AGENTS.md: vitest suite overview (35 tests, four files), toolchain notes (client-entry alias, jsdom for component smoke tests), and 'local only, no CI' stance; layout line now lists vitest.config.js.
cmd_test now runs pytest (engine + API) and then 'npm run test' in frontend/ (35 vitest tests); help text updated to reflect both suites.
…ated tests/test_engine.py + test_api.py -> tests/backend/ (pytest from repo root still finds them; ./do test covers both suites). A shared tests/frontend/ folder was tried and abandoned: Vite refuses to load test files outside its project root even with root lifted to the repo root, so the vitest suite stays under frontend/src/ (vitest convention). AGENTS.md layout + test docs updated to match.
Feat/frontend tests
… workflow (3-OS PyInstaller, smoke test, GitHub Release)
…-and-release Feat/GitHub actions for development and release
Directories become first-class rename entries:
- RenameFile gains is_dir; for directories the whole name is the base
and ext is always "" (a dot in a dir name is part of the name, so
backup.tar renames to x_backup.tar). set_name keeps the entry
extension-less after a rename.
- build_files(path, names, dirs=None) marks the named subset as
directories; callers without dirs keep the historical behavior.
- preview() items gain type ("file" | "dir").
- Duplicate detection is cross-type by design (os.path.exists covers
dir->existing-file, dir->existing-dir, file->existing-dir).
- perform_rename works on directories via os.rename (content preserved);
errors are still collected per entry.
All seven modifiers work unchanged since they operate on base.
Numbering follows the combined on-screen list order across mixed
file+dir selections. +20 engine tests (126 total), all green.
…ename
- GET /api/list now returns files *and* subdirectories; every entry
carries type ("file" | "dir"), dirs report size 0. The UI filters
the two types with view toggles (default: files shown, dirs hidden),
so the default view matches the historical behavior.
- /api/preview, /api/check, /api/rename accept an optional dirs list
(names in files that are directories); omitted dirs keeps the
historical all-files behavior (backward compatible).
- Preview items report type; duplicate detection is cross-type
(dir->existing-file, dir->existing-dir, file->existing-dir all
detected, /rename refuses with 409).
+7 API tests (133 total), all green.
- New store state: showFiles (default true), showDirs (default false), treeVersion (bumped after renames so the tree can refresh labels). Toggles are view state, not modifier config (defaultConfig unchanged). - selectAll only selects visible entries; hiding a type prunes it from the selection, previews and duplicate highlights, so hidden entries can never be renamed invisibly. - preview/check/rename payloads now include dirs (the selected names that are directories), matching the new API contract. - i18n: fileList.toggleFiles/toggleDirs added; dialog copy generalized File(s)->Item(s) (de: Datei(en)->Element(e)); app subtitle updated. +8 store tests (43 frontend total), i18n key parity holds.
…fresh - FileList: Files/Directories checkboxes in the toolbar (default: files only, the historical view); rows render only visible entries; dir rows get a small type badge; header checkbox and select-all key off the visible set so hidden entries are never selected. - DirectoryTree: watch store.treeVersion and re-fetch the children of every loaded node, so renamed directories get fresh labels. - RenameButton: bump treeVersion after a successful rename. - i18n: fileList.typeDir (en "dir" / de "Ordner"). +3 smoke tests (46 frontend total).
- AGENTS.md: §1 files and directories; §2 extension-less dir rule; §3 toggles/badges/cross-type safety; §4 new behavior decisions (directory renaming ADDED, view toggles are view state not config); §5 typed /list, dirs payload, store state, refreshed test counts (116/17); milestone 10. - README.md: intro + safety paragraph mention directories and toggles. - plan.md: final execution log with commit SHAs and deviations.
A leading dot (index 0) no longer starts the extension — the os.path.splitext convention. Previously ".bashrc" split into base="" + ext=".bashrc", and since every modifier operates on the base, dot-files (hidden files on Linux/macOS) could never be renamed at all (Replace/Case were no-ops; Add suffix inserted before the name text; Remove could never strip the dot). - split_name: a dot at index 0 is not an extension delimiter; ".hidden.tar" still splits at its last real dot (base ".hidden"). - +10 tests (TestDotFiles: replace/case/add/remove/counting/ifthen/ preview/perform_rename/duplicate-check on dot-files, incl. an on-disk .bashrc -> .zshrc rename); 2 old tests updated to the new convention. - AGENTS.md: §2 split rule, §4 FIXED entry, engine test count, milestone 11.
- card <div> (has drag handlers) gets role="listitem"; the modifiers <section> container becomes role="list" (listitem needs a list parent) - the grip (role="button") gets tabindex=0 so it is keyboard-focusable Silences vite-plugin-svelte's a11y_no_static_element_interactions and a11y_interactive_supports_focus warnings; the production build is now clean. Reordering by keyboard is a possible follow-up.
Stale fixes (verified against the code): - layout tree: add build/ (packaging sources + gitignored output), pyproject.toml, requirements-build.txt, ModifierCard.svelte; drop the meaningless '(done)' marker - store action list: add reorderModifier / resetModifierOrder - frontend architecture: add a ModifierCard bullet (DnD reorder, insertion-line, role=list/listitem) - store test description: mention view toggles - milestone 8: drag-and-drop, empty states and error handling are done (ModifierCard, store.error, empty rows) — only dark mode + keyboard shortcuts remain; mark partial instead of 'next' - §3 intro: 'UI wiring lands in later milestones' is no longer true Filler removed: - the end-of-§5 blockquote repeating the §4 'preview shows the full name' decision - the 'For a full desktop run' paragraph duplicating the backend section and §1's ':8000 fallback'
Feat/folder editing
…vetica 'A') - public/favicon.svg: purple gradient folder, all corners rounded, bold white 'A' drawn from the real Helvetica Bold glyph outline (pixel-identical on every OS, no font dependency) - public/favicon-32.png + apple-touch-icon.png: rasterized compatibility variants - index.html: icon links (SVG primary, PNG fallbacks) - App.svelte: show the logo in the header next to the title
…ndows exe)
- build/make_icons.py: rasterize frontend/public/favicon.svg into
build/icons/{arenamer.png,arenamer.ico,arenamer.icns}; best effort - keeps
the committed icons when cairosvg/Pillow are unavailable
- build/icons/: the generated icon assets (committed so any OS can build)
- build/arenamer.spec: BUNDLE icon for the macOS .app, EXE icon on Windows,
build/icons bundled for the runtime window icon, version wired into
CFBundleShortVersionString; CONSOLE=False - a GUI app (console=True forces
LSBackgroundOnly=True, which hid the Dock icon entirely)
- build/smoke.spec: exe icon on Windows
- backend/main.py: pass the icon via webview.start(icon=) - pywebview 6.x
moved the icon parameter out of create_window (GTK/QT; on macOS/Windows
the window icon comes from the .app/exe icon)
- build/build.py: icon (re)generation step + --no-icons flag
Verified: .app plist references arenamer.icns (0.2.0, Retina, Dock icon),
frozen app launches cleanly, headless smoke test passes.
The subtitle sat on the vertical center of the title's tall line box, which made it float near the top of the big text. Baseline-align the title group so it sits anchored next to the bottom of the title text; drop the old h1 margin hack and pin line-height for stable metrics.
Feat/add logos and favicon
make_icons.py is best-effort by design, but its guard only caught ImportError. When cairosvg is installed yet the native libcairo is not on the dynamic linker's default search path (e.g. Homebrew cairo with python.org's Python), cairocffi raises OSError and the whole build died. - catch the native-lib case separately: find the cairo lib in the usual locations and re-exec the script once with it on DYLD_FALLBACK_LIBRARY_PATH / LD_LIBRARY_PATH (dyld is read at process start, and matches loaded libs by absolute path, so preloading cannot work); guard against re-exec loops - if neither the package nor the native lib is available, warn and keep the committed build/icons/ — the build succeeds - pin cairosvg + pillow in requirements-build.txt so fresh clones/CI can regenerate icons - CI (build.yml): install libcairo2 on the Linux job so the regeneration path actually runs there instead of silently falling back
The treeVersion $effect in DirectoryTree ran refreshLoaded() inside the effect body, so Svelte tracked every deep node read (children/loaded/ loading). Each refresh re-assigns children and toggles loading, which invalidated the effect again: an endless re-fetch loop that replaced child nodes with fresh collapsed objects. In the app the tree therefore collapsed to its first level as soon as you expanded anything (you could enter adrian via the file list, but repos never appeared); in tests the loop starved the event loop into a hang, making the tree untestable. The effect now tracks only treeVersion (guarded by lastTreeVersion) and runs the refresh in a queueMicrotask outside the effect body, so the deep reads are untracked. refreshLoaded also preserves a node object (expansion, loaded flag, subtree) for entries whose name survives a rename, so label refreshes no longer collapse the user's view. Adds DirectoryTree.test.js: expanding to depth 3 persists after settling, and a treeVersion bump refreshes labels while surviving expansions stay.
/list and /dirs called os.listdir unguarded, so a directory that exists but the app may not read (e.g. macOS TCC blocking an external volume) crashed with a raw PermissionError traceback and an opaque '500 Internal Server Error' in the UI. Both endpoints now go through _list_dir(), which translates PermissionError into a 403 naming the path and, on macOS, pointing to System Settings > Privacy & Security > Full Disk Access; other OSErrors become a 500 naming the path. The rest of the stack was already safe (date modifier falls back, duplicate check uses os.path.exists, perform_rename collects per-entry errors). Adds test_unreadable_dir_403_with_friendly_detail.
Adds a round x-in-circle button on the right of the error banner (clearError in the store, i18n app.errorDismiss in en/de). The existing behavior is kept: the banner also clears itself on the next loadDir.
fix(build): never fail the build on icon (re)generation
Fix/tree depth and permissions
- fix Windows artifact name (-win64.zip -> -windows.zip, matches build/build.py) - CONSOLE=False is unconditional in arenamer.spec (GUI app; macOS console=True would hide the Dock icon) - release.yml fires on the closed/merged PR event (fix 1089079), not a direct PR run; notes fetch-depth: 0, optional RELEASE_PAT, push master --tags - pyproject.toml is pip-installable (pip install -e . -> arenamer) - note plan.md as historical (directory renaming shipped in 0.3.0) - App.svelte error banner is dismissible; httpx2 in dev requirements
docs: align AGENTS.md and README with v0.3.0
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.
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.
feat: modern UI — design tokens, dark mode, APCA-tuned contrast
Remove dead code and hedged assertions that let implementations drift: - drop ghost "state.path" writes (no such property; guards key off currentPath) - drop the 250ms waits (the debounce lives in App.svelte's $effect, not in the store functions the tests call directly; a 0ms flush suffices) - assert the /preview payload strictly (names as strings, sanitized config) - assert duplicateNames as a plain array (the store assigns res.names) - exercise interpolation with a real key (rename.confirmMsg) instead of a fake key that only works via the unknown-key fallback
Adds the two test files identified in the suite review and expands the
store/component smoke tests:
- lib/api.test.js: response parsing + error mapping (10 tests),
including the new {path, dirs} payload contract
- modifiers smoke: all seven panels (toggle class, bindings,
conditional inputs, grey-out) as a second component layer (21 tests)
- store: goUp bounds, refreshPreview guards, checkDuplicates,
performRename happy + 409 paths, dialog resolution semantics
(13 -> 27 tests)
- components smoke: RenameButton dialog paths mount the Dialog host
alongside; ModifierCard gets a proper (raw) snippet as children and
real DataTransfer drag events
Frontend suite: 48 -> 98 tests, all passing. Svelte 5 flushes DOM
updates on a later tick, so DOM assertions follow an explicit settle.
The panels greyed out via CSS only (opacity + pointer-events: none),
which blocks the mouse but not the keyboard: users could Tab into a
"disabled" panel and edit its config, and assistive tech reported
the controls as enabled. Wrap the controls in <fieldset disabled> —
the attribute cascades a real disabled state to every input and
select — and keep the opacity as the visual cue (dropping
pointer-events).
Remove's range inputs already used real disabled attributes for their
sub-option gating and compose with the fieldset unchanged; the
sub-option split becomes a stated rule in AGENTS.md: mode-only
inputs render conditionally ({#if}), persistent-value inputs
disable via attribute.
Tests now assert the fieldset's disabled attribute (a strictly
stronger check than the old class check).
The Range checkbox shared its row with the from/to values and the until-end checkbox, which read as one crowded strip. Give the toggle its own row and let the range values (from, to, until end) group on the row below — the only row that still dims while range is off (the toggle itself no longer greys out, since it is the control that turns the section back on). DOM input order is unchanged, so the existing tests need no selector updates.
- AGENTS.md: DESIGN.md + index.css in repo layout; App.svelte line now reflects theme toggle and breadcrumb path bar; new theming bullet in frontend essentials; dark mode no longer an open item - README.md: mention the light/dark theme toggle; DESIGN.md in layout - DESIGN.md: point token reference at frontend/src/index.css (the prototype lives on a separate branch) - drop superseded plan.md (directory renaming shipped in 0.3.0)
Frontend test hardening + docs sync (rebased onto develop)
- RenameButton: move the light-theme .pill override to index.css (global). An html-attribute ancestor inside scoped component CSS trips svelte's css_unused_selector check; the rule is unchanged. - ModifierCard: the drag grip is now purely decorative (aria-hidden, no tabindex/role). Dragging is mouse-only; the accessible controls are step-head (toggle) and reset-mini (reset). Removes the a11y_click_events_have_key_events warning and a dead tab stop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
release of v0.4.0