VDS Canvas Tab — Chip-Filtered Decor Picker (Design A, post real-data check)

Real category counts: Furnishings 85 / Accents 32 / Structural 17 / Lighting 7 / Nature 2 (143 total, 5 categories). Master-detail was overkill for only 5 categories -- switched to horizontal chip strip + full-width flat list. Loadouts move to a chip-wall footer in the preview panel.

Full window (Canvas view, ~1080 × 540)

Vamoose's Dye Studio ? ×
Dyeable Decor
Category:
Furnishings (85)
Stitched Pillow Roll 1
Sturdy Wooden Bed 7
Sturdy Wooden Bench 16
Sturdy Wooden Chair
Sturdy Wooden Shelf 3
Sturdy Wooden Table
Sturdy Wooden Trellis 2
Tall Wooden Shelf
Naturally Elegant Doormat
Carved Elven Bookcase 4
Carved Wooden Bar Table
Elegant Curved Table 20
Circular Elven Table
... 72 more in Furnishings
Browse Dyes
Colors
Custom
Theme
Stock
Herbs
Canvas
Scope:
Alliance
Nazjatar Navy
Alliance Blue x15
Midnight Blue
Zephras Blue
Bone-White
Highborne Marble x3
Highland Birch x1
Kul Tiran Steel x1
Zandalari Gold x1
Gold x1
Preview: Sturdy Wooden Bench
[3D model preview]
Lush Green
Arcwine
Warm Teak

What changed vs current

Removed

Added

Same

Data dependencies

What we need Source Status
decor.categoryIDs[] + decor.subcategoryIDs[] C_HousingCatalog.GetCatalogEntryInfo(entryID) Returned but not captured. Add to VDS_DecorInfoResolver.lua:66 dispatched item shape.
Category name lookup C_HousingCatalog.GetCatalogCategoryInfo(catID).name One call per category at observer init; cache in session.catalog.categoryNames.
Decor → category map Already implicit in catalog Selector projects byCategory[catID] = { decorIDs... }.

State shape changes

session.ui.byView.canvas = {
    expandedDecors = {...},        -- (unused after migration; remove)
    scope = nil,                   -- existing (canvas scope dropdown)
    selectedCategory = nil,        -- NEW. nil = "All". Otherwise categoryID.
    -- selectedDecorID stays at session.ui.canvas.decorID (top-level, unchanged)
}

Selectors needed

SelectorReadsReturns
canvas.master.categories catalog.dyeableDecor, byView.canvas.selectedCategory Array of { id, name, count, selected } for master list rows. "All" row at index 1.
canvas.detail.decor catalog.dyeableDecor, byView.canvas.selectedCategory, ui.canvas.pickerSearch, ui.canvas.decorID Filtered + sorted decor rows for the selected category. Search overrides category to "All Matching".
canvas.loadouts.forSelected account.canvas.loadouts, ui.canvas.decorID Loadout footer rows. Empty array when nothing selected.

Actions

ActionPayloadInvalidates
CANVAS_SET_CATEGORY { value = catID | nil } session.ui.byView.canvas.selectedCategory

Existing CANVAS_SET_DECOR handles detail-pane row click. No change to that action.

Open design questions

1. Search behavior — filter within category, or override to "All Matching"?

When user types in search, options:

(b) matches search intent ("find a thing"). User can clear search to return to category-filtered view.

2. Master row icons — text-only or icon+label?

Text-only at 110px works for "Furniture / Lights / Rugs" but feels stark. Adding category icons (~14×14) would polish but eat 18px horizontal — master pane needs to grow to ~130px which crowds the detail pane.

Recommended: text-only for v1, add icons in v2 polish pass if user requests.

3. Owned-only filter behavior on master counts

"Show owned only" config option exists. Should master count badges reflect owned count or total count?

4. Loadouts chip wall — interaction details

Resolved: chip-wall grid (4×5, all 20 visible at once). Open sub-questions:

5. Empty states

Categories with zero decor — hide the row entirely, or show greyed-out with "0" count? Recommend hide entirely — only categories that have at least one dyeable decor appear in master.

Build sequence (rough)

  1. Capture category data in VDS_DecorInfoResolver.lua — add categoryIDs/subcategoryIDs/categoryNames to dispatched item shape.
  2. State + action + meta: selectedCategory slot + CANVAS_SET_CATEGORY + invalidates declaration.
  3. Selectors: canvas.master.categories, canvas.detail.decor, canvas.loadouts.forSelected.
  4. Layout shuffle: split canvasPicker.body into masterPane + detailPane sections; new row factories for master + detail rows.
  5. Add loadouts footer panel to canvasPreview.body (between channels and action-bar) with its own row factory.
  6. Wire controllers: master row OnClick dispatches CATEGORY_SET; detail row OnClick dispatches CANVAS_SET_DECOR (existing); loadout row OnClick dispatches CANVAS_APPLY_LOADOUT (existing).
  7. Drop the tree: remove scrollBoxTreeList widget + canvas.tree.nodes selector + expandedDecors state.