Compare commits

...
Author SHA1 Message Date
devops ca26c6ebea fix(ecommerce): boton duplicado en mobile y leftovers fucsia en pagina de producto
Reportado en produccion: en mobile aparecian dos botones 'Añadir al
carrito' apilados. ProductPurchase.tsx nunca ocultaba su boton en
mobile, y MobileAddToCart.tsx (barra fija abajo, solo lg:hidden) ya
cubre esa misma accion ahi -- se agrega hidden lg:inline-flex al
primero para que solo uno este visible por breakpoint.

De paso, dos leftovers del esquema arcoiris original en la misma
pagina, vistos en la misma sesion de pruebas: el badge de variante
seleccionada (border-fuchsia-500 bg-fuchsia-50 text-fuchsia-700 -> azul
de marca) y la etiqueta de categoria/coleccion (text-fuchsia-600 ->
ari-coral).
2026-08-28 14:14:23 +00:00
devops 12d3c692d3 Merge fix/adr0008-logo-circular-header into main
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 18s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 8m27s
2026-08-28 14:08:37 +00:00
devops 9c441032de feat(ecommerce): logo circular en el header (pedido del usuario)
Reemplaza el lockup rectangular (logo-ari.svg) por un isotipo circular
nuevo (logo-ari-circular.svg) con el icono mariposa + wordmark 'ARI
shopping' integrados dentro del mismo circulo azul -- el formato que
tenia la version previa a ADR 0008, preferido sobre el rectangulo
segun mockup de referencia del usuario. El footer conserva el lockup
rectangular, donde si tiene sentido como bloque de color autocontenido
sobre el fondo oscuro.
2026-08-28 14:08:35 +00:00
devops 603f4b963d chore: release v1.0.195 [skip ci] 2026-08-28 06:32:19 +00:00
devops ac37774245 Merge fix/adr0008-tiles-pastel into main
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 18s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 7m1s
2026-08-28 06:25:03 +00:00
devops 7aa9f40abc feat(ecommerce): ADR 0008 sub-fase 4 - tiles de categoria pastel
category-tiles.tsx (PromoTile, compartido entre home y grid de
catalogo) nunca se toco en ninguna sub-fase de ADR 0008 -- la sub-fase
4 ('Home: hero, tiles, secciones') quedo pendiente de implementar
(confirmado: no existe merge fix/adr0008-*-subfase4-* en el historial).
Seguia con gradientes saturados rojo/violeta/magenta/cian del esquema
arcoiris original, confirmado visualmente en produccion.

Reemplaza los gradientes por fondos pastel solidos (amber/sky/rose/
emerald/orange/violet-100, patron Walmart-inspirado), texto oscuro
sobre el pastel, boton circular de accion en azul de marca
(--color-ari-marino) en vez de blanco/negro generico, y los acentos
fucsia/cian del encabezado de seccion por los tokens de marca.
2026-08-28 06:22:47 +00:00
devops 617a69d58a chore: release v1.0.194 [skip ci] 2026-08-28 06:15:43 +00:00
devops 09a8698f7e Merge fix/adr0008-buscador-un-solo-foco into main
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 21s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 6m22s
2026-08-28 06:09:08 +00:00
devops 23700c5b1d fix(ecommerce): buscador usa un solo indicador de foco (no dos)
El outline de accesibilidad global (input:focus-visible, azul de
marca, globals.css) y el indicador de foco propio del wrapper
(focus-within:border-ari-marino + ring amarillo) se mostraban ambos a
la vez -- redondear el outline (commit anterior) evito el rectangulo
recto pero seguia leyendose como dos bordes anidados, reportado en
produccion (v1.0.193).

focus-visible:outline-none en el input iguala la especificidad de la
regla global (mismo patron que ya usa buttonVariants en button.tsx) y
deja el indicador de foco del wrapper como el unico visible -- no se
pierde accesibilidad, se elimina la duplicacion.
2026-08-28 06:09:06 +00:00
devops 5c173d5f40 chore: release v1.0.193 [skip ci] 2026-08-28 06:02:07 +00:00
devops d0a3554959 Merge fix/adr0008-buscador-outline-a11y-redondeado into main
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 18s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 8m32s
2026-08-28 05:53:17 +00:00
devops 75dedc4e67 fix(ecommerce): outline de accesibilidad del buscador sigue el pill
globals.css define input:focus-visible { outline: 3px solid
rgb(0 85 184 / .5); ... } como indicador de foco por teclado global,
intencional (no se toca). Le gana en especificidad CSS a la utilidad
outline-none de Tailwind, asi que seguia mostrandose sobre el input
propio del buscador (commit anterior) -- pero como ese input no tenia
border-radius, se veia como un rectangulo de esquinas rectas
superpuesto al pill redondeado, reportado en produccion (v1.0.192).

Agrega rounded-full al input para que el mismo outline (que se debe
conservar) siga el contorno del pill en vez de cortarlo.
2026-08-28 05:53:15 +00:00
devops 801db95aa8 chore: release v1.0.192 [skip ci] 2026-08-28 05:45:57 +00:00
devops 6df24abcb9 Merge fix/adr0008-buscador-ring-input-propio into main
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 18s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 7m49s
2026-08-28 05:37:51 +00:00
devops a6291ffe14 fix(ecommerce): buscador usa input propio en vez del <Input> compartido
El fix anterior (focus:border-0 focus:ring-0) no elimino de forma
confiable el focus:ring-4 focus:ring-cyan-300/30 hardcodeado del
componente <Input> compartido dentro de este contenedor compuesto
(input + boton circular en el mismo wrapper) -- reportado en
produccion (v1.0.191): un segundo ring amarillo se superponia y
cortaba visualmente el boton de la lupa al hacer click.

En vez de seguir peleando el override via tailwind-merge, el buscador
deja de usar el <Input> compartido y usa un <input> nativo con sus
propias clases minimas, sin nada que sobreescribir.
2026-08-28 05:37:50 +00:00
devops 030bf2712a chore: release v1.0.191 [skip ci] 2026-08-28 05:20:14 +00:00
devops f5ddb63c8a Merge fix/adr0008-buscador-doble-ring into main
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 19s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 8m3s
2026-08-28 05:11:56 +00:00
devops d536b0bb8b fix(ecommerce): quita el doble ring de foco del buscador
El Input base (components/ui/input.tsx) trae focus:border-cyan-500 y
focus:ring-4 focus:ring-cyan-300/30 hardcodeados. La correccion
anterior del buscador solo anulaba focus-visible:ring-0, una variante
distinta a :focus -- el ring cian del componente base seguia
disparandose al hacer click, superpuesto al ring amarillo del wrapper
nuevo (dos anillos visibles a la vez, reportado por el usuario en
produccion tras el deploy de v1.0.190).

focus:border-0 focus:ring-0 anula correctamente ambas utilidades del
Input base via tailwind-merge (mismo variant+propiedad).
2026-08-28 05:10:22 +00:00
devops 260c7076e2 chore: release v1.0.190 [skip ci] 2026-08-28 05:06:07 +00:00
devops 59024cd6e5 Merge fix/adr0008-buscador-azul-amarillo into main
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 20s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 7m20s
2026-08-28 04:58:28 +00:00
devops ebd341c060 feat(ecommerce): ADR 0008 - buscador azul & amarillo (nunca migrado)
predictive-search.tsx nunca estuvo en el alcance de ninguna sub-fase
de ADR 0008 (sub-fase 2 solo tocaba navbar.tsx/footer.tsx), asi que
seguia con el borde degradado cian/violeta/fucsia y acentos violeta
del esquema arcoiris original -- confirmado visualmente en produccion
(captura del usuario) y en el codigo.

Reemplaza el wrapper de gradiente por un input con borde solido y un
boton circular azul (--color-ari-marino) con la lupa a la derecha, en
vez del icono flotando a la izquierda sobre el degradado. Foco usa
ring amarillo (--color-ari-coral-claro), igual que el resto de
componentes interactivos del sitio (button.tsx). Hover/foco del panel
de resultados y del boton 'Buscar en todo el catalogo' pasan de
violeta a los tokens de marca.
2026-08-28 04:57:43 +00:00
devops e7349f34c3 chore: release v1.0.189 [skip ci] 2026-08-28 04:52:12 +00:00
devops c94ad5f651 Merge fix/adr0008-logo-cache-busting into main
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 22s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 9m39s
2026-08-28 04:42:10 +00:00
devops ce929188ac fix(ecommerce): baja TTL de cache a 5min para /images/brand/*
El header de /images/:path* (7 dias, pensado para fotos de producto
que no cambian) tambien alcanzaba a los assets de marca (logo,
favicon), que si cambian con cada iteracion de identidad visual.

Confirmado en vivo contra shop.cruzcloud.net: Cloudflare seguia
sirviendo el logo de la iteracion anterior 'Navy & Coral'
(fill #122844/#D85A30) mas de 14h despues de que el pod ya corria
v1.0.185 con el logo 'Azul & Amarillo' (#0055B8/#FFC220) --
cf-cache-status: HIT, age: 51378s, last-modified anterior al redeploy.

No es un bug del logo.svg en si (el contenido del archivo ya era
correcto desde la sub-fase 1 de ADR 0008) sino de una politica de
cache compartida con assets que no deberian tener el mismo TTL.
2026-08-28 04:35:18 +00:00
gitea-actions 98e0d331e9 chore(gitops): deploy Docs Portal v1.0.188 [skip ci] 2026-08-28 04:10:37 +00:00
devops d299fc1201 Merge fix/docs-tmux-persistir-trust-claude-json into main
Build and Push Docs Portal / Escaneo de Secretos (Gitleaks) (push) Successful in 20s
Build and Push Docs Portal / Construir y publicar Docs Portal (push) Successful in 5m37s
Merge local directo (sin PR de Gitea) por acuerdo del usuario; ver
feedback_git_fix_branches en memoria del proyecto para el porque de
este flujo estandar.
2026-08-27 23:04:40 -05:00
devops 72a2715502 docs: agrega mount de .claude.json (la confianza del folder no persistia)
hasTrustDialogAccepted vive en ~/.claude.json (archivo suelto en
/root), no dentro de ~/.claude/ que ya estaba montado -- se perdia en
cada recreacion del contenedor pese a que el login si persistia.
Fix: bind mount archivo-a-archivo, sembrado con el .claude.json ya
confirmado. Verificado en vivo: claude arranca sin re-pedir el
dialogo tras recrear el contenedor.
2026-08-27 23:04:26 -05:00
gitea-actions 1ed06edbdd chore(gitops): deploy Docs Portal v1.0.187 [skip ci] 2026-08-28 03:56:52 +00:00
devops ffcadc5dca Merge fix/docs-tmux-mounts-symlink-git-ownership into main
Build and Push Docs Portal / Escaneo de Secretos (Gitleaks) (push) Successful in 24s
Build and Push Docs Portal / Construir y publicar Docs Portal (push) Successful in 12m59s
Merge local directo (sin PR de Gitea) por acuerdo del usuario; ver
feedback_git_fix_branches en memoria del proyecto para el porque de
este flujo estandar.
2026-08-27 22:44:07 -05:00
devops 61c57fbf28 docs: corrige receta de tmux-host (symlink /DATA/AppData + git ownership)
El comando docker run documentado no funcionaba realmente desde
dentro del contenedor: /DATA/AppData es un symlink absoluto a
/media/HDD-Storage/AppData, que queda roto si solo se monta /DATA.
Agrega el mount que falta, el git safe.directory necesario (baked en
el arranque, no persiste en el volumen de credenciales), y documenta
ambos gotchas con como se detectaron.
2026-08-27 22:33:27 -05:00
devops a31a0d96b6 Merge fix/docs-sesion-tmux-persistente into main
Build and Push Docs Portal / Escaneo de Secretos (Gitleaks) (push) Successful in 25s
Build and Push Docs Portal / Construir y publicar Docs Portal (push) Failing after 7m38s
Merge local directo (sin PR de Gitea) por acuerdo del usuario; ver
feedback_git_fix_branches en memoria del proyecto para el porque de
este flujo estandar.
2026-08-27 22:18:48 -05:00
10 changed files with 196 additions and 30 deletions

No files matched your search

@@ -54,17 +54,114 @@ docker run -d \
--name tmux-host \ --name tmux-host \
--restart unless-stopped \ --restart unless-stopped \
-v /DATA:/DATA \ -v /DATA:/DATA \
-v /media/HDD-Storage:/media/HDD-Storage \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
-v /DATA/AppData/k3d-lab-v3/claude-code-home:/root/.claude \ -v /DATA/AppData/k3d-lab-v3/claude-code-home:/root/.claude \
-v /DATA/AppData/k3d-lab-v3/claude-code-home.json:/root/.claude.json \
--network host \ --network host \
alpine sh -c "apk add --no-cache tmux docker-cli bash curl git kubectl nodejs npm && npm install -g @anthropic-ai/claude-code && tail -f /dev/null" alpine sh -c "apk add --no-cache tmux docker-cli bash curl git kubectl nodejs npm && npm install -g @anthropic-ai/claude-code && git config --global --add safe.directory '*' && tail -f /dev/null"
``` ```
| Volumen | Destino | Por qué | | Volumen | Destino | Por qué |
|---|---|---| |---|---|---|
| `/DATA` | `/DATA` | Acceso a los repos (`apps-registry`, `platform-infra`, `scripts`) y configs del lab, mismos paths que en el host | | `/DATA` | `/DATA` | Acceso a los repos (`apps-registry`, `platform-infra`, `scripts`) y configs del lab, mismos paths que en el host |
| `/media/HDD-Storage` | `/media/HDD-Storage` | Ver "Gotcha: el symlink `/DATA/AppData`" abajo — sin esto, los repos no se ven dentro del contenedor pese a montar `/DATA` |
| `/var/run/docker.sock` | `/var/run/docker.sock` | Seguir controlando Docker y k3d (contenedores, `k3d node`, etc.) desde dentro del contenedor | | `/var/run/docker.sock` | `/var/run/docker.sock` | Seguir controlando Docker y k3d (contenedores, `k3d node`, etc.) desde dentro del contenedor |
| `/DATA/AppData/k3d-lab-v3/claude-code-home` | `/root/.claude` | Persistir login/config de Claude Code — sobrevive no solo a un `docker restart`, sino a recrear el contenedor entero | | `/DATA/AppData/k3d-lab-v3/claude-code-home` | `/root/.claude` | Persistir login/config de Claude Code — sobrevive no solo a un `docker restart`, sino a recrear el contenedor entero |
| `/DATA/AppData/k3d-lab-v3/claude-code-home.json` | `/root/.claude.json` | Ver "Gotcha: la confianza del folder no persistía" abajo — es un archivo suelto, no vive dentro de `~/.claude/` |
## Gotcha: el symlink `/DATA/AppData`
Montar `-v /DATA:/DATA` **no alcanza** para ver los repos dentro del
contenedor. `/DATA/AppData` en el host es un symlink *absoluto*:
```
/DATA/AppData -> /media/HDD-Storage/AppData
```
Los repos del lab (`lab-cruzcloud`) viven físicamente en el disco
btrfs `/media/HDD-Storage`, no en el ext4 de `/DATA` — ZimaOS lo
expone dentro de `/DATA` como submount (`/DATA/.media/HDD-Storage/...`,
que sí viaja al contenedor porque Docker hace bind recursivo por
defecto) pero el path "bonito" `/DATA/AppData/...` que usan `CLAUDE.md`
y todos los scripts del lab llega ahí vía ese symlink absoluto, no vía
el submount. Un symlink absoluto que apunta fuera del árbol montado
queda roto dentro del contenedor aunque el submount subyacente esté
perfectamente accesible por otra ruta.
**Cómo se detectó:** todo el trabajo de configuración inicial de este
mismo setup se hizo con `git`/`docker exec` corriendo *en el host*, no
dentro del contenedor — nadie había probado `cd
/DATA/AppData/k3d-lab-v3/lab-cruzcloud` desde una sesión `lab` real
hasta que se preguntó explícitamente por esto. `ls` devolvía "No such
file or directory" pese a que el volumen de `/DATA` estaba montado y
"andando".
**Fix:** montar también `-v /media/HDD-Storage:/media/HDD-Storage`,
para que el symlink resuelva exactamente igual que en el host.
## Gotcha: git "dubious ownership" dentro del contenedor
Con el mount anterior ya arreglado, `git status`/`git log` dentro del
contenedor fallan con:
```
fatal: detected dubious ownership in repository at '/media/HDD-Storage/...'
```
Los archivos en `/DATA`/`/media/HDD-Storage` pertenecen al usuario
`devops` del host (via samba), pero los procesos dentro del
contenedor corren como `root` (UID 0) — git, desde la versión que
introdujo la protección CVE-2022-24765, rechaza operar sobre un repo
cuyo dueño no coincide con el UID del proceso, a menos que el path
esté explícitamente marcado como seguro.
**Fix:** `git config --global --add safe.directory '*'` dentro del
contenedor. El wildcard (en vez de listar cada repo) es intencional
acá: este contenedor ya tiene acceso root-equivalente al host vía el
volumen de `/var/run/docker.sock`, así que restringir `safe.directory`
por repo no suma aislamiento real.
**Por qué está en el comando de arranque y no como paso manual
aparte:** igual que los paquetes `apk`, esta configuración vive en el
filesystem efímero del contenedor (`/root/.gitconfig`, que no está
bajo el volumen de `/root/.claude`) — se perdería si el contenedor se
recrea. Ponerla en el `sh -c` de arranque la deja recreándose sola
cada vez, sin pasos manuales que recordar.
## Gotcha: la confianza del folder no persistía
Al abrir `claude` por primera vez en un directorio, pide confirmar un
diálogo de "¿confiás en este folder?" (más aún acá, porque este repo
trae un `.claude/settings.local.json` con permisos pre-aprobados). Esa
confirmación se guarda en `~/.claude.json` — un **archivo suelto en
`/root`**, no dentro de la carpeta `~/.claude/` que ya estaba montada
como volumen. Resultado: aunque el login (`~/.claude/.credentials.json`)
sí persistía, el diálogo de confianza volvía a aparecer cada vez que
se recreaba el contenedor — justo el tipo de "reconfigurar de nuevo"
que este setup existe para evitar.
**Cómo se detectó:** al confirmar el diálogo por primera vez, se
comparó `/root/.claude.json` dentro del contenedor contra sí mismo
antes y después — mismo tamaño y mismo `mtime`, es decir, la
confirmación en pantalla nunca se había registrado. La causa real:
la primera vez se movió el selector con las flechas pero nunca se
presionó Enter, así que en rigor el diálogo seguía sin confirmar; una
vez confirmado *de verdad* (`tmux send-keys` moviendo el cursor a
"Yes, I trust this folder" + Enter), el archivo saltó de 2.2 KB a
40 KB y apareció la clave `projects` con la ruta del repo adentro.
**Fix:** copiar ese `.claude.json` ya confirmado a
`/DATA/AppData/k3d-lab-v3/claude-code-home.json` en el host, y
montarlo **archivo a archivo** (no como carpeta) en
`-v .../claude-code-home.json:/root/.claude.json`. A diferencia de
los demás volúmenes de este setup, este necesitó sembrarse con
contenido real primero — un bind mount de archivo con un origen que
no existe todavía en el host termina creando un directorio vacío en
su lugar, no un archivo, y rompe el arranque del contenedor.
Verificado en vivo: tras recrear el contenedor con este mount, `claude`
arrancó directo en la sesión normal, sin volver a pedir el diálogo de
confianza.
Sobre paquetes: alpine (imagen `alpine` sin pin de versión, la misma Sobre paquetes: alpine (imagen `alpine` sin pin de versión, la misma
que uso este setup) sí trae `kubectl` en sus repos — no hizo falta que uso este setup) sí trae `kubectl` en sus repos — no hizo falta
@@ -104,7 +201,7 @@ crea. No hay que recordar cuál de los dos casos aplica.
|---|---|---| |---|---|---|
| Corte de SSH / cerrar la terminal / cambiar de dispositivo | Sí — es exactamente el caso que este setup resuelve | Sí | | Corte de SSH / cerrar la terminal / cambiar de dispositivo | Sí — es exactamente el caso que este setup resuelve | Sí |
| `docker restart tmux-host` (reinicio del NAS) | **No** — el proceso `tmux server` muere con el contenedor; `lab-session.sh` crea una sesión nueva automáticamente | Sí, vía el volumen montado en `/root/.claude` | | `docker restart tmux-host` (reinicio del NAS) | **No** — el proceso `tmux server` muere con el contenedor; `lab-session.sh` crea una sesión nueva automáticamente | Sí, vía el volumen montado en `/root/.claude` |
| `docker rm` + recrear el contenedor desde cero | No | Sí, vía el mismo volumen — no hace falta volver a autenticar | | `docker rm` + recrear el contenedor desde cero | No | Sí, vía el mismo volumen — no hace falta volver a autenticar ni reconfirmar el diálogo de confianza del folder (ver el mount de `claude-code-home.json`) |
## Validación en vivo ## Validación en vivo
@@ -119,3 +216,10 @@ un archivo de prueba escrito en `/root/.claude` dentro del contenedor
apareció de inmediato en apareció de inmediato en
`/DATA/AppData/k3d-lab-v3/claude-code-home` en el host, confirmando `/DATA/AppData/k3d-lab-v3/claude-code-home` en el host, confirmando
el bind mount de credenciales. el bind mount de credenciales.
Después de agregar el mount de `/media/HDD-Storage` y el
`safe.directory`, se validó además: `md5sum` de `CLAUDE.md` idéntico
leído desde el host y desde dentro del contenedor (vía el path con
symlink `/DATA/AppData/...`), y `git log`/`git status` corriendo sin
error sobre `apps-registry` y `scripts` desde una shell dentro de
`tmux-host`.
@@ -25,7 +25,7 @@ spec:
runAsNonRoot: false runAsNonRoot: false
containers: containers:
- name: app - name: app
image: gitea.cruzcloud.net/devops/docs-portal:v1.0.181 image: gitea.cruzcloud.net/devops/docs-portal:v1.0.188
# El base saca TODAS las capabilities. Probado en vivo y # El base saca TODAS las capabilities. Probado en vivo y
# revertido: con drop: ALL (aunque se agregue NET_BIND_SERVICE # revertido: con drop: ALL (aunque se agregue NET_BIND_SERVICE
# de vuelta) el propio entrypoint de nginx:1.27-alpine falla al # de vuelta) el propio entrypoint de nginx:1.27-alpine falla al
@@ -33,5 +33,5 @@ export default async function ProductPage({ params }: ProductPageProps) {
if (!product) notFound(); if (!product) notFound();
const products = await getProducts(); const products = await getProducts();
const recommended = products.filter((candidate) => candidate.id !== product.id && (candidate.category === product.category || candidate.collection === product.collection)).slice(0, 8); const recommended = products.filter((candidate) => candidate.id !== product.id && (candidate.category === product.category || candidate.collection === product.collection)).slice(0, 8);
return <><section className="mx-auto grid max-w-7xl gap-10 px-4 py-10 lg:grid-cols-[1.08fr_.92fr] lg:py-16"><ProductGallery images={product.images} name={product.name} /><div><span className="text-xs font-black uppercase tracking-[.18em] text-fuchsia-600">{product.category} · {product.collection}</span><h1 className="mt-3 text-4xl font-black tracking-tight sm:text-6xl">{product.name}</h1><p className="mt-5 text-lg leading-8 text-slate-600">{product.description}</p><div className="mt-8"><ProductPurchase product={product} /></div><Accordion type="single" collapsible className="mt-8"><AccordionItem value="details"><AccordionTrigger>Detalles del producto</AccordionTrigger><AccordionContent>Marca: {product.brand}. Edad recomendada: {product.ageRange}. La disponibilidad y presentación final se confirman con Ari Shopping.</AccordionContent></AccordionItem><AccordionItem value="shipping"><AccordionTrigger>Envíos y entregas</AccordionTrigger><AccordionContent>Coordinamos envíos nacionales y entrega según ubicación. Los tiempos se confirman durante la cotización.</AccordionContent></AccordionItem></Accordion></div></section>{recommended.length > 0 && <section className="mx-auto max-w-7xl px-4 pb-24"><h2 className="mb-7 text-3xl font-black">También te puede gustar</h2><ProductCarousel products={recommended} /></section>}<MobileAddToCart product={product} /></>; return <><section className="mx-auto grid max-w-7xl gap-10 px-4 py-10 lg:grid-cols-[1.08fr_.92fr] lg:py-16"><ProductGallery images={product.images} name={product.name} /><div><span className="text-xs font-black uppercase tracking-[.18em] text-ari-coral">{product.category} · {product.collection}</span><h1 className="mt-3 text-4xl font-black tracking-tight sm:text-6xl">{product.name}</h1><p className="mt-5 text-lg leading-8 text-slate-600">{product.description}</p><div className="mt-8"><ProductPurchase product={product} /></div><Accordion type="single" collapsible className="mt-8"><AccordionItem value="details"><AccordionTrigger>Detalles del producto</AccordionTrigger><AccordionContent>Marca: {product.brand}. Edad recomendada: {product.ageRange}. La disponibilidad y presentación final se confirman con Ari Shopping.</AccordionContent></AccordionItem><AccordionItem value="shipping"><AccordionTrigger>Envíos y entregas</AccordionTrigger><AccordionContent>Coordinamos envíos nacionales y entrega según ubicación. Los tiempos se confirman durante la cotización.</AccordionContent></AccordionItem></Accordion></div></section>{recommended.length > 0 && <section className="mx-auto max-w-7xl px-4 pb-24"><h2 className="mb-7 text-3xl font-black">También te puede gustar</h2><ProductCarousel products={recommended} /></section>}<MobileAddToCart product={product} /></>;
} }
@@ -10,16 +10,20 @@ type TileContent = { subtitle: string; image?: string; color: string };
// (ver ADR 0005) -- solo se mapean imagen/subtitulo para las que ya tienen // (ver ADR 0005) -- solo se mapean imagen/subtitulo para las que ya tienen
// un asset en public/images/categories/. Las demas caen al tile de solo // un asset en public/images/categories/. Las demas caen al tile de solo
// gradiente en vez de mostrar una imagen que no les corresponde. // gradiente en vez de mostrar una imagen que no les corresponde.
// Fondos pastel solidos (patron Walmart-inspirado, ADR 0008 sub-fase 4)
// en vez de los gradientes saturados originales del esquema arcoiris --
// un tono pastel distinto por categoria, no una escala de marca (serian
// todos casi iguales), pero coordinados en la misma temperatura suave.
const tileContent: Record<string, TileContent> = { const tileContent: Record<string, TileContent> = {
juguetes: { subtitle: "Personajes que inspiran", image: "/images/categories/figuras.webp", color: "from-yellow-300 to-orange-400" }, juguetes: { subtitle: "Personajes que inspiran", image: "/images/categories/figuras.webp", color: "bg-amber-100" },
"mochilas-y-morrales": { subtitle: "Estilo para cada aventura", image: "/images/categories/mochilas.webp", color: "from-cyan-400 to-blue-500" }, "mochilas-y-morrales": { subtitle: "Estilo para cada aventura", image: "/images/categories/mochilas.webp", color: "bg-sky-100" },
"bebes-y-primera-infancia": { subtitle: "Primeros descubrimientos", color: "from-pink-300 to-fuchsia-400" }, "bebes-y-primera-infancia": { subtitle: "Primeros descubrimientos", color: "bg-rose-100" },
educativos: { subtitle: "Aprender jugando", color: "from-emerald-400 to-teal-600" }, educativos: { subtitle: "Aprender jugando", color: "bg-emerald-100" },
"accion-y-aventura": { subtitle: "Para explorar sin límites", color: "from-red-500 to-rose-700" }, "accion-y-aventura": { subtitle: "Para explorar sin límites", color: "bg-orange-100" },
variedades: { subtitle: "Un poco de todo", color: "from-violet-400 to-purple-600" }, variedades: { subtitle: "Un poco de todo", color: "bg-violet-100" },
}; };
const defaultTile: TileContent = { subtitle: "Explora esta categoría", color: "from-slate-400 to-slate-600" }; const defaultTile: TileContent = { subtitle: "Explora esta categoría", color: "bg-slate-100" };
// Tile individual reutilizado tanto por la seccion de mundos del home // Tile individual reutilizado tanto por la seccion de mundos del home
// como por el grid del catalogo (tiles promocionales intercalados, // como por el grid del catalogo (tiles promocionales intercalados,
@@ -31,20 +35,20 @@ export function PromoTile({ category, className = "" }: { category: Category; cl
return ( return (
<Link <Link
href={`/catalog?category=${encodeURIComponent(category.name)}`} href={`/catalog?category=${encodeURIComponent(category.name)}`}
className={cn(`group relative min-h-64 overflow-hidden rounded-[2rem] bg-gradient-to-br ${content.color} shadow-xl`, className)} className={cn(`group relative min-h-64 overflow-hidden rounded-[2rem] ${content.color} shadow-sm ring-1 ring-slate-900/5`, className)}
> >
{content.image && ( {content.image && (
<> <>
<Image src={content.image} alt={category.name} fill sizes="(max-width: 640px) 100vw, (max-width: 1024px) 33vw, 16vw" className="object-cover transition duration-500 group-hover:scale-105" /> <Image src={content.image} alt={category.name} fill sizes="(max-width: 640px) 100vw, (max-width: 1024px) 33vw, 16vw" className="object-cover transition duration-500 group-hover:scale-105" />
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/80 via-transparent to-white/5" /> <div className="absolute inset-x-0 bottom-0 h-2/5 bg-gradient-to-t from-white/95 via-white/70 to-transparent" />
</> </>
)} )}
<div className="absolute inset-x-0 bottom-0 flex items-end justify-between p-6 text-white"> <div className="absolute inset-x-0 bottom-0 flex items-end justify-between p-6 text-slate-900">
<div> <div>
<h3 className="text-xl font-black">{category.name}</h3> <h3 className="text-xl font-black">{category.name}</h3>
<p className="mt-1 text-sm font-semibold text-white/80">{content.subtitle}</p> <p className="mt-1 text-sm font-semibold text-slate-600">{content.subtitle}</p>
</div> </div>
<span className="grid size-11 shrink-0 place-items-center rounded-full bg-white text-slate-950 transition group-hover:rotate-45"> <span className="grid size-11 shrink-0 place-items-center rounded-full bg-ari-marino text-white transition group-hover:rotate-45">
<ArrowUpRight /> <ArrowUpRight />
</span> </span>
</div> </div>
@@ -61,10 +65,10 @@ export function CategoryTiles({ categories }: { categories: Category[] }) {
<section className="mx-auto max-w-7xl px-4 py-14"> <section className="mx-auto max-w-7xl px-4 py-14">
<div className="mb-8 flex items-end justify-between gap-4"> <div className="mb-8 flex items-end justify-between gap-4">
<div> <div>
<span className="text-xs font-black uppercase tracking-[.18em] text-fuchsia-600">Explora por mundos</span> <span className="text-xs font-black uppercase tracking-[.18em] text-ari-coral">Explora por mundos</span>
<h2 className="mt-2 text-4xl font-black tracking-tight sm:text-5xl">Encuentra tu próximo tesoro</h2> <h2 className="mt-2 text-4xl font-black tracking-tight sm:text-5xl">Encuentra tu próximo tesoro</h2>
</div> </div>
<Link href="/catalog" className="hidden font-black text-cyan-700 sm:block">Ver todo →</Link> <Link href="/catalog" className="hidden font-black text-ari-marino sm:block">Ver todo →</Link>
</div> </div>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6"> <div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
{roots.map((category) => ( {roots.map((category) => (
@@ -52,9 +52,13 @@ export function Navbar({ products, categories }: { products: Product[]; categori
<div className="relative overflow-visible bg-white"> <div className="relative overflow-visible bg-white">
<div className="pointer-events-none absolute inset-x-0 top-0 h-1 bg-ari-coral-claro" /> <div className="pointer-events-none absolute inset-x-0 top-0 h-1 bg-ari-coral-claro" />
<div className="mx-auto grid max-w-7xl grid-cols-[auto_1fr_auto] items-center gap-4 px-4 py-4 lg:gap-8"> <div className="mx-auto grid max-w-7xl grid-cols-[auto_1fr_auto] items-center gap-4 px-4 py-4 lg:gap-8">
{/* Isotipo circular con icono + wordmark integrados (como en la
version previa a ADR 0008), pedido explicito del usuario en
vez del lockup rectangular. logo-ari.svg (rectangular) se
mantiene para el footer. */}
<Link href="/" aria-label="ARI shopping" className="shrink-0"> <Link href="/" aria-label="ARI shopping" className="shrink-0">
{/* eslint-disable-next-line @next/next/no-img-element -- vector propio, next/image no optimiza SVG sin habilitar dangerouslyAllowSVG */} {/* eslint-disable-next-line @next/next/no-img-element -- vector propio, next/image no optimiza SVG sin habilitar dangerouslyAllowSVG */}
<img src="/images/brand/logo-ari.svg" alt="ARI shopping" className="h-11 w-auto lg:h-14" width={200} height={64} /> <img src="/images/brand/logo-ari-circular.svg" alt="ARI shopping" className="size-16 lg:size-20" width={200} height={200} />
</Link> </Link>
<PredictiveSearch /> <PredictiveSearch />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -18,5 +18,5 @@ export function ProductPurchase({ product }: { product: Product }) {
selectedVariant.inventoryQuantity <= LOW_STOCK_THRESHOLD selectedVariant.inventoryQuantity <= LOW_STOCK_THRESHOLD
? selectedVariant.inventoryQuantity ? selectedVariant.inventoryQuantity
: null; : null;
return <div><div className="mb-5 flex flex-wrap gap-2">{product.variants.map((variant) => <button key={variant.id} onClick={() => setVariantId(variant.id)} className={`rounded-full border px-4 py-2 text-sm font-bold ${variantId === variant.id ? "border-fuchsia-500 bg-fuchsia-50 text-fuchsia-700" : "border-slate-200"}`}>{variant.value}</button>)}</div><div className="mb-6 flex items-end gap-3"><strong className="text-3xl font-black">{formatMoney(product.price, product.currency)}</strong>{product.compareAtPrice && <del className="text-slate-400">{formatMoney(product.compareAtPrice, product.currency)}</del>}</div><Button variant="primary" size="lg" className="w-full" onClick={() => addItem(product, variantId)}><ShoppingBag />Añadir al carrito</Button><div className="mt-5 grid gap-3 rounded-2xl bg-slate-50 p-4 text-sm">{lowStock !== null ? <span className="flex items-center gap-2 font-bold text-amber-600"><TriangleAlert className="size-4" />Quedan {lowStock} unidades</span> : <span className="flex items-center gap-2 font-bold"><CheckCircle2 className="size-4 text-emerald-500" />Disponibilidad por confirmar</span>}<span className="flex items-center gap-2 font-bold"><Truck className="size-4 text-cyan-600" />Envíos nacionales</span></div></div>; return <div><div className="mb-5 flex flex-wrap gap-2">{product.variants.map((variant) => <button key={variant.id} onClick={() => setVariantId(variant.id)} className={`rounded-full border px-4 py-2 text-sm font-bold ${variantId === variant.id ? "border-ari-marino bg-ari-marino text-white" : "border-slate-200"}`}>{variant.value}</button>)}</div><div className="mb-6 flex items-end gap-3"><strong className="text-3xl font-black">{formatMoney(product.price, product.currency)}</strong>{product.compareAtPrice && <del className="text-slate-400">{formatMoney(product.compareAtPrice, product.currency)}</del>}</div>{/* oculto en mobile: MobileAddToCart (barra fija abajo, lg:hidden) ya cubre esa misma accion ahi -- sin el hidden aqui, ambos se mostraban a la vez. */}<Button variant="primary" size="lg" className="hidden w-full lg:inline-flex" onClick={() => addItem(product, variantId)}><ShoppingBag />Añadir al carrito</Button><div className="mt-5 grid gap-3 rounded-2xl bg-slate-50 p-4 text-sm">{lowStock !== null ? <span className="flex items-center gap-2 font-bold text-amber-600"><TriangleAlert className="size-4" />Quedan {lowStock} unidades</span> : <span className="flex items-center gap-2 font-bold"><CheckCircle2 className="size-4 text-emerald-500" />Disponibilidad por confirmar</span>}<span className="flex items-center gap-2 font-bold"><Truck className="size-4 text-cyan-600" />Envíos nacionales</span></div></div>;
} }
@@ -5,7 +5,6 @@ import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Search, X } from "lucide-react"; import { Search, X } from "lucide-react";
import { FormEvent, useEffect, useState } from "react"; import { FormEvent, useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
import type { Product } from "@/lib/headless/types"; import type { Product } from "@/lib/headless/types";
export function PredictiveSearch() { export function PredictiveSearch() {
@@ -54,12 +53,34 @@ export function PredictiveSearch() {
return ( return (
<form onSubmit={submit} className="relative w-full"> <form onSubmit={submit} className="relative w-full">
<div className="rounded-full bg-gradient-to-r from-cyan-400 via-violet-500 to-fuchsia-500 p-[2px] shadow-lg shadow-violet-500/10"> <div className="flex items-center rounded-full border-2 border-slate-200 bg-white pl-5 pr-1.5 shadow-sm transition focus-within:border-ari-marino focus-within:ring-4 focus-within:ring-ari-coral-claro/30">
<div className="relative rounded-full bg-white"> {/* input propio (no el <Input> compartido) -- ese componente trae su
<Search className="pointer-events-none absolute left-5 top-1/2 z-10 size-5 -translate-y-1/2 text-violet-600" /> propio focus:ring-4 focus:ring-cyan-300/30 hardcodeado que no se
<Input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Busca juguetes, mochilas, personajes…" className="h-[52px] border-0 bg-transparent pl-[52px] pr-12 shadow-none focus-visible:ring-violet-300/30" /> anulaba de forma confiable via tailwind-merge dentro de este
{query && <button type="button" aria-label="Limpiar búsqueda" className="absolute right-4 top-1/2 -translate-y-1/2 rounded-full p-1 text-slate-400 hover:bg-slate-100" onClick={() => setQuery("")}><X className="size-4" /></button>} contenedor compuesto (input + boton circular), causando un
</div> segundo ring visible al hacer foco. Mas simple y robusto no
heredar clases de foco que hay que pelear despues.
focus-visible:outline-none (no outline-none a secas): globals.css
define un outline de accesibilidad global por teclado
(input:focus-visible, azul de marca) que le gana en
especificidad a un outline-none plano. El wrapper de este
componente ya tiene su propio indicador de foco visible
(focus-within:border-ari-marino + ring amarillo, mismo patron
que button.tsx) que cumple el mismo proposito de accesibilidad
sin duplicarlo -- variante focus-visible: en vez de la base
iguala la especificidad de la regla global (mismo truco que ya
usa button.tsx) y evita el doble indicador de foco. */}
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Busca juguetes, mochilas, personajes…"
className="h-[48px] w-full border-0 bg-transparent p-0 text-sm text-slate-950 focus-visible:outline-none"
/>
{query && <button type="button" aria-label="Limpiar búsqueda" className="mr-1 shrink-0 rounded-full p-1.5 text-slate-400 hover:bg-slate-100" onClick={() => setQuery("")}><X className="size-4" /></button>}
<button type="submit" aria-label="Buscar" className="grid size-10 shrink-0 place-items-center rounded-full bg-ari-marino text-white transition hover:brightness-110">
<Search className="size-4.5" />
</button>
</div> </div>
{loading && normalized.length >= 2 && matches.length === 0 && ( {loading && normalized.length >= 2 && matches.length === 0 && (
<div className="absolute left-0 right-0 top-[calc(100%+10px)] z-50 rounded-2xl border border-slate-200 bg-white p-4 text-center text-sm text-slate-500 shadow-2xl"> <div className="absolute left-0 right-0 top-[calc(100%+10px)] z-50 rounded-2xl border border-slate-200 bg-white p-4 text-center text-sm text-slate-500 shadow-2xl">
@@ -69,12 +90,12 @@ export function PredictiveSearch() {
{matches.length > 0 && ( {matches.length > 0 && (
<div className="absolute left-0 right-0 top-[calc(100%+10px)] z-50 overflow-hidden rounded-2xl border border-slate-200 bg-white p-2 shadow-2xl"> <div className="absolute left-0 right-0 top-[calc(100%+10px)] z-50 overflow-hidden rounded-2xl border border-slate-200 bg-white p-2 shadow-2xl">
{matches.map((product) => ( {matches.map((product) => (
<Link key={product.id} href={`/product/${product.slug}`} prefetch={false} className="grid grid-cols-[54px_1fr] items-center gap-3 rounded-xl p-2 hover:bg-violet-50" onClick={() => setQuery("")}> <Link key={product.id} href={`/product/${product.slug}`} prefetch={false} className="grid grid-cols-[54px_1fr] items-center gap-3 rounded-xl p-2 hover:bg-ari-marino/5" onClick={() => setQuery("")}>
<div className="relative aspect-square overflow-hidden rounded-lg bg-slate-100"><Image src={product.thumbnail} alt="" fill sizes="54px" className="object-cover" /></div> <div className="relative aspect-square overflow-hidden rounded-lg bg-slate-100"><Image src={product.thumbnail} alt="" fill sizes="54px" className="object-cover" /></div>
<div><strong className="line-clamp-1 text-sm">{product.name}</strong><span className="text-xs text-slate-500">{product.category} · {product.collection}</span></div> <div><strong className="line-clamp-1 text-sm">{product.name}</strong><span className="text-xs text-slate-500">{product.category} · {product.collection}</span></div>
</Link> </Link>
))} ))}
<button type="submit" className="mt-1 w-full rounded-xl bg-slate-950 px-4 py-3 text-sm font-black text-white hover:bg-violet-700">Buscar “{query.trim()}” en todo el catálogo</button> <button type="submit" className="mt-1 w-full rounded-xl bg-ari-marino px-4 py-3 text-sm font-black text-white hover:brightness-110">Buscar “{query.trim()}” en todo el catálogo</button>
</div> </div>
)} )}
</form> </form>
@@ -25,7 +25,7 @@ spec:
runAsUser: 1001 runAsUser: 1001
containers: containers:
- name: app - name: app
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.185 image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.195
env: env:
- name: HEADLESS_PROVIDER - name: HEADLESS_PROVIDER
value: medusa value: medusa
+16
View File
@@ -42,6 +42,22 @@ const nextConfig: NextConfig = {
}, },
], ],
}, },
// Assets de marca (logo, favicon) cambian con cada iteracion de
// identidad visual, a diferencia de las fotos de producto -- el
// cache de 7 dias de la regla anterior dejaba servido el logo
// viejo en Cloudflare/navegador durante toda una semana despues de
// cada deploy (ver ADR 0008). Next.js aplica el ultimo header que
// matchee un mismo path, asi que esta regla, al ir despues,
// sobreescribe el Cache-Control solo para /images/brand/*.
{
source: "/images/brand/:path*",
headers: [
{
key: "Cache-Control",
value: "public, max-age=300, must-revalidate",
},
],
},
]; ];
}, },
}; };
@@ -0,0 +1,17 @@
<!-- ADR 0008, paleta "Azul & Amarillo". Isotipo circular (formato de la
version previa a ADR 0008, pedido explicito del usuario en vez del
lockup rectangular de logo-ari.svg): mariposa geometrica de 4 elipses
+ wordmark "ARI shopping", todo dentro del mismo circulo azul.
Ver docs-portal/docs/decisiones/0008-identidad-navy-coral.md -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" role="img" aria-label="ARI shopping">
<circle cx="100" cy="100" r="98" fill="#0055B8"/>
<g transform="translate(100 68) scale(2.6) translate(-17 -11)">
<ellipse cx="10" cy="8" rx="9" ry="7" transform="rotate(-18 10 8)" fill="#FFC220"/>
<ellipse cx="24" cy="8" rx="9" ry="7" transform="rotate(18 24 8)" fill="#FFC220"/>
<ellipse cx="12" cy="16" rx="6" ry="5" transform="rotate(-10 12 16)" fill="#FFDD73"/>
<ellipse cx="22" cy="16" rx="6" ry="5" transform="rotate(10 22 16)" fill="#FFDD73"/>
<rect x="16" y="3" width="2" height="18" rx="1" fill="#0055B8"/>
</g>
<text x="100" y="132" text-anchor="middle" font-family="Poppins, ui-sans-serif, system-ui, sans-serif" font-weight="700" font-size="38" fill="#FFFFFF">ARI</text>
<text x="100" y="153" text-anchor="middle" font-family="Poppins, ui-sans-serif, system-ui, sans-serif" font-weight="700" font-size="14" letter-spacing="3" fill="#FFC220">shopping</text>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB