## Version 2.3.0 (June 2026)

**Added**
- **Auto-Harmonise** -- pick any dye and Dye Studio now suggests colour
  companions for it: harmonious neighbours, the bold complement, balanced
  triads, and deliberate clashes, plus neutral grounding tones. It works
  wherever you dye:
  - **Colors tab** -- selecting a dye shows a band of harmony swatches beneath
    it, laid over a watercolour wash of that colour so you can judge each
    pairing against the real thing. Click a swatch to jump to that dye.
  - **Live mode** -- a strip of strategy buttons (Neutral, Mono, Analogous,
    Complement, Split, Triad, Clash) fills the decor's other channels around
    your focused channel in one click. A neutral main colour swaps the buttons
    to Tones / Contrast / Accent.
  - **Canvas** -- a "Harmonise" button opens a menu of the same strategies
    (with colour previews) and dresses the previewed decor.
  Every channel gets filled -- a complement on a 3-channel piece, for example,
  adds a grounding neutral so nothing is left blank.
  Under the hood, each dye is placed on a colour wheel by its perceived hue and
  brightness; Dye Studio reads off the classic relationships -- the opposite hue
  for a complement, neighbours for analogous, evenly-spaced hues for a triad, a
  deliberately unrelated hue for a clash -- and picks the nearest match from the
  palette, keeping a brightness gap so pairings read as distinct, not muddy.
  Suggestions are drawn from the 62 in-game housing dyes, so some colours have a
  deeper range than others: a sparsely-stocked hue (pink, say) has fewer shades
  to work with and leans on grounding neutrals to dress a full piece.
- Vamoose's Dye Studio now appears in the minimap's Addon Compartment drawer.
  Click the entry to toggle the window.

**Changed**
- Colours-tab dye rows are now click-to-select: the detail panel stays on the
  dye you click instead of following the cursor as you move toward it.
- The Live action bar is reorganised -- Apply is now larger and on the left
  (the primary action), then Revert and Snapshot, with the Owned-only toggle on
  the right. The "+N dyes" cost readout was removed; instead, the Apply tooltip
  now lists the exact dyes (and how many of each) a commit will consume.
- The live editor's channel rows are tighter, and the "Slot 1/2/3" labels were
  dropped -- hovering a channel already names it and its dye, so the numbers were
  redundant.

**Fixed**
- Canvas: for decor whose dye channels skip the first one, clicking a dye now
  lands on the first channel the item actually has instead of being applied to
  an unavailable channel (where it appeared to do nothing).

## Version 2.2.0 (June 2026)

**Live mode**
- The recent-dyes strip from v1.x is back: the last 12 dyes you applied in
  live mode show as clickable swatches under the action bar. Click one to
  re-apply it to the focused slot. Recents now persist across sessions
  (account-wide) instead of resetting on logout, and the strip holds 12
  (up from 10 in v1.x).
- The Cancel button is now "Revert" -- it puts every slot back to the last
  applied state and keeps you in customize mode, so the old name oversold
  what it did. It also greys out until you have pending changes, mirroring
  Apply (and unlike Apply it stays clickable when a staged dye is unowned,
  since reverting costs nothing).
- When Apply is locked because a staged dye isn't in your bags, its text now
  turns the same red as the unowned slot row, and hovering it tells you
  exactly which dye(s) you're missing -- no more wondering why the button
  won't light up. (The game would silently revert unowned dyes on commit;
  that's why the gate exists.)
- Fixed a long-standing bug where a disabled button (usually Apply) could
  render with full-brightness text after a theme repaint, making it look
  clickable when it wasn't.

## Version 2.1.7 (June 2026)

**Fixes**
- Re-clicking the active filter chip (color group / theme / palette / craft /
  herb) now clears the filter again, as intended. Previously the toggle-off
  never fired (Lua ternary trap) and the only way to clear was the "All" chip.

**Live mode action bar**
- Swapped Auto-save with the owned-dyes filter. The Live edit action bar now
  has "Owned only" (filter the dye list to dyes you've collected -- handy
  while picking dyes mid-edit), and "Auto-save dye combos on Apply" moved to
  the config sidebar as a set-and-forget preference. Both settings keep their
  saved values across the move.

## Version 2.1.6 (June 2026)

- Updated for WoW 12.0.7. Dropped 12.0.1 from the supported game versions.

## Version 2.1.5 (Released May 2026)

**Live mode Apply button**
- Apply now lights up correctly when you stage dye changes on a decor that started with empty channels. Previously the button stayed greyed out with no visible cost, even when you'd picked owned dyes for every empty slot. Root cause: the old gate relied on Blizzard's GetNumDyesToSpendOnSelectedDecor which returns 0 in the empty-fill scenario. New gate compares your current picks against the channel state when you first clicked the decor, independent of the broken API.
- Apply stays disabled when you preview a dye you don't actually own in your bags. Blizzard previews dyes regardless of ownership (the world shows the color), but CommitDyesForSelectedDecor silently reverts to the original because it can't deduct what you don't have. The new ownership check prevents the silent revert; if you're missing dyes, Apply waits until you pick them up. The slot picker also shows the count next to each dye so you can see the inventory state at a glance.
- After a successful Apply, the baseline refreshes to the just-committed state, so subsequent changes or clears re-enable Apply correctly. Previously the baseline was frozen at the LIVE_MODE_ENTER moment; "apply purple, then clear" wouldn't re-enable Apply for the clear because the slot-vs-snapshot comparison both saw "empty".

**Cost preview ("+N dyes" label)**
- The "+N dyes / -N back" label next to Auto-save now derives from your slot picks vs the original snapshot, not Blizzard's cost API. Previously the label blanked out in the empty-fill scenario; now correctly shows "+3 dyes" for a three-channel fill on an empty decor. Apply gate and cost label read from the same source so they always agree.

**Channel picker count suffix**
- Each channel slot at the bottom of the Live edit panel now shows the owned count of the assigned dye, matching the format used in the dye list above ("Arcwine x3" instead of just "Arcwine"). Picking a dye you don't own shows "--" and turns the name red, signaling the affordability issue at the slot row itself.

**Auto-save no longer creates blank snapshots**
- Hitting Apply after a "Clear all" (or right-clicking every channel) used to autosave a blank loadout in the Saved Variants sidebar -- an all-empty snapshot with no useful content. Now skips the autosave entirely when the commit is a full wipe, so the sidebar stays clean. Manual Snapshot button gets the same fix. Root cause: the empty-check used `if slot.dyeColorID` which is truthy for the no-dye sentinel value 0 (Lua 0-truthy trap).

**Internal**
- Unified the originalDyes snapshot path. Previously the LiveCustomizeObserver's SnapshotDyes and the LIVE_COMMIT_SNAPSHOT reducer each had their own loop to build the baseline, producing slightly different shapes (nil vs 0 for empty slots) that consumers had to normalize. Replaced both with a single buildOriginalDyes helper in VDS_Store.lua, used by LIVE_MODE_ENTER and LIVE_COMMIT_SNAPSHOT reducers. SnapshotDyes deleted. One canonical shape, no path divergence possible.
- Added LIVE_COMMIT_SNAPSHOT action that refreshes live.originalDyes from the just-committed slots after each successful Apply, so the Apply gate's baseline tracks each commit instead of going stale.
- live.cost selector rewired to derive spend/remove from the slot-vs-original delta instead of reading session.ui.live.cost (which was populated from Blizzard's flaky cost API). Same baseline as the Apply gate so the two stay in lockstep.

## Version 2.1.4 (Released May 2026)

**Fixes**
- Send to Auctionator no longer ships a stale herb/dye list on repeat clicks. Reported on CurseForge: the first click sent the correct list, but subsequent clicks (even after resetting targets) kept shipping the original herbs. Root cause: the Send-to-AH cart accumulated entries on every click and was never cleared. Each click now ignores any prior cart state and ships exactly the live deficit (current target minus current owned), so the Auctionator list always mirrors what the Stock tab shows. Same fix applied to Buy (Dyes) mode.
- Buy (Dyes) Send-to-AH was silently skipping any color group you had collapsed in the Stock list -- the list reflected what was visible on screen, not the full shopping list. Now ships every dye you're short on, regardless of which groups are expanded.

**Stock tab**
- The "Herbs needed to craft target" popout now shows TWO total rows at the bottom so the buy gap is visible at a glance regardless of toggle state:
  - `Crafts will use   | x4800 | 10k g` -- what the planned crafts actually consume (gross)
  - `You need to buy   | x3200 |  7k g` -- what's left after subtracting on-hand bag + bank + reagent bank + warband (net)
- New **Use owned herbs** checkbox above the Send-to-AH button (Craft mode only -- hidden in Buy mode where the dye deficit is already net). When on (default), the herb list subtracts what you already hold row-by-row, the `Buy Herbs: Xg` button shows the after-on-hand gold, and Send-to-AH ships the net amount. Toggle off to see and ship gross totals. The button label flips to `Craft from on-hand` when your stock covers every remaining craft. Hover the checkbox for details.
- Herb rows in the "Herbs needed to craft target" popout now spell out ownership in the subline: `@ 2g each - own 331 of 1360 needed`. The per-row buy gap is now visible without flipping the toggle.
- The dual footer totals now use the body font (larger / easier to read) instead of the caption font, with the cells re-sized to fit the 224px popout container.
- Send-to-AH chat print is now more verbose so you can verify what you shipped without leaving Stock: `Sent 8 herb kinds (4800 herbs total) to Auctionator list: VDS-Herbs`. Buy (Dyes) mode uses the equivalent dye phrasing.
- Clicking Buy Herbs / Buy Dyes when there's nothing to buy now prints `Nothing to buy -- all at target. Auctionator list unchanged.` Previously the button click was silent, which felt broken.
- Herbs popout subtitle simplified from `X herbs - Y kinds` to just `Y kinds` (the qty is in the footer rows now).

**Stock tab visual refresh**
- Expanded warehouse rows (Brass, Bronze, Gold, etc.) now carry the dye's swatch colour as a full-row backdrop -- same chrome the Browse panel uses for dye rows. Lets you scan the Stock list by colour the way you scan Colors.
- Group-header rows (RED, ORANGE, etc.) now sit on a neutral dark-grey backdrop so they read as a "band" without competing with the group's own RGB on the chev/name/progress bar. BLACK group label gets a luminance floor so it stays legible against the dark backdrop (would otherwise read invisible since the dye-group RGB and the row tint are identical).
- All Stock tab +/- buttons (per-dye target, group-header target, header all-groups bumper) swapped from FontString `+` / `-` to the `communities-chat-icon-plus` / `communities-chat-icon-minus` atlases. Resting: desaturated grey at 85% alpha. Hover: full colour at 100% alpha. Settled after iterating from desat/0.75 (washed out on coloured rows) -> no-desat/0.9 (taking over) -> the current midpoint.
- Action bar tightened: the "Auto-save" checkbox shrunk 92px -> 76px (fixed an over-spec warning + pulled the `+1 dye / -1 back` cost text closer to the checkbox). Added a 10px breather row between the channel picker and the action bar so the buttons read as a distinct band.
- Tooltip text rewrites: "Buy Herbs" button now reads `Send this list to Auctionator's shopping list` (was misleading "Browse the auction house"). "Use owned herbs" checkbox now reads `Subtract herbs you already own (bag, bank, warband) from the totals.` (was an essay). Apply button drops the stale `shown above` directional copy now that the cost sits beside the button rather than above it.

**Live mode**
- Right-click any slot row in the Live edit panel to clear that channel's selected dye, without leaving Live mode. Then hit **Apply** to commit the cleared state to the decor. Discord feedback: there was previously no way to unselect a channel's dye short of hitting Cancel (which exits the whole flow). Matches the pre-Lattice donor behaviour. Hovering a slot now shows a tooltip with the click hints + the currently-selected dye name. (Slot-clear sentinel: `ApplyDyeToSelectedDecor(slotID, nil)` per donor; the MCP-recorded `dyeColorID=0` is a silent no-op on 12.0.5 retail.)
- **Cancel** now reverts pending dye changes and keeps you in Live mode instead of slamming the window shut. Previously it called `CancelActiveEditing()` which killed customize mode AND closed VDS -- jarring when you just wanted to back out of one experiment. Walks the per-slot snapshot taken on enter and re-applies each original dye (or clears the slot if it was empty originally). Tooltip updated to `Revert pending dye changes.`
- **Empty-slot Cancel bug**: an originally-empty slot's snapshotted `dyeColorID` is `0` (Blizzard's sentinel for "no dye" in reads). Lua treats `0` as truthy, so Cancel was routing it through `ApplyDye(slotID, 0)` -- a verified silent no-op on 12.0.5. Now Cancel explicitly checks `~= 0` and routes empties through `ClearSlot(slotID, nil)`, so painting into a previously-undyed channel and hitting Cancel actually reverts it.
- New **Clear all** button to the right of the decor name. Wipes every slot's preview back to "no dye"; Apply commits. Decor name now left-aligned (was centred) to make room. Note: Blizzard's bulk-clear APIs are both broken on 12.0.5 -- `ClearDyesOnSelectedDecor` doesn't exist (despite a "verified" MCP entry, same trap class as `GetNumDyeSlots`), and `ClearDyesForSelectedDecor` is a silent no-op. VDS works around it by iterating `info.dyeSlots` from `GetSelectedDecorInfo` and calling `ApplyDyeToSelectedDecor(slot.ID, nil)` per slot. The three broken APIs have updated MCP gotchas to prevent future code from repeating the trap.

**Fixes (late cycle)**
- Send-to-AH no longer ships `\" Dye\"` as a search string when `C_Item.GetItemNameByID` returns `\"\"` for a not-yet-loaded item. The cache layer already filtered empty strings, but the boundary API call didn't; Lua treats `\"\"` as truthy so the downstream "pad with Dye suffix" path produced `\" Dye\"`. `resolveItemName` now treats both cache and API empties as a miss.
- `TooltipEngine.resolveDef` now logs via `VDS.Log:Warn("tooltip", ...)` when a function recipe (like `R.LiveSlot`) errors inside the `pcall`. Previously the error was silently swallowed; per CLAUDE.md no silent pcall is allowed.
- Iron Invariant cleanup: dropped defensive `widget._check or widget` fallback in `dispatchCheckbox`, `self._slotID = ... or nil` fallback in Live-slot OnClick, and `if not (live and live.originalDyes)` guard in Cancel. Each was structurally guaranteed by other invariants (build() always assigns `_check`; visibility selector gates the slot widget on `live.hasSlotN`; `originalDyes` is `{}`-initialized at boot + reset on LIVE_MODE_EXIT). Failure now surfaces loud instead of hiding schema drift.

**Tooltip engine audit**
- String tooltip recipes now render. Previously `TooltipEngine.resolveDef` returned `nil` for any non-table / non-function recipe, so the 30+ `R.X = "..."` string recipes (Snapshot, Apply, OwnedOnly, StockMinusAll, StockPlusAll, every checkbox in the config sidebar, every iconButton in the palette toolbar, etc.) hovered silent. Strings now wrap as `{ body = string }` and render as a single wrapped line.
- 4 widget kinds had spec-attached recipes that never rendered because their `build()` functions didn't read `spec.tooltip`: `searchBox` (SearchDecor, SearchDyesHerbs), `tabBar` (StockModeToggle), `scopeDropdown` (ScopeFilter), `categoryDropdown` (CategoryFilter). All four now route through a new `TooltipEngine:AttachFromSpec(widget, spec)` helper so future kinds get tooltip support in one line instead of duplicating the recipe-lookup boilerplate.

**Internal**
- Send-to-Auctionator now reads deficit selectors (`stock.buy.cart`, `stock.herbs.list`) directly instead of routing through a persisted cart projection. Removed `account.cart` (`shoppingList`, `herbCart`, `dirty`) from SavedVariables -- the cart was a redundant snapshot, persisted-but-unread, and the source of the accumulation bug above. Existing installs auto-drop the stale `account.cart` slot on first load. Aligns VDS with the HDGR Mogul tab's send-to-Auctionator pattern.
- `test_layout_compute` now exercises the Stock view (Craft mode) and asserts `Layout._overSpecWarned` is empty after every Compute. Previously the test only loaded the master `VDS_LayoutConfig.lua` and skipped the four sub-LayoutConfig modules (`_Default`, `_Canvas`, `_Live`, `_Warehouse`), so panel-internal sections were missing from the normalized tree and the engine's over-spec check never ran on them. The dual footer rows landed with a 244px width inside a 224px container and only surfaced at runtime via a chat warning; the regression net would now catch the same class of bug offline.
- `test_first_paint` + `test_controller_wire_smoke` now load `VDS_TooltipEngine` + `VDS_TooltipRecipes` before the Components / Controllers dofiles. The new `AttachFromSpec` calls in widget builds (searchBox, tabBar, dropdowns) require the engine module at file-load time; the wire-smoke also needs it for `LiveSlot`-style controller-attached tooltips.

## Version 2.1.3 (Released May 2026)

**Canvas tab reliability**
- Canvas now opens with your last-session decor list instantly, instead of "Loading..." while the housing catalog primes. The catalog is snapshotted to SavedVariables on every successful fetch and re-hydrated on /reload before the live searcher returns. First-ever Canvas open on a new install still waits for the live fetch; every session after that is instant.
- When the live catalog search fails to return data, VDS now auto-retries up to 3 times (5 seconds each) with a fresh searcher each attempt, instead of getting stuck on "Loading..." forever. Visible status during the retry: "Loading dyeable decor..." -> "Catalog stuck, retrying... (N/3)" -> "Catalog unavailable" if all attempts fail. Addresses the "stuck on Loading..." case reported on Reddit.
- After a hard "Catalog unavailable" failure, switching tabs and back into Canvas triggers a fresh retry attempt -- no need to /reload unless that also fails. Recovers transient failures (network blips, slow Blizzard backend, addon-priming races) automatically.
- Live catalog fetch is now deferred until you open VDS and view the Canvas tab (mirrors Blizzard's own Housing Dashboard, which lazy-loads its catalog when you click it). Previously VDS would kick the searcher at login for users whose saved view was Canvas -- that's the window where the housing backend is least likely to be ready on fresh chars / cold machines. Combined with snapshot rehydrate, the user-visible cost of the deferral is zero: you still see your last-session decor list the moment Canvas opens. Should make the retry / failure path much rarer in practice.

**Live Customize mode**
- New **Snapshot** button next to Cancel/Apply -- saves the currently-displayed dye scheme as a variant without applying. Useful for capturing a palette you've found on existing decor before experimenting. Dedup is automatic; identical palettes are bumped to the top of the variants list instead of duplicating.
- **Apply** button now greys out when there are no pending dye changes, instead of being clickable but a no-op.
- Footer layout tightened: "Auto-save applied" -> "Auto-save" so the action buttons (Snapshot/Cancel/Apply) fit without clipping at narrow widths.

**Config sidebar**
- New **Open Debug Log** button under the Debug Mode checkbox. Opens the same popup window as /vds log. Useful for grabbing logs for bug reports without remembering the slash command.

**Debug log window**
- Every line now has a `[HH:MM:SS]` timestamp prefix for correlating events to chat / Blizzard event timing.
- New Discord invite footer in the log popup -- one click to copy the invite URL when grabbing logs for a bug report.
- Hint text + footer message now use readable text colors (previously dim grey, easily missed).

**Internal**
- Every tooltip in VDS now lives in a central `VDS.TooltipRecipes` registry. Widget specs reference recipes by name (`tooltip = { recipe = "..." }`) instead of inline strings/tables/functions. Boot validator enforces declaration on every widget. Sets up the seam for future localization and consistent tooltip styling.
- Catalog hydration (snapshot rehydrate + live searcher Init) consolidated under a single `Hydrate()` entry point on the DecorInfoResolver. Previously the two phases ran at different lifecycle points (rehydrate at module-enable, Init at canvas-shown); now both fire from the same canvas-shown wake event in the correct snapshot-then-live order. Easier to reason about, easier to log, removes a class of "why did rehydrate fire but Init didn't" debugging puzzles.
- Herb item-name pre-warm moved out of `Init.lua` into `ItemNameResolver`'s own `onEnable`. Init.lua no longer pokes into module-specific bootstrap code; each module owns its own hydration lifecycle.

## Version 2.1.2 (Released May 2026)

**Canvas tab**
- Loadout chips now support right-click to rename and shift+right-click to delete. Previously the only way to remove a saved loadout was to enter Live Customize mode on the actual decor and use the gallery tile -- a Canvas-tab cleanup loop is now possible without leaving the tab.
- The "+" save tile pulses through a rainbow color cycle (the same spectrum used by the title bar) whenever the current channel colors are not saved as a loadout for the selected decor. It quiets the moment the combo matches a saved loadout or you save it. Tooltip switches to "Loadout not saved -- click to save" while pulsing.
- The "+" tile is now hidden when you're at the 10/10 cap for a decor instead of misleadingly inviting another save that would silently fail.
- The "+" tile is now a hollow bordered icon in keeping with the rest of the chip wall, instead of a brassy gold block.

**Fixes**
- Item names that read as "Loading..." on first paint of a cold session now resolve reliably. The async name resolver now uses Blizzard's canonical ItemEventListener primitive (the same one AllTheThings uses) which wraps the kick + listen wiring around C_Item.RequestLoadItemDataByID and ITEM_DATA_LOAD_RESULT. Replaces the previous interim fix which was listening for the wrong event. Affects herb names in Stock tab, Canvas decor picker, Warehouse rows, and anywhere else item names appear.

## Version 2.1.1 (Released May 2026)

**UX**
- ESC now closes the main window, matching the journal/collections/character panel behaviour. (Boggle, Discord)

**Fixes**
- Herb names that read as "Herb <itemID>" on first paint now resolve and re-paint when the item info arrives, instead of staying stuck for the rest of the session. The async name resolver was kicking C_Item.GetItemNameByID, which warms a name-only cache but does NOT fire GET_ITEM_INFO_RECEIVED -- so the re-paint trigger never arrived for cold items. Now also calls C_Item.RequestLoadItemDataByID in the miss path to force the full-info fetch that the event actually listens to.
- Canvas decor picker no longer renders empty on the first open of a cold session (especially noticeable on slower machines where the housing catalog populates after VDS's initial searcher pass). DecorInfoResolver now listens for HOUSING_CATALOG_CATEGORY_UPDATED / HOUSING_CATALOG_SUBCATEGORY_UPDATED and re-runs the catalog search if the prior pass returned zero. Previously the only fix was /reload. (Boggle, Discord)

## Version 2.1.0 (Released May 2026)

**Canvas tab**
- The Canvas decor picker now has a Category dropdown above the list to narrow decor by Housing category.
- A new Scope filter at the top of the Canvas tab narrows the available dyes to a curated theme or one of your custom palettes. Default is "All Dyes".
- Saved loadouts for the selected decor appear as color chips below the channel pickers in the preview panel. Click a chip to apply.
- Each row in the Canvas decor picker now shows droplet icons for the decor's dye channels, plus a count badge for how many saved loadouts you have for that decor.
- Picker lists every available dyeable decor, including decor you don't own.

**Fixes**
- Saving the same color combination twice for one decor no longer creates a duplicate entry.
- Clear button now resets all three channel pickers.
- Category dropdown no longer flashes "All Categories (0)" on first open.

## Version 2.0.0 (Released May 2026)

Complete rewrite. The colored vials look the same; everything underneath is new.

**New: Stock tab**
- Set per-color-group target counts (e.g. "3 of every red"), with per-dye
  overrides for the specific dyes you want more of.
- Deficit roll-up across bags, bank, reagent bank, and warband bank.
- One-click Restock-and-Send-to-AH: Buy mode ships missing dyes to an
  Auctionator shopping list; Craft mode ships the cheapest herbs needed to
  craft them. Auctionator is a soft dependency.
- Master +/- bumps every group target at once (shift-click for x10).
- Per-dye reset button reverts an override back to the group default.

**New defaults**
- "Replace Blizzard's dye picker" is on by default for new installs.
- Custom palette and Canvas loadout creation pre-fills "Palette N" /
  "Loadout N" with an auto-incrementing number; press Enter to accept.

**Live dye mode polish**
- Focused slot now has a gold border matching the Canvas channel picker.
- Cost preview tracks Blizzard's dye-bottle spend as you preview changes.

**Tabs:** Colors / Custom / Theme / Stock / Herbs / Canvas.

**Migration**
- Your custom palettes carry forward from v1.x.
- Other state (cart, loadouts, dyeable-item cache, minimap/panel position)
  resets to defaults on first launch.

**Slash commands:** `/vds` / `/dyes` (toggle), `/vds debug`, `/vds minimap`,
`/vds hardreset`.
