---
task: Fix broken layout on /add-products-to-store--h:<storeHash> — products render outside their cards, sometimes don't load, and the card boxes appear huge but empty.
cycles: 5
context: true
private: false
started: 2026-05-17T00:00:00Z
finished: 2026-05-17T00:00:00Z
---

## files
- resources/js/Pages/AddProductsToStore.vue [lines 202-393, 395-459] — page template + scoped CSS for `.product-pick-card`, `.product-thumb`, `.sticky-bottom-bar`. Product grid uses `.row g-2` with `col-12 col-sm-6 col-lg-4`. Step 1 picker is where boxes look huge/empty.
- resources/js/Components/Core/CardSimple.vue [lines 37-47, 79-97] — wrapper used 3× on this page. Has `height: 100%; display: flex; flex-direction: column;` + `.is-premium` (default true) adds white translucent background + hover `transform: translateY(-2px)`. Hover transform on the surrounding card (when product list is *inside* it) makes the whole grid jitter.
- resources/js/Components/Core/FileImage.vue [lines 1-38] — emits `<img>` directly. Page wraps it in `.product-thumb` with `:deep(img) { width: 100%; height: 100%; object-fit: cover }`. If `photourl` is `null`/missing, `p.photourl && p.photourl[0]` passes empty string → FileImage falls back. But if `photourl` is a *string* (not array), `p.photourl[0]` returns the first character — produces a 1-char src that 404s, briefly showing a blank box before `@error` swaps in the fallback. This is the "sometimes does not load" symptom.
- resources/js/Pages/BatchAddProducts.vue — sister page; reference for known-good layout patterns (cross-check spacing, container, image handling).
- ai-docs/dictionary.md — read first per repo CLAUDE.md. Update with any new findings about `FileImage` array-vs-string handling and `CardSimple` hover side-effects.

## steps
1. Read `ai-docs/dictionary.md` first (repo rule). Then open `resources/js/Pages/AddProductsToStore.vue` and reproduce mentally: the picker grid at lines 258–286 is **not** wrapped in `CardSimple`, so it sits directly inside `.tf-container`. Confirm by inspecting structure.
2. **Image robustness (root of "sometimes doesn't load" + tiny/broken thumbs):** In `AddProductsToStore.vue` lines 264 and 349, the `:src` binding is `p.photourl && p.photourl[0] ? p.photourl[0] : ''`. Replace both occurrences with a safe accessor:
   ```js
   :src="Array.isArray(p.photourl) ? (p.photourl[0] || '') : (p.photourl || '')"
   ```
   Add a small helper `const firstPhoto = (v) => Array.isArray(v) ? (v[0] || '') : (v || '');` in `<script setup>` and use `firstPhoto(p.photourl)` / `firstPhoto(r.photourl)` in the template.
3. **Thumb sizing (the "huge empty box"):** The `.product-thumb` is 56×56 and `.product-thumb-sm` is 40×40 — these are fine in isolation, but `.product-pick-card` uses `d-flex gap-3 align-items-center` with `.flex-grow-1 min-w-0` *and* a `form-check` on the right. On narrow viewports (`col-12`), if `p.name` is long and `text-truncate` is bypassed by Bootstrap's flex defaults, the row grows tall. Audit:
   - Ensure `.product-pick-card` has `min-height` set (e.g. `min-height: 84px;`) so cards have a consistent floor and don't appear "empty huge" while image loads.
   - Add `align-items: center; min-width: 0;` to the inner `.d-flex` wrapper (Bootstrap already does this, but verify).
   - Constrain the card body so the thumb cannot stretch: ensure `.product-thumb` keeps `flex-shrink: 0` (already set) and confirm it isn't being stretched by `align-items: stretch` from any parent.
4. **Row equal-height (cards "huge but empty" when description is short):** Bootstrap `.row.g-2` makes cols equal-height by default (`align-items: stretch`). Each col contains a single `.product-pick-card` that fills the column. When ONE card in a row has a long name (wraps to 2 lines) the others stretch to match — those look "empty huge". Fix by either:
   - Adding `align-self: start` to `.product-pick-card`, OR
   - Adding `.row.g-2 { align-items: flex-start; }` scoped to the page (preferred — keeps cards as tall as their own content).
5. **Products outside the box:** Inspect `.tf-container` (global class). If it provides `max-width` + `margin: 0 auto`, the picker grid should sit inside it. Confirm the grid is INSIDE the `<div class="tf-container mt-4">` block — currently it is. But the `.sticky-bottom-bar` at line 288 uses `position: sticky; bottom: 12px;` with its own background — on small screens the sticky bar can overlap product cards if the parent has `overflow: hidden`. Check `CardSimple` — `.card-custom` has `overflow: hidden` (line 42). The sticky bar is OUTSIDE CardSimple so that's fine, but verify by browsing the rendered page that no card has `overflow: hidden` cropping the checkbox/title.
6. **CardSimple hover transform causing visual "popping out":** `.is-premium:hover { transform: translateY(-2px) }` (CardSimple line 94-97) applies to the *page-level* CardSimples (target store header, search bar). When the user mouses over them they lift, which can look like products jumping out of the box. Either pass `:is-premium="false"` on the search-bar/header CardSimples on this page (they shouldn't lift), or scope the hover lift to a class that's only added when used as a clickable card.
7. **Empty state height:** Lines 254-257 — `text-center py-5 text-muted` for the empty state. Ensure `py-5` is enough that the empty state doesn't collapse into a thin strip after the search input. Likely fine; verify visually.
8. **Loading state:** Line 251-253 — `text-center py-5` for the spinner. Same — fine.
9. **Theming compliance (per CLAUDE.md):** No `bg-white`, `bg-light`, or `text-dark` in this file — confirmed clean. Do not introduce them.
10. **Sticky-top header offset:** Page does not use `sticky-top` on a child header — N/A.
11. **VueRouteMap:** Verify `AddProductsToStore` is registered in `app/Http/Controllers/Support/VueRouteMap.php` with appropriate `allowedUserTypes`. If missing (the URL `/add-products-to-store--h:…` is reachable, so likely present), no change. Otherwise add it in the same commit.
12. After edits, run `npm run build` (or `npm run dev` if Vite is hot-reloading) and visually verify in browser at `/add-products-to-store--h:<a real store hash>`. Confirm: (a) cards are uniform, hugging their content; (b) thumbnails always render (fallback kicks in when photourl is empty/string); (c) nothing overflows the page container; (d) sticky bottom bar stays inside the container.
13. Update `ai-docs/dictionary.md` with: (a) note that `FileImage` expects `string|array|null` for `src` and pages must pass via a safe accessor — never `arr[0]` directly on an unknown shape; (b) note that `CardSimple` is `is-premium` by default and lifts on hover — pass `:is-premium="false"` for non-interactive panels.

## context

### AddProductsToStore.vue — product card (Step 1 picker), lines 258-286
```html
<div v-else class="row g-2">
  <div v-for="p in filteredProducts" :key="p.hashkey" class="col-12 col-sm-6 col-lg-4">
    <div class="product-pick-card" :class="{ picked: selected[p.hashkey] }"
         @click="toggleProduct(p.hashkey)">
      <div class="d-flex gap-3 align-items-center">
        <div class="product-thumb">
          <FileImage :src="p.photourl && p.photourl[0] ? p.photourl[0] : ''"
                     class="img-fluid rounded" alt="Product"
                     fallback="https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/146710fe9ece.bin" />
        </div>
        <div class="flex-grow-1 min-w-0"> … name / category / price … </div>
        <div class="form-check"><input type="checkbox" … /></div>
      </div>
    </div>
  </div>
</div>
```

### AddProductsToStore.vue — scoped CSS, lines 395-459 (key blocks)
```css
.product-pick-card { border: 1px solid …; border-radius: 12px; padding: 12px; cursor: pointer; }
.product-thumb { width: 56px; height: 56px; flex-shrink: 0; overflow: hidden; border-radius: 8px; }
.product-thumb :deep(img) { width: 100%; height: 100%; object-fit: cover; }
.sticky-bottom-bar { position: sticky; bottom: 12px; … border-radius: 14px; z-index: 5; }
```

### FileImage.vue — relevant logic, lines 14-33
```js
const processedSrc = computed(() => {
  if (!props.src) return props.fallback;
  if (Array.isArray(props.src)) return props.src[0];   // safe array handling here
  if (props.src.startsWith("http") || props.src.startsWith("/") || props.src.startsWith("data:")) return props.src;
  if (props.src.length > 20 && !props.src.includes(".") && !props.src.includes("/")) return `/RequestData/File/${props.src}`;
  return props.src;
});
```
Key insight: `FileImage` itself already handles arrays. The bug is the *caller* unsafely doing `p.photourl[0]` — if `photourl` is a string, this yields one character.

### CardSimple.vue — hover transform, lines 80-97
```css
.is-premium { background: rgba(255,255,255,0.6); backdrop-filter: blur(12px); border: 1px solid rgba(0,0,0,0.05); box-shadow: 0 8px 32px 0 rgba(31,38,135,0.07); }
.is-premium:hover { border-color: var(--accent-color, rgba(83,61,234,0.3)); transform: translateY(-2px); }
```
`is-premium` defaults to `true`. Non-interactive panels (target store header, search bar) shouldn't lift.

## notes
- dictionary: ai-docs/dictionary.md — MUST read first and update after (repo CLAUDE.md mandate).
- linters: eslint (via npm), no phpcs, tsc, pylint relevant for this Vue file. Run `npm run build` or `npm run lint` if available.
- constraints:
  - Repo CLAUDE.md: no `bg-white`/`bg-light`/`text-dark` hardcoded. Theme via CSS vars. This file is currently compliant — keep it so.
  - Vue page edits don't require touching `routes/web.php`, `UserActions`, `UserPermissions`, or `VueRouteMap` unless registration is missing.
  - Do NOT modify legacy Blade. All work is in `.vue` files + scoped CSS.
  - Test direct URL access (reload at `/add-products-to-store--h:<hash>`) after fix.
  - Keep edits minimal — fix the layout regressions, do not refactor the component.
