# media-portal

Vue 3 microfrontend, published as `@feedmepos/mf-media` and mounted into the portal shell
(`mf-portal-root`) via import map at `/media`. Two tabs:

| Route | View | What it is |
|---|---|---|
| `/library` | `views/AssetLibrary.vue` | the asset centre — upload, tag, search, delete |
| `/studio` | `views/AiStudio.vue` | the AI image studio — compose from a prompt |

The tab bar lives in `App.vue` because it is this module's own chrome, and `App.vue` is what
`app.ts` exports as `FmApp` for the shell to mount. The active tab is derived from the route,
so a deep link, the back button and a click cannot disagree.

## Running it

```bash
pnpm dev          # standalone dev server on :5190
pnpm type-check
pnpm test         # node --test over test/*.test.ts
pnpm build:mf     # the published artifact
```

Standalone dev still requires a real portal sign-in: the studio reads
`coreStore.currentBusiness`, so without a session there is no businessId and nothing can be
generated. `src/main.ts` points at the local Go backend per `.env.development`.

To exercise the studio you need the backend too, and with no `GEMINI_API_KEY` it uses the
Stub, which renders proportioned placeholders for free:

```bash
cd ../media-backend && GOTOOLCHAIN=go1.23.9 go run .
```

## What verification here actually covers

Three gaps are worth knowing, because each one silently passes work that is wrong:

- **`pnpm lint` does not run.** There is no ESLint config anywhere in this repo, so ESLint 8
  parses every file as ES5 and fails on the first `import` — about 20 errors on a clean
  checkout, in files no feature touches. Not a gate; fixing it is separate work.
- **`pnpm type-check` does not check `Fm*` component props.** `<FmButton size="not-a-real-size">`
  passes clean. Read the component's own `.d.ts` under
  `node_modules/@feedmepos/ui-library/dist/components/` before using a prop. Things a guess
  gets wrong: `FmButton.size` is `'md' | 'lg'` with no `'sm'`, its `icon` prop is deprecated
  in favour of `prependIcon`, `FmCircularProgress.size` starts at `'md'`, and `FmChip`
  selection is `selected` + `selectable`.
- **`pnpm type-check` does not check anything from `@feedmepos/media-picker` either.** That
  package's `exports.types` points at a `dist/index.d.ts` its build never emits, so the whole
  module resolves as `any`. And because the portal resolves it through the built `dist` rather
  than source, **any change to media-picker needs `pnpm picker:build` from the repo root**
  before the dev server or a build can see it.

So `pnpm build:mf` and the browser are the only real checks on a template. Note that
`build:mf` only compiles *reachable* modules — a component nothing imports yet is not being
verified by it.

## Spacing numbers are px, and the scale is sparse

The design system's Tailwind plugin sets `theme.extend.spacing` to a **px-valued** scale:

```
0 4 8 12 16 24 32 40 48 56 64 72 80 88 96 104 112 120
```

It arrives through `extend`, not as a replacement, so a number **not** on that list silently
falls through to Tailwind's own **rem** scale and lands about four times too large. `gap-20` is
not 20px, it is 80px. `h-36` is 144px. `w-44` is 176px.

This cost real time: an 80px-padded 320px panel left ~160px of content, split into two columns,
which wrapped every label to one character per line. Nothing warns — the classes are real
Tailwind, just not the values the design intends. When translating a mock's pixel values, check
each number against the list above.

Arbitrary values (`w-[320px]`, `bg-[--fm-color-brand-primary,#FF7823]`) **do** work and are the
established idiom here — see `AssetGrid.vue`. Use them for anything off-scale.

## Studio state survives a tab switch

The batches live in `stores/generation.ts`, not in `AiStudio.vue`'s setup, so leaving for the
asset centre and coming back does not throw away the run. The poll loop is deliberately **not**
stopped on unmount: a batch generating while the merchant looks at the library should be
finished when they return, not frozen where they left it.

Read it with `storeToRefs`, never plain destructuring — a Pinia store is a reactive proxy, and
pulling `batches` straight out hands the template a snapshot that never updates.

## Layout

```
src/
  App.vue                        the tab bar over <RouterView>
  router/shared.ts               both routes, each permission-wrapped
  views/AssetLibrary.vue         asset centre
  views/AiStudio.vue             studio page — layout only, no logic
  composables/
    generationState.ts           pure state transitions (tested)
    useGeneration.ts             reactive shell: refs, poll timer, network
  components/studio/             the studio's five components
  api/generation.ts              the three studio endpoints
  locales/*.json                 7 locales, identical key sets
test/
  generationState.test.ts        node --test, no vitest
```

`generationState.ts` is split from `useGeneration.ts` so the transitions with edge cases can
be tested without Vue — the same division `packages/media-picker` draws between `variants.ts`
and `useAssetVariants.ts`. The `saved`-preserving merge is the reason it earns its keep: a
poll landing a second after a save would otherwise flip the button back to "save", which
nobody would reproduce by clicking around.
