Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .atl/.skill-registry.cache.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"fingerprint": "d11040d9c59a89af514af78b245d01c57a37d79a"
"fingerprint": "52eb4455fccd840aa5bcee63cf799aa6f1f7fb3e"
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# local agent/tool state
.serena/
47 changes: 47 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ This document defines the mandatory rules and constraints for the AI agent worki
- DO NOT modify, delete, or rewrite existing Prisma migration files on `main`; create a new migration for schema changes instead.
- AI MUST NOT take autonomous business decisions (suggestions only).
- **No commits or PRs without explicit permission**: never run git commit, git push, or create PRs unless the user explicitly requests it. Changes must remain in the staging area/working tree until the user decides otherwise.
- **No issue required for PRs**: this repository does not require a GitHub issue to open a pull request. If a PR template asks for issue linkage, mark it as not applicable instead of blocking PR creation.
- **Never auto-complete PRs**: always stop and ASK before merging. Never merge PRs automatically, even if all checks pass. The user must explicitly approve every merge.

---
Expand Down Expand Up @@ -66,3 +67,49 @@ This document defines the mandatory rules and constraints for the AI agent worki
- Event Bus: `docs/event-bus.md`
- Entity Model: `docs/entities.md`
- Modules: See `docs/module-*.md`

<!-- headroom:rtk-instructions -->

# RTK (Rust Token Killer) - Token-Optimized Commands

When running shell commands, **always prefix with `rtk`**. This reduces context
usage by 60-90% with zero behavior change. If rtk has no filter for a command,
it passes through unchanged — so it is always safe to use.

## Key Commands

```bash
# Git (59-80% savings)
rtk git status rtk git diff rtk git log

# Files & Search (60-75% savings)
rtk ls <path> rtk read <file> rtk grep <pattern>
rtk find <pattern> rtk diff <file>

# Test (90-99% savings) — shows failures only
rtk pytest tests/ rtk cargo test rtk test <cmd>

# Build & Lint (80-90% savings) — shows errors only
rtk tsc rtk lint rtk cargo build
rtk prettier --check rtk mypy rtk ruff check

# Analysis (70-90% savings)
rtk err <cmd> rtk log <file> rtk json <file>
rtk summary <cmd> rtk deps rtk env

# GitHub (26-87% savings)
rtk gh pr view <n> rtk gh run list rtk gh issue list

# Infrastructure (85% savings)
rtk docker ps rtk kubectl get rtk docker logs <c>

# Package managers (70-90% savings)
rtk pip list rtk pnpm install rtk npm run <script>
```

## Rules

- In command chains, prefix each segment: `rtk git add . && rtk git commit -m "msg"`
- For debugging, use raw command without rtk prefix
- `rtk proxy <cmd>` runs command without filtering but tracks usage
<!-- /headroom:rtk-instructions -->
2 changes: 2 additions & 0 deletions app/[locale]/cart/cart-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { CartView } from '@/modules/cart/presentation/components/cart-view';
export type { CartItemDTO } from '@/modules/cart/presentation/components/cart-view';
139 changes: 139 additions & 0 deletions app/[locale]/cart/design-preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
'use client';

import { useEffect, useRef } from 'react';

export interface DesignPositionData {
imageUrl: string;
x: number;
y: number;
scale: number;
rotation_deg: number;
opacity: number;
blend_mode: string;
}

interface DesignPreviewProps {
productImageUrl: string;
designImageUrl: string;
designPosition: DesignPositionData;
width?: number;
height?: number;
borderRadius?: number;
}

export function DesignPreview({
productImageUrl,
designImageUrl,
designPosition,
width = 100,
height = 100,
borderRadius = 6,
}: DesignPreviewProps) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);

useEffect(() => {
let cancelled = false;

async function draw() {
const c = canvasRef.current?.getContext('2d');
if (!c) return;

const productImg = await loadImage(productImageUrl);
const designImg = await loadImage(designImageUrl);
if (cancelled) return;

c.clearRect(0, 0, width, height);
c.fillStyle = '#f4f2e6';
c.fillRect(0, 0, width, height);

if (productImg) {
drawCover(c, productImg, width, height);
}

if (designImg) {
c.globalCompositeOperation =
designPosition.blend_mode === 'multiply'
? 'multiply'
: ('source-over' as GlobalCompositeOperation);
c.globalAlpha = designPosition.opacity / 100;
drawDesign(c, designImg, designPosition, width, height);
}
}
Comment on lines +37 to +61

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

El estado del contexto 2D no se reinicia entre repintados.

drawDesign aplica translate/rotate sobre la matriz del contexto, pero draw() nunca la restablece. Como el mismo <canvas>/contexto persiste entre renders, en el siguiente repintado clearRect/fillRect/drawCover se ejecutan bajo la matriz transformada del render anterior, y globalAlpha/globalCompositeOperation también quedan con valores residuales. Esto produce recortes y posiciones incorrectas tras cambiar designPosition, width o height.

🐛 Reinicio de la matriz y guardado del estado del contexto
       const productImg = await loadImage(productImageUrl);
       const designImg = await loadImage(designImageUrl);
       if (cancelled) return;

+      c.setTransform(1, 0, 0, 1, 0, 0);
+      c.globalAlpha = 1;
+      c.globalCompositeOperation = 'source-over';
       c.clearRect(0, 0, width, height);
       c.fillStyle = '`#f4f2e6`';
       c.fillRect(0, 0, width, height);

       if (productImg) {
         drawCover(c, productImg, width, height);
       }

       if (designImg) {
+        c.save();
         c.globalCompositeOperation =
           designPosition.blend_mode === 'multiply'
             ? 'multiply'
             : ('source-over' as GlobalCompositeOperation);
         c.globalAlpha = designPosition.opacity / 100;
         drawDesign(c, designImg, designPosition, width, height);
+        c.restore();
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function draw() {
const c = canvasRef.current?.getContext('2d');
if (!c) return;
const productImg = await loadImage(productImageUrl);
const designImg = await loadImage(designImageUrl);
if (cancelled) return;
c.clearRect(0, 0, width, height);
c.fillStyle = '#f4f2e6';
c.fillRect(0, 0, width, height);
if (productImg) {
drawCover(c, productImg, width, height);
}
if (designImg) {
c.globalCompositeOperation =
designPosition.blend_mode === 'multiply'
? 'multiply'
: ('source-over' as GlobalCompositeOperation);
c.globalAlpha = designPosition.opacity / 100;
drawDesign(c, designImg, designPosition, width, height);
}
}
async function draw() {
const c = canvasRef.current?.getContext('2d');
if (!c) return;
const productImg = await loadImage(productImageUrl);
const designImg = await loadImage(designImageUrl);
if (cancelled) return;
c.setTransform(1, 0, 0, 1, 0, 0);
c.globalAlpha = 1;
c.globalCompositeOperation = 'source-over';
c.clearRect(0, 0, width, height);
c.fillStyle = '`#f4f2e6`';
c.fillRect(0, 0, width, height);
if (productImg) {
drawCover(c, productImg, width, height);
}
if (designImg) {
c.save();
c.globalCompositeOperation =
designPosition.blend_mode === 'multiply'
? 'multiply'
: ('source-over' as GlobalCompositeOperation);
c.globalAlpha = designPosition.opacity / 100;
drawDesign(c, designImg, designPosition, width, height);
c.restore();
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`[locale]/cart/design-preview.tsx around lines 37 - 61, The 2D canvas
context in draw() is being reused without resetting its transform and drawing
state, so stale translate/rotate, globalAlpha, and globalCompositeOperation
values leak into later renders. In app/[locale]/cart/design-preview.tsx, update
draw() around the existing drawCover and drawDesign calls to reset the context
state before repainting and isolate the design-specific state changes inside
drawDesign or with save/restore on the shared context. Ensure the context is
restored to a clean baseline on every invocation of draw() so changing
designPosition, width, or height does not inherit the previous render’s matrix
or compositing settings.


draw();

return () => {
cancelled = true;
};
}, [productImageUrl, designImageUrl, designPosition, width, height]);

return (
<canvas
ref={canvasRef}
width={width}
height={height}
style={{
width,
height,
borderRadius,
display: 'block',
flexShrink: 0,
}}
/>
);
}

function loadImage(src: string): Promise<HTMLImageElement | null> {
return new Promise((resolve) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => resolve(img);
img.onerror = () => resolve(null);
img.src = src;
});
}

function drawCover(
ctx: CanvasRenderingContext2D,
image: HTMLImageElement,
width: number,
height: number,
) {
const ratio = image.naturalWidth / image.naturalHeight;
const targetRatio = width / height;
let drawWidth: number;
let drawHeight: number;

if (ratio > targetRatio) {
drawHeight = height;
drawWidth = height * ratio;
} else {
drawWidth = width;
drawHeight = width / ratio;
}

const offsetX = (width - drawWidth) / 2;
const offsetY = (height - drawHeight) / 2;
ctx.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
}

function drawDesign(
ctx: CanvasRenderingContext2D,
image: HTMLImageElement,
position: DesignPositionData,
width: number,
height: number,
) {
const scaleFactor = position.scale / 100;
const baseWidth = Math.min(width, image.naturalWidth);
const ratio = image.naturalHeight / image.naturalWidth;
const drawWidth = baseWidth * scaleFactor;
const drawHeight = drawWidth * ratio;

const centerX = position.x * width;
const centerY = position.y * height;

ctx.translate(centerX, centerY);
ctx.rotate((position.rotation_deg * Math.PI) / 180);
ctx.drawImage(image, -drawWidth / 2, -drawHeight / 2, drawWidth, drawHeight);
}
22 changes: 16 additions & 6 deletions app/[locale]/checkout/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { ProductEntity } from '@/modules/products/domain/product-repository
import { Money } from '@/shared/kernel/domain/value-objects/money';
import { Currency } from '@/shared/kernel/domain/value-objects/currency';
import type { CustomizationSnapshot } from '@/modules/cart/domain/customization-lookup-port';
import Image from 'next/image';
import styles from './page.module.css';

interface CheckoutItem {
Expand All @@ -28,6 +29,7 @@ interface CheckoutItem {
color: string | null;
size: string | null;
imageUrl: string | null;
designPosition: CustomizationSnapshot['designPosition'];
}>;
}

Expand Down Expand Up @@ -75,13 +77,11 @@ export default async function CheckoutPage({
// Enrich items with product display data.
const productRepository = container.getProductRepository();
const productIds = [...new Set(cart.items.map((i) => i.productId.value))];
const products = await Promise.all(
productIds.map((id) => productRepository.findById(id, locale)),
);
const productMap = new Map<string, ProductEntity>();
products.forEach((p) => {
if (p) productMap.set(p.id, p);
});
for (const id of productIds) {
const product = await productRepository.findById(id, locale);
if (product) productMap.set(product.id, product);
}

const currency = cart.items[0].unitPriceSnapshot.currency;
const hasMixedCurrencies = cart.items.some(
Expand Down Expand Up @@ -112,6 +112,7 @@ export default async function CheckoutPage({
color: c.color,
size: c.size,
imageUrl: c.imageUrl,
designPosition: c.designPosition,
}));

return {
Expand Down Expand Up @@ -197,6 +198,15 @@ export default async function CheckoutPage({
.join(' · ')}
</span>
)}
{item.customizations[0]?.imageUrl && (
<Image
src={item.customizations[0].imageUrl}
alt="Customization preview"
width={48}
height={48}
className={styles.itemCustomizationThumbnail}
/>
)}
{item.customizationIdList.length >
item.customizations.length && (
<span className={styles.itemCustomizationRemoved}>
Expand Down
Loading
Loading