From d141b4623131f77084d95a358ae38264c68398bf Mon Sep 17 00:00:00 2001
From: FivelSystems <253386542+FivelSystems@users.noreply.github.com>
Date: Wed, 8 Jul 2026 03:44:37 -0300
Subject: [PATCH 1/2] chore(ui-rework): snapshot in-progress UI rework for
scheduled review
---
CHANGELOG.md | 2 +
app.go | 12 +
docs/design/search-syntax.md | 28 +-
docs/design/theming.md | 2 +-
docs/domain/frontend-architecture.md | 2 +-
frontend/src/Dashboard.tsx | 5 +-
frontend/src/components/ui/ContextMenu.tsx | 72 +++-
frontend/src/components/ui/FilterDropdown.tsx | 217 +++++++++++
frontend/src/constants.ts | 3 +-
frontend/src/context/ActionContext.tsx | 2 +
.../src/features/layout/GlobalOverlays.tsx | 7 +-
.../src/features/layout/SidebarContainer.tsx | 15 +-
frontend/src/features/library/Sidebar.tsx | 354 ++++++++----------
frontend/src/features/library/utils.ts | 4 +-
frontend/src/hooks/usePackageActions.ts | 52 ++-
frontend/src/hooks/usePackages.ts | 4 +-
frontend/src/index.css | 17 +
frontend/src/types.ts | 11 +-
frontend/src/utils/search/index.ts | 2 +-
frontend/src/utils/search/match.ts | 40 +-
frontend/src/utils/search/search.test.ts | 42 +++
frontend/src/utils/search/suggest.test.ts | 2 +-
frontend/src/utils/search/suggest.ts | 8 +-
frontend/src/utils/search/tokens.test.ts | 19 +-
frontend/src/utils/search/tokens.ts | 21 ++
frontend/src/utils/search/types.ts | 8 +-
frontend/src/wailsjs/go/main/App.d.ts | 4 +
frontend/src/wailsjs/go/main/App.js | 8 +
frontend/src/wailsjs/go/models.ts | 6 +-
pkg/database/db.go | 56 +++
pkg/database/db_test.go | 44 +++
pkg/manager/manager.go | 11 +
pkg/models/package.go | 3 +-
pkg/services/library/analysis.go | 9 +-
pkg/services/library/dependency_graph.go | 6 +-
pkg/services/library/dependency_graph_test.go | 6 +-
pkg/services/library/read.go | 13 +-
pkg/services/library/service.go | 7 +
pkg/services/library/user_metadata.go | 25 ++
39 files changed, 900 insertions(+), 249 deletions(-)
create mode 100644 frontend/src/components/ui/FilterDropdown.tsx
create mode 100644 pkg/services/library/user_metadata.go
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c68eab..44315fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,8 +13,10 @@
- **Manual Update Check**: A "Check now" button in the Updates section lets you check your selected channel on demand instead of waiting for the startup check.
- **Instant Grid on Launch**: Opening a library now shows its packages immediately from the local index while a fresh scan reconciles with disk in the background — no more waiting on an empty grid. Files deleted since the last scan drop off automatically once the scan completes.
- **Cross-Library Dependencies**: In the details panel, a dependency or "Used By" entry that lives in a *different* library is now labelled with that library's name and is clickable — clicking switches to that library and **locates & highlights** the package in the grid (your current selection and open panel stay put). Those rows are coloured by the target's enabled/disabled state, so only the library label is blue. Previously such entries were unclickable and could leave the grid empty.
+- **Ratings & Favourites**: You can now rate a package from **1 to 5 stars** and mark it as a **favourite**. Right-click any package to set its rating or toggle the heart; ratings and favourites are then searchable and filterable from the sidebar (the star row, the *Only favourites* toggle) or the search bar (`rating:4`, `rating:>=3`, `favorite:true`). A rating applies to the whole package family, so every version shares it, and your choices persist in the local database. (Desktop for now; the mobile/web entry point arrives with the upcoming card redesign.)
### Changed
+- **Redesigned Sidebar Filters**: **Status**, **Creators**, and **Categories** are now compact **dropdowns** that overlay when opened instead of long lists that pushed the panel around, each with its own built-in search and each showing the values you've picked. The freed space adds new filters: a **star rating** control, an **Only favourites** toggle, and a **dependency-relationship** group — **Standalone** (packages that declare no dependencies) and **Removable** (packages nothing depends on, so removing them breaks nothing). "Removable" reports the same whether a package is enabled or disabled, since a disabled package still exists in your library. "All Packages" became a **Clear all** action that appears only when a filter is active, and the library's total package count now sits centred beneath its name. The show/hide animation is smoother on desktop and mobile. The star rating and *Only favourites* controls now filter live (see **Ratings & Favourites** above).
- **Local Database (Foundation)**: Package and library metadata is now indexed into a local SQLite database, laying the groundwork for faster startups, persistent ratings/favourites, and upcoming features. Your `config.json` remains the source of truth for configured library paths, so the change is fully backward-compatible.
- **Dependency Version Fallback**: Clicking a dependency whose exact version isn't installed no longer dead-ends with "specific version not found". YAVAM now takes you to the newest available copy — locally or in another library — with a heads-up that the exact version wasn't found.
- **Deletion**: Improved package deletion behaviour for more reliable and predictable results.
diff --git a/app.go b/app.go
index bc42082..1249a28 100644
--- a/app.go
+++ b/app.go
@@ -387,6 +387,18 @@ func (a *App) LocateDependencies(ids []string) map[string]models.DependencyLocat
return a.manager.LocateDependencies(ids)
}
+// SetPackageRating stores a 0–5 star rating (0 clears) for a package family
+// ("Creator.Name"), version-agnostic — every version of the family shares one
+// rating. Persisted in the user_metadata table.
+func (a *App) SetPackageRating(family string, rating int) error {
+ return a.manager.SetPackageRating(family, rating)
+}
+
+// SetPackageFavorite marks a package family ("Creator.Name") as a favourite.
+func (a *App) SetPackageFavorite(family string, favorite bool) error {
+ return a.manager.SetPackageFavorite(family, favorite)
+}
+
func (a *App) ScanPackages(vamPath string) error {
if vamPath == "" || vamPath == "." {
return nil
diff --git a/docs/design/search-syntax.md b/docs/design/search-syntax.md
index 224f69d..6278ae9 100644
--- a/docs/design/search-syntax.md
+++ b/docs/design/search-syntax.md
@@ -17,13 +17,13 @@ token → AND (must match)
## Token types
```
-status:enabled status:disabled status:missing status:corrupt status:standalone
-status:hidden status:visible
+status:enabled status:disabled status:missing status:corrupt
+status:standalone status:removable status:hidden status:visible
creator:acidbubbles
type:scene
tag:dress
license:cc-by license:pc license:pc-ea
-rating:>=4
+rating:>=4 rating:<=2 rating:3
favorite:true
size:>500mb size:10mb..100mb
```
@@ -31,8 +31,15 @@ size:>500mb size:10mb..100mb
- `tag:` is available through search only; there is no dedicated tag sidebar
section.
- `size:` supports a single bound (`size:>100mb`) or a range (`size:10mb..500mb`).
-- `status:standalone` narrows to standalone packages even when the dependency
- visibility mode is `all`.
+- `rating:` supports a single bound (`rating:>=4`, `rating:<3`) or an exact
+ value (`rating:5`). The sidebar's star control emits the exact form
+ (`rating:N`) — with no way to also set an upper bound, a minimum could not
+ express "only the 3-star ones". Ratings are version-agnostic (keyed by family)
+ and stored in `user_metadata`; an unrated package counts as `rating:0`.
+- The two dependency-relationship axes are orthogonal and both backed today:
+ `status:standalone` = the package declares **no dependencies**;
+ `status:removable` = **no package depends on it**, so removing it breaks
+ nothing (enable-agnostic — a disabled package still counts as existing).
## Examples
@@ -110,10 +117,13 @@ which itself waits on the scan/validation rework that switching libraries needs.
## Not-yet-backed tokens
-`rating:`, `favorite:`, and `license:` are parsed and shown as chips but do not
-filter until the ratings/favourites data layer exists; they are inert no-ops
-until then. `status:standalone`, `status:hidden`, and `status:visible` likewise
-wait on the dependency-visibility mode.
+`license:` is parsed and shown as a chip but does not filter until license data
+is surfaced; it is an inert no-op until then. `status:hidden` and
+`status:visible` likewise wait on the dependency-visibility mode.
+`status:standalone`, `status:removable`, `rating:`, and `favorite:` are backed
+and filter live. Ratings/favourites are joined onto each package from the
+`user_metadata` store (keyed by family) on the cache-first read path; they are
+written on desktop through the package right-click menu and the sidebar controls.
## Related: dependency-visibility mode
diff --git a/docs/design/theming.md b/docs/design/theming.md
index 948d842..f1406c2 100644
--- a/docs/design/theming.md
+++ b/docs/design/theming.md
@@ -25,7 +25,7 @@ compatible.
--yavam-card-enabled --yavam-card-disabled
--yavam-card-corrupt --yavam-card-missing-deps
--yavam-card-duplicate --yavam-card-obsolete
---yavam-card-standalone /* "Standalone" in UI; internal code: isOrphan */
+--yavam-card-standalone /* root packages nothing depends on; internal code: isRemovable */
/* UI chrome */
--yavam-creator-label-bg --yavam-sidebar-icon-active
diff --git a/docs/domain/frontend-architecture.md b/docs/domain/frontend-architecture.md
index d5537d3..3cab6b8 100644
--- a/docs/domain/frontend-architecture.md
+++ b/docs/domain/frontend-architecture.md
@@ -123,7 +123,7 @@ export const Dashboard = () => {
## 6. Centralized Logic Patterns
### Package Status Authority
-Package status (Valid, Corrupt, Duplicate, Root) is complex and derived from multiple properties (`isCorrupt`, `missingDeps`, `isOrphan`).
+Package status (Valid, Corrupt, Duplicate, Root) is complex and derived from multiple properties (`isCorrupt`, `missingDeps`, `isRemovable`).
**Rule:** NEVER implement ad-hoc `if/else` checks for package status in your components (e.g., `PackageCard`, `Sidebar`).
**Solution:** Always use the centralized helper: `src/features/library/utils.ts` -> `getPackageStatus(pkg)`.
This ensures that "Duplicate", "Obsolete", and "Root" statuses are visualized consistently across the entire application (Grid, List, Dependants).
diff --git a/frontend/src/Dashboard.tsx b/frontend/src/Dashboard.tsx
index 4ee10d6..f3c7135 100644
--- a/frontend/src/Dashboard.tsx
+++ b/frontend/src/Dashboard.tsx
@@ -130,7 +130,8 @@ const DashboardContent = () => {
const {
handleBulkToggle, handleOpenFolder, setInstallModal, handleCopyPath,
- handleCopyFiles, handleCutFile, handleDeleteClick, handleInstantMerge, handleSingleResolve
+ handleCopyFiles, handleCutFile, handleDeleteClick, handleInstantMerge, handleSingleResolve,
+ setPackageRating, setPackageFavorite
} = useActionContext();
// Drag & Drop (Upload)
@@ -333,6 +334,8 @@ const DashboardContent = () => {
onMerge={(p) => handleInstantMerge(p, false)}
onMergeInPlace={(p) => handleInstantMerge(p, true)}
onResolve={handleSingleResolve}
+ onSetRating={setPackageRating}
+ onSetFavorite={setPackageFavorite}
/>
)}
diff --git a/frontend/src/components/ui/ContextMenu.tsx b/frontend/src/components/ui/ContextMenu.tsx
index b581c20..c470632 100644
--- a/frontend/src/components/ui/ContextMenu.tsx
+++ b/frontend/src/components/ui/ContextMenu.tsx
@@ -1,6 +1,7 @@
import { useRef, useEffect, useState, useLayoutEffect } from 'react';
import { VarPackage } from '../../types';
-import { Power, FolderOpen, Copy, Trash2, FileCode, Scissors, Download, Layers, Sparkles } from 'lucide-react';
+import clsx from 'clsx';
+import { Power, FolderOpen, Copy, Trash2, FileCode, Scissors, Download, Layers, Sparkles, Star, Heart } from 'lucide-react';
interface ContextMenuProps {
x: number;
@@ -18,14 +19,62 @@ interface ContextMenuProps {
onMerge: (pkg: VarPackage) => void;
onMergeInPlace: (pkg: VarPackage) => void;
onResolve: (pkg: VarPackage) => void;
+ onSetRating: (pkg: VarPackage, rating: number) => void;
+ onSetFavorite: (pkg: VarPackage, favorite: boolean) => void;
}
-const ContextMenu = ({ x, y, pkg, selectedCount = 0, onClose, onToggle, onOpenFolder, onDownload, onCopyPath, onCopyFiles, onCutFile, onDelete, onMerge, onMergeInPlace, onResolve }: ContextMenuProps) => {
+// Controlled rating row for the right-click menu: `value` is the live rating,
+// `onRate` receives the star clicked (the parent resolves click-active-to-clear).
+const MenuRatingStars = ({ value, onRate }: { value: number, onRate: (n: number) => void }) => {
+ const [hover, setHover] = useState(0);
+ const shown = hover || value;
+ return (
+