Skip to content

Commit 2cc614b

Browse files
committed
Merge: HedgeBuddy UI Modernization (v0.10.0)
Phase 1 (visual + chrome refresh) + Phase 2 (behaviors + correctness) plus iterative QA-driven fixes across 1.1-1.3 and 2.1-2.3. Summary: - Sidebar shell + right-side drawers replace the legacy top toolbar - Design tokens, OS-native fonts, Lucide icons, reactive renderer pattern - Component library: CardRow, Sidebar, Drawer, IconButton, InlineStateButton, Modal, FieldRow - No-toast feedback contract via inline button states + row flashes - Inline validation, fsnotify auto-reload, case-normalized storage dir - CRITICAL: rename collision check (was silently overwriting variables) - Python lib drops Linux branch; matches Windows + macOS scope - 61 commits, ~12 affected packages See CHANGELOG.md for the full delta.
2 parents 6ef8725 + aa45b56 commit 2cc614b

86 files changed

Lines changed: 4879 additions & 1683 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,56 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
88

99
## [Unreleased]
1010

11+
## [0.10.0] - 2026-05-25
12+
13+
Major UI modernization. Old top-toolbar layout replaced with a sidebar shell + right-side drawers. The 5 legacy views (`*view.go`) have been removed and rebuilt as drawers and modals composed from a shared component library.
14+
15+
### Added
16+
17+
- **Sidebar shell** — left-aligned 200px sidebar with PROFILES section (active profile highlighted, inline `+` composer and inline rename), FILTERS section (`All / String / Path / URL / Secret` with live counts and Lucide icons), and Settings/About footer links.
18+
- **Right-side drawer overlay** — replaces the previous full-pane navigation for Edit/New/Import/Export. Esc and scrim-tap both close. Drawer content swaps without re-mounting the shell.
19+
- **Design token packages**`app/internal/ui/tokens/` defines colors, spacing, and radii as the single source of truth. The theme proxies to tokens.
20+
- **OS-native font loader** — Segoe UI on Windows (`segoeui.ttf` + `segoeuib.ttf`), SF Pro on macOS, with safe fallback to Fyne's default when files are missing. Path uses `%SystemRoot%` for portability.
21+
- **Lucide icon set** — 28 outlined SVGs vendored via `tools/icons` fetcher tool and embedded via `//go:embed`. Replaces all `theme.*Icon()` callsites in the new UI surfaces.
22+
- **Reusable component primitives** (`app/internal/ui/components/`): `Sidebar`+`SidebarItem`, `CardRow`, `Drawer`, `IconButton` (with hover-tint and tooltip), `InlineStateButton` (idle→busy→done/error state machine), `Modal`+`ShowDeleteConfirm`, `FieldRow` with inline error caption.
23+
- **No-toast feedback contract**`InlineStateButton` morphs on save/import/export; `CardRow.Flash()` tints rows on save/import/duplicate/copy; `CardRow.ConfirmCopy()` swaps copy icon to a check for 1s. Replaces transient toast notifications.
24+
- **Inline validation**`Entry.Validator` + `FieldRow.SetError` surface variable name and value errors under the field as the user types. Modal `dialog.ShowError` reduced to disk-IO last resort.
25+
- **Auto-reload via fsnotify**`vars.json` is watched; external edits refresh the list automatically.
26+
- **Variable name collision check**`SaveVariable` refuses to rename or create a variable that would clobber an existing one. Surfaces inline under the Name field via `ErrVariableExists` sentinel.
27+
- **Auto-scroll to flashed row** — duplicate/save/import scrolls the list to bring the affected row into view (uses real `Position().Y`, not estimated row height).
28+
- **Inline profile composer** — new-profile and rename happen as inline editable rows in the sidebar. Modal forms remain only for Import-as-Profile.
29+
- **Sidebar filter icons** — Lucide `type/folder/link/lock` icons next to filter labels.
30+
- **Code-block styling for variable values** — recessed `Surface1` background distinguishes value from description.
31+
- **Tooltip layer** — IconButtons embed `ttwidget.ToolTipWidget` for hover tooltips on all actionable icons.
32+
- **Browse File / Browse Folder split** — path-type variables now offer both file and folder pickers.
33+
- **Phase 2 manual QA checklist**`docs/superpowers/specs/2026-05-24-hedgebuddy-ui-modernization-qa.md`.
34+
1135
### Changed
1236

13-
- **Quills extracted to its own repository at [shakedex/quills](https://github.com/shakedex/quills).** The `service/` and `quills/` directories, the Quills release workflow, and the combined "HedgeBuddy Suite" installer have been removed. Historical entries below that describe Quills work remain for context, but Quills development continues in the new repo.
37+
- **Storage directory case-normalized to lowercase `hedgebuddy/`** on Windows and macOS. Existing `HedgeBuddy/` directories migrate automatically on first launch. Aligns Go side with Python side; eliminates case-sensitive filesystem mismatches.
38+
- **Python check + update check sequenced** — update dialog only fires after the Python check completes or dismisses, eliminating modal stacking.
39+
- **Export warning** — now mentions both `.env` and JSON formats (both write secrets in plain text).
40+
- **Delete confirm dialog** — action-specific button label (`Delete API_KEY`) and terse single-line body.
41+
- **Python/update dialogs** — two-button layout with inline "Don't remind me again" checkbox (was 3-4 stacked buttons).
42+
- **Install Update** — shows "Launching updater…" inline for 600ms before quit instead of disappearing instantly.
43+
- **Validator messages** — human language ("URLs must start with http:// or https://"; "can't find this path on this machine").
44+
- **About modal** — softened disclaimer; less shouty.
45+
- **Default and active profile** — Rename and Delete options disabled in the ⋯ menu instead of throwing errors after the click.
46+
47+
### Removed
48+
49+
- **6 legacy view files**`aboutview.go`, `formview.go`, `importview.go`, `exportview.go`, `profileview.go`, `helpers.go`. Replaced by `aboutmodal.go`, `editdrawer.go`, `importdrawer.go`, `exportdrawer.go`, `profilemodal.go`, `settingsmodal.go`, and the new components package.
50+
- **Linux branch of Python lib's `_get_base_dir()`** — raises `StorageNotFoundError` on Linux. The Go GUI doesn't ship for Linux, so the silent fallback was creating drift.
51+
- **Toast notifications and `ShowStatus`** — replaced by the inline feedback contract.
52+
- **Manual "Refresh" toolbar button** — fsnotify replaces it.
53+
54+
### Fixed
55+
56+
- **CRITICAL: rename collision data loss** — renaming variable `A` to an existing variable `B`'s name no longer silently overwrites `B`. (Pre-Phase-2 behavior would merge into `B`, losing its value.)
57+
- **Drop-handler leak across views** — drop handler scoped to drawer lifecycle.
58+
- **Confirm-delete modal text wrapping** — uses `widget.Label` with `TextWrapWord`.
59+
- **`container.NewMax`** deprecated calls replaced with `container.NewStack`.
60+
- **Manual string truncation** replaced with `widget.Label.Truncation = TextTruncateEllipsis`.
1461

1562
## [0.9.1] - 2026-04-12
1663

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.9.1
1+
0.10.0

app/FyneApp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ Website = "https://shaked.co"
44
Icon = "icon.png"
55
Name = "HedgeBuddy"
66
ID = "co.hedge.hedgebuddy"
7-
Version = "0.9.1"
7+
Version = "0.10.0"
88
Build = 7

app/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.25.0
55
require (
66
fyne.io/fyne/v2 v2.7.3
77
github.com/dweymouth/fyne-tooltip v0.4.0
8+
github.com/fsnotify/fsnotify v1.10.1
89
github.com/ncruces/zenity v0.10.14
910
github.com/tidwall/gjson v1.18.0
1011
github.com/tidwall/sjson v1.2.5
@@ -17,7 +18,6 @@ require (
1718
github.com/davecgh/go-spew v1.1.1 // indirect
1819
github.com/dchest/jsmin v1.0.0 // indirect
1920
github.com/fredbi/uri v1.1.1 // indirect
20-
github.com/fsnotify/fsnotify v1.9.0 // indirect
2121
github.com/fyne-io/gl-js v0.2.0 // indirect
2222
github.com/fyne-io/glfw-js v0.3.0 // indirect
2323
github.com/fyne-io/image v0.1.1 // indirect

app/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
1616
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
1717
github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
1818
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
19-
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
20-
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
19+
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
20+
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
2121
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
2222
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
2323
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=

app/internal/profile/profile.go

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ func GetBaseDir() (string, error) {
3030
if appData == "" {
3131
return "", fmt.Errorf("APPDATA environment variable not found")
3232
}
33-
return filepath.Join(appData, "HedgeBuddy"), nil
33+
return filepath.Join(appData, "hedgebuddy"), nil
3434
case "darwin":
3535
homeDir, err := os.UserHomeDir()
3636
if err != nil {
3737
return "", err
3838
}
39-
return filepath.Join(homeDir, "Library", "Application Support", "HedgeBuddy"), nil
39+
return filepath.Join(homeDir, "Library", "Application Support", "hedgebuddy"), nil
4040
default:
4141
return "", fmt.Errorf("unsupported platform: %s", runtime.GOOS)
4242
}
@@ -113,6 +113,57 @@ func SaveIndex(idx *ProfileIndex) error {
113113
return nil
114114
}
115115

116+
// migrateCapitalizedDir performs a one-shot rename of an existing capitalized
117+
// "HedgeBuddy" directory to the new lowercase "hedgebuddy" name. Safe to call
118+
// repeatedly — it no-ops when there's nothing to migrate.
119+
//
120+
// This addresses the historical Go (capital H) / Python (lowercase) mismatch
121+
// that breaks on case-sensitive filesystems.
122+
func migrateCapitalizedDir() error {
123+
var oldDir string
124+
switch runtime.GOOS {
125+
case "windows":
126+
oldDir = filepath.Join(os.Getenv("APPDATA"), "HedgeBuddy")
127+
case "darwin":
128+
homeDir, err := os.UserHomeDir()
129+
if err != nil {
130+
return err
131+
}
132+
oldDir = filepath.Join(homeDir, "Library", "Application Support", "HedgeBuddy")
133+
default:
134+
return nil
135+
}
136+
137+
newDir, err := GetBaseDir()
138+
if err != nil {
139+
return err
140+
}
141+
142+
// Nothing to do if paths are identical (case-insensitive FS or already lowercase).
143+
if oldDir == newDir {
144+
return nil
145+
}
146+
147+
// Skip if no old dir.
148+
oldInfo, err := os.Stat(oldDir)
149+
if os.IsNotExist(err) {
150+
return nil
151+
}
152+
if err != nil {
153+
return err
154+
}
155+
if !oldInfo.IsDir() {
156+
return nil
157+
}
158+
159+
// Skip if new dir already exists (don't clobber it).
160+
if _, err := os.Stat(newDir); err == nil {
161+
return nil
162+
}
163+
164+
return os.Rename(oldDir, newDir)
165+
}
166+
116167
// Migrate performs a one-time migration from the flat vars.json layout to
117168
// the profiles-based layout. If profiles/ already exists, it's a no-op.
118169
//
@@ -121,6 +172,12 @@ func SaveIndex(idx *ProfileIndex) error {
121172
// 2. Move existing vars.json into profiles/default/vars.json
122173
// 3. Create profiles.json with active = "default"
123174
func Migrate() error {
175+
// One-shot rename from capitalized HedgeBuddy dir to lowercase hedgebuddy.
176+
// No-op when there's nothing to migrate.
177+
if err := migrateCapitalizedDir(); err != nil {
178+
fmt.Println("Warning: migrating HedgeBuddy → hedgebuddy:", err.Error())
179+
}
180+
124181
base, err := GetBaseDir()
125182
if err != nil {
126183
return err

app/internal/ui/aboutmodal.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package ui
2+
3+
import (
4+
"net/url"
5+
6+
"fyne.io/fyne/v2"
7+
"fyne.io/fyne/v2/canvas"
8+
"fyne.io/fyne/v2/container"
9+
"fyne.io/fyne/v2/layout"
10+
"fyne.io/fyne/v2/widget"
11+
12+
"app/internal/ui/components"
13+
"app/internal/ui/icons"
14+
"app/internal/ui/tokens"
15+
)
16+
17+
// ShowAboutModal opens the About centered modal.
18+
func ShowAboutModal(c *AppController) {
19+
logo := canvas.NewImageFromResource(AppIcon())
20+
logo.FillMode = canvas.ImageFillContain
21+
logo.SetMinSize(fyne.NewSize(80, 80))
22+
23+
title := canvas.NewText(AppName, tokens.Accent)
24+
title.TextSize = 24
25+
title.TextStyle = fyne.TextStyle{Bold: true}
26+
title.Alignment = fyne.TextAlignCenter
27+
28+
version := canvas.NewText("v"+AppVersion, tokens.TextMuted)
29+
version.TextSize = 12
30+
version.Alignment = fyne.TextAlignCenter
31+
32+
author := widget.NewLabel("Created by Shaked Lipszyc")
33+
author.Alignment = fyne.TextAlignCenter
34+
35+
websiteBtn := widget.NewButtonWithIcon("shaked.co", icons.ExternalLink, func() {
36+
u, _ := url.Parse(WebsiteURL)
37+
_ = fyne.CurrentApp().OpenURL(u)
38+
})
39+
githubBtn := widget.NewButtonWithIcon("GitHub", icons.ExternalLink, func() {
40+
u, _ := url.Parse(GithubURL)
41+
_ = fyne.CurrentApp().OpenURL(u)
42+
})
43+
44+
disclaimer := widget.NewLabel("Independent project. Not affiliated with Hedge (hedge.co). MIT licensed.")
45+
disclaimer.Wrapping = fyne.TextWrapWord
46+
disclaimer.Alignment = fyne.TextAlignCenter
47+
disclaimer.Importance = widget.LowImportance
48+
49+
body := container.NewVBox(
50+
container.NewCenter(logo),
51+
title,
52+
version,
53+
widget.NewSeparator(),
54+
author,
55+
container.NewCenter(container.NewHBox(websiteBtn, githubBtn)),
56+
widget.NewSeparator(),
57+
disclaimer,
58+
)
59+
60+
closeBtn := widget.NewButton("Close", nil)
61+
d := components.ShowCustomModal(c.Window, "About", body, []fyne.CanvasObject{
62+
layout.NewSpacer(), closeBtn,
63+
})
64+
closeBtn.OnTapped = func() { d.Hide() }
65+
}

app/internal/ui/aboutview.go

Lines changed: 0 additions & 109 deletions
This file was deleted.

0 commit comments

Comments
 (0)