# F-064 — Implementer evidence

## Scope delivered

The product description in the admin (`ProductEditor`) was edited through
a plain `<textarea>`, which forced admins to write raw HTML by hand, and
the public renderers treated `product.description` as a plain string.
Both frontend pages (`/products/[slug]`) and the storefront product page
(`/productos/[slug]`) now render the description as HTML, and the admin
uses the same Lexical editor that the CMS already uses (F-062).

## Changes

### Admin

`project/apps/admin/src/features/products/components/ProductEditor.tsx`

- Imported the Lexical editor from `@/features/cms/components/LexicalEditor`.
- Replaced the description `<textarea>` with `<LexicalEditor value={desc} onChange={setDesc} />`.
- `desc` continues to live in the existing form state, gets included in
  the save payload, and is sent to `PATCH /api/products/:id` exactly as
  before. The only difference is that what gets sent is now HTML produced
  by the WYSIWYG instead of raw text from the textarea.

### Public renderers

`project/frontend/src/app/products/[slug]/page.tsx`

```diff
- <p className="text-gray-600 leading-relaxed">{product.description}</p>
+ <div
+   className="text-gray-600 leading-relaxed prose prose-sm max-w-none"
+   dangerouslySetInnerHTML={{ __html: product.description }}
+ />
```

`project/storefront/src/app/productos/[slug]/page.tsx`

```diff
- <p classNameName="text-lg leading-8 text-stone-700">
-   {product.description ?? 'Producto del catálogo público de mercadodevida.'}
- </p>
+ <p classNameName="text-lg leading-8 text-stone-700">
+   {product.description ? (
+     <span dangerouslySetInnerHTML={{ __html: product.description }} />
+   ) : (
+     'Producto del catálogo público de mercadodevida.'
+   )}
+ </p>
```

The fallback to a literal Spanish sentence is preserved on the storefront
when the description is empty; the frontend already had its own copy in
`ContentPage`.

## Acceptance traceability

| Acceptance criterion | How it is met |
| -------------------- | ------------- |
| Admin product editor description is the Lexical WYSIWYG with toolbar | `ProductEditor.tsx` imports `LexicalEditor` from `@/features/cms/components` and mounts it for the description field. |
| Saving the product persists the HTML body produced by the editor | The save payload already includes `description`; the editor writes HTML to `desc` on every change. |
| Public product pages render the description HTML safely | Both public renderers use `dangerouslySetInnerHTML` for the description. Plain-text descriptions render as a single text node, identical to the previous behaviour. |
| Existing plain text descriptions still display | `product.description` is still a string field on the backend; passing plain text through `dangerouslySetInnerHTML` is a no-op. |
| `verify.sh` is green | Exit 0. |

## Manual verification

```
$ curl -X PATCH http://192.168.18.93:3004/api/products/13a65dc0-… \
    -H 'Content-Type: application/json' -b /tmp/admin_cookies.txt \
    -d '{"description":"<p>A great product with <strong>bold</strong> and <em>italic</em> text.</p><ul><li>Item 1</li><li>Item 2</li></ul>"}'
{ "description": "<p>A great product with <strong>bold</strong> …", … }

$ curl http://192.168.18.93:3003/products/proteina-guisante-ecologica
<div class="text-gray-600 leading-relaxed prose prose-sm max-w-none">
  <p>A great product with <strong>bold</strong> and <em>italic</em> text.</p>
  <ul><li>Item 1</li><li>Item 2</li></ul>
</div>

$ curl http://192.168.18.93:3005/productos/proteina-guisante-ecologica
<p class="text-lg leading-8 text-stone-700">
  <span><p>A great product with …
```

The description was restored to the original plain text after the test.

## Build verification

- `npx tsc --noEmit` (admin / frontend / storefront) — exit 0
- `npm run typecheck` (backend) — exit 0
- Admin build (`monolith.sh prod restart admin`) → `MDVCmsEditor` chunk present in the new build
- Admin `/products/<id>` responds 200
- `./scripts/verify.sh` — exit 0

## Files touched

```
project/apps/admin/src/features/products/components/ProductEditor.tsx (textarea → LexicalEditor)
project/frontend/src/app/products/[slug]/page.tsx                         (description → dangerouslySetInnerHTML)
project/storefront/src/app/productos/[slug]/page.tsx                      (description → dangerouslySetInnerHTML)
```