Skip to content

cosmetic refactor#3628

Merged
evanpelle merged 1 commit into
mainfrom
main-cosmetics-refactor
Apr 10, 2026
Merged

cosmetic refactor#3628
evanpelle merged 1 commit into
mainfrom
main-cosmetics-refactor

Conversation

@evanpelle
Copy link
Copy Markdown
Collaborator

@evanpelle evanpelle commented Apr 10, 2026

Description:

The motivation is to have a single "cosmetic-button" element, so we can abstract out the cosmetic types. This will make it much easier to add new cosmetic types in the future.

Unifies PatternButton and FlagButton into a single CosmeticButton component. Extracts a resolveCosmetics() function that flattens patterns × color palettes + flags into a ResolvedCosmetic[] with relationship status pre-computed, replacing duplicated resolution logic across four callers.

  • New CosmeticButton — renders patterns or flags based on ResolvedCosmetic.type
  • New resolveCosmetics() — centralizes ownership/purchase/blocked resolution
  • Extracted PatternPreview — canvas rendering split into its own module
  • Added type: "pattern" | "flag" discriminator to Zod cosmetic schemas
  • Deleted FlagButton.ts and PatternButton.ts
  • Added 320-line test suite for resolveCosmetics

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

evan

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR refactors the cosmetics system by introducing unified ResolvedCosmetic type and resolveCosmetics() function to normalize patterns and flags into a consistent shape, replacing separate PatternButton and FlagButton components with a single CosmeticButton component, and extracting pattern preview logic into a dedicated module. Updates applied across UI layers.

Changes

Cohort / File(s) Summary
Core Cosmetics Normalization
src/client/Cosmetics.ts
Adds ResolvedCosmetic type with type/cosmetic/colorPalette/relationship/key fields, plus resolveCosmetics() function to normalize patterns and flags, and resolvedToPlayerPattern() converter.
Component Consolidation
src/client/components/CosmeticButton.ts, src/client/components/PatternPreview.ts
Introduces new CosmeticButton unified component rendering patterns/flags via .resolved property. Extracts pattern preview rendering into separate PatternPreview module with renderPatternPreview() and generatePreviewDataUrl() functions.
Component Removal
src/client/components/PatternButton.ts, src/client/components/FlagButton.ts
Deletes pattern-button and flag-button custom elements and associated preview rendering logic.
Store Layer Refactor
src/client/Store.ts
Replaces manual pattern selection state and per-item relationship loops with resolveCosmetics() pipeline; switches UI to cosmetic-button with .resolved model; updates purchase wiring and selection comparison logic.
Modal Updates
src/client/TerritoryPatternsModal.ts, src/client/FlagInputModal.ts
Refactor pattern and flag selection to use resolveCosmetics(), render cosmetic-button instead of separate button types, and route selection through unified handlers.
Utility Updates
src/client/AccountModal.ts, src/client/Matchmaking.ts, src/client/TokenLoginModal.ts, src/client/PatternInput.ts
Remove side-effect imports of PatternButton; update import path for renderPatternPreview in PatternInput.
Graphics & Info
src/client/graphics/layers/WinModal.ts, src/client/components/CosmeticInfo.ts
Update WinModal.loadPatternContent() to use resolveCosmetics() and cosmetic-button. Add showAdFree property to CosmeticInfo to conditionally render ad-free tooltip.
Tests
tests/ResolveCosmetics.test.ts, tests/Privilege.test.ts
Add comprehensive test suite for resolveCosmetics() behavior (null handling, default pattern, palette expansion, relationship derivation, ordering). Update fixture with explicit type field.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • support for unlockable flags #3479: Modifies overlapping cosmetics/flag/pattern code in Store, modals, and button components; shares similar refactoring scope.
  • cosmetic refactor #3628: Implements the same cosmetics refactor pattern—adding ResolvedCosmetic, resolveCosmetics(), extracting PatternPreview, replacing button components with CosmeticButton, and updating all callers.

Poem

✨ Cosmetics unified, patterns and flags align,
One button to render them all, so fine!
Previews extracted, relationships clear,
The selector's new home is drawing near. 🎨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "cosmetic refactor" is vague and does not clearly summarize the main change. While it relates to cosmetics, it does not convey what the refactor accomplishes (e.g., unifying components or centralizing logic). Consider a more descriptive title like "Unify PatternButton and FlagButton into CosmeticButton" or "Refactor cosmetics to use single unified component and centralized resolveCosmetics function".
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the motivation, main changes (CosmeticButton, resolveCosmetics, PatternPreview extraction, type discriminators), deletions, and test coverage. It directly relates to the changeset and provides meaningful context.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/client/graphics/layers/WinModal.ts (1)

159-160: Fetch me and cosmetics in parallel here.

These calls are independent. Awaiting them one after the other adds a full extra network round trip before the modal can open.

Suggested fix
   async loadPatternContent() {
-    const me = await getUserMe();
-    const cosmetics = await fetchCosmetics();
+    const [me, cosmetics] = await Promise.all([
+      getUserMe(),
+      fetchCosmetics(),
+    ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/WinModal.ts` around lines 159 - 160, The code
awaits getUserMe() and then fetchCosmetics() sequentially, causing unnecessary
latency; change the implementation in WinModal.ts to run them in parallel (e.g.,
use Promise.all with the getUserMe and fetchCosmetics promises) and then
destructure the results into me and cosmetics so both requests happen
concurrently before continuing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/components/CosmeticInfo.ts`:
- Around line 25-26: The empty-state guard currently returns nothing even when
showAdFree is true, preventing the ad-free label/tooltip from rendering; update
the guard in the component's render/template logic (the check around the early
return at/near the lines referencing "nothing") so it only returns nothing when
there is truly no metadata AND showAdFree is false — i.e., include showAdFree in
the condition or invert the logic to allow rendering when showAdFree is true;
apply the same change to the second guard affecting the branch around lines
59-63 so the ad-free branch can render independently of other metadata.

In `@src/client/components/PatternPreview.ts`:
- Around line 17-22: The PatternPreview component currently always renders an
<img> with src set to generatePreviewDataUrl(pattern, width, height), which can
return an empty string on decode failure and produce a broken image; instead,
call generatePreviewDataUrl(...) and if it returns a falsy/empty string, return
renderBlankPreview() (preserving the same classes/attributes when rendering the
<img> case). Apply this same change to the second preview branch that currently
creates the <img> (the other occurrence referenced in the comment), replacing
usages that pass "" into src with a conditional that falls back to
renderBlankPreview().

In `@src/client/Store.ts`:
- Line 173: The code creates a new UserSettings instance for the flag lookup
instead of using the class property; change the call to use the existing
this.userSettings (i.e., use this.userSettings.getFlag() with the same nullish
fallback) so you don't instantiate a redundant UserSettings object and keep
lookups consistent; ensure this.userSettings is initialized before use and
preserve the default "" fallback.

---

Nitpick comments:
In `@src/client/graphics/layers/WinModal.ts`:
- Around line 159-160: The code awaits getUserMe() and then fetchCosmetics()
sequentially, causing unnecessary latency; change the implementation in
WinModal.ts to run them in parallel (e.g., use Promise.all with the getUserMe
and fetchCosmetics promises) and then destructure the results into me and
cosmetics so both requests happen concurrently before continuing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 693d3830-c4da-4739-ab49-7177d486434e

📥 Commits

Reviewing files that changed from the base of the PR and between f0b3c49 and b76165a.

📒 Files selected for processing (17)
  • src/client/AccountModal.ts
  • src/client/Cosmetics.ts
  • src/client/FlagInputModal.ts
  • src/client/Matchmaking.ts
  • src/client/PatternInput.ts
  • src/client/Store.ts
  • src/client/TerritoryPatternsModal.ts
  • src/client/TokenLoginModal.ts
  • src/client/components/CosmeticButton.ts
  • src/client/components/CosmeticInfo.ts
  • src/client/components/FlagButton.ts
  • src/client/components/PatternButton.ts
  • src/client/components/PatternPreview.ts
  • src/client/graphics/layers/WinModal.ts
  • src/core/CosmeticSchemas.ts
  • tests/Privilege.test.ts
  • tests/ResolveCosmetics.test.ts
💤 Files with no reviewable changes (5)
  • src/client/TokenLoginModal.ts
  • src/client/AccountModal.ts
  • src/client/Matchmaking.ts
  • src/client/components/FlagButton.ts
  • src/client/components/PatternButton.ts

Comment thread src/client/components/CosmeticInfo.ts
Comment on lines +17 to +22
return html`<img
src="${generatePreviewDataUrl(pattern, width, height)}"
alt="Pattern preview"
class="w-full h-full object-contain [image-rendering:pixelated] pointer-events-none"
draggable="false"
/>`;
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.

⚠️ Potential issue | 🟡 Minor

Avoid rendering <img src=""> on preview decode failures.

generatePreviewDataUrl() already treats decode errors as recoverable, but this branch still feeds "" into src. That usually degrades into a broken image or a request back to the current document. Fall back to renderBlankPreview() instead.

Suggested fix
 export function renderPatternPreview(
   pattern: PlayerPattern | null,
   width: number,
   height: number,
 ): TemplateResult {
   if (pattern === null) {
     return renderBlankPreview();
   }
+  const src = generatePreviewDataUrl(pattern, width, height);
+  if (!src) {
+    return renderBlankPreview();
+  }
   return html`<img
-    src="${generatePreviewDataUrl(pattern, width, height)}"
+    src="${src}"
     alt="Pattern preview"
     class="w-full h-full object-contain [image-rendering:pixelated] pointer-events-none"
     draggable="false"
   />`;
 }

Also applies to: 83-85

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/components/PatternPreview.ts` around lines 17 - 22, The
PatternPreview component currently always renders an <img> with src set to
generatePreviewDataUrl(pattern, width, height), which can return an empty string
on decode failure and produce a broken image; instead, call
generatePreviewDataUrl(...) and if it returns a falsy/empty string, return
renderBlankPreview() (preserving the same classes/attributes when rendering the
<img> case). Apply this same change to the second preview branch that currently
creates the <img> (the other occurrence referenced in the comment), replacing
usages that pass "" into src with a conditional that falls back to
renderBlankPreview().

Comment thread src/client/Store.ts
</div>`;
}

const selectedFlag = new UserSettings().getFlag() ?? "";
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.

⚠️ Potential issue | 🟡 Minor

Use existing this.userSettings instead of creating a new instance.

Line 173 creates new UserSettings() but the class already has this.userSettings as a property (line 32). Using the existing instance avoids redundant object creation and keeps flag lookups consistent.

Proposed fix
-    const selectedFlag = new UserSettings().getFlag() ?? "";
+    const selectedFlag = this.userSettings.getFlag() ?? "";
📝 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
const selectedFlag = new UserSettings().getFlag() ?? "";
const selectedFlag = this.userSettings.getFlag() ?? "";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/Store.ts` at line 173, The code creates a new UserSettings
instance for the flag lookup instead of using the class property; change the
call to use the existing this.userSettings (i.e., use
this.userSettings.getFlag() with the same nullish fallback) so you don't
instantiate a redundant UserSettings object and keep lookups consistent; ensure
this.userSettings is initialized before use and preserve the default ""
fallback.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/client/graphics/layers/WinModal.ts (1)

182-183: Non-null assertions are safe but could be more explicit.

rc.cosmetic!.product! relies on purchasable patterns always having non-null cosmetic and product. This is true by design (default pattern is always "owned"), but a guard would make it clearer for future readers.

Optional: Add early-continue guard
         ${selected.map(
-          (r) => html`
+          (r) => {
+            if (!r.cosmetic?.product) return nothing;
+            return html`
             <cosmetic-button
               .resolved=${r}
               .onPurchase=${(rc: ResolvedCosmetic) =>
-                handlePurchase(rc.cosmetic!.product!, rc.colorPalette?.name)}
+                handlePurchase(rc.cosmetic!.product!, rc.colorPalette?.name)}
             ></cosmetic-button>
-          `,
+          `;
+          }
         )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/WinModal.ts` around lines 182 - 183, Replace the
chained non-null assertions in the purchase handler with an explicit guard to
make assumptions clear: before calling handlePurchase, verify that the
ResolvedCosmetic instance has a cosmetic and that cosmetic.product exists (e.g.,
check rc.cosmetic && rc.cosmetic.product) and only then invoke
handlePurchase(rc.cosmetic.product, rc.colorPalette?.name); reference the
onPurchase attribute where rc is typed as ResolvedCosmetic and the
handlePurchase function to locate the change and consider an early
return/continue or conditional noop to keep behavior identical if either value
is missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/components/PatternPreview.ts`:
- Around line 100-104: The code in PatternPreview that creates a canvas and does
const ctx = canvas.getContext("2d") should not throw when ctx is null (e.g.,
SSR/workers); instead handle the null like other error paths by returning an
empty string. Replace the throw in the block that checks ctx with an early
return of "" (or the component's existing empty-string error return path),
ensuring the function/component (PatternPreview) continues to return "" when ctx
is null and does not crash non-browser environments.

In `@src/client/Cosmetics.ts`:
- Around line 232-234: The lookup for resolvedPalette using cp and
cosmetics.colorPalettes can yield null silently when
cosmetics.colorPalettes?.[cp.name] is undefined; update the code in the
Cosmetics class where resolvedPalette is computed to explicitly handle the
missing-key case: check if cp is non-null and the lookup returns undefined, then
either (a) log a warning including cp.name and cp.isArchived via the module
logger/console, and set resolvedPalette to a safe fallback (e.g., cp-derived
default or empty palette) or (b) throw/return early if a palette is required;
reference the resolvedPalette variable, the cp object and its name (cp.name),
and cosmetics.colorPalettes to locate and change the logic.

---

Nitpick comments:
In `@src/client/graphics/layers/WinModal.ts`:
- Around line 182-183: Replace the chained non-null assertions in the purchase
handler with an explicit guard to make assumptions clear: before calling
handlePurchase, verify that the ResolvedCosmetic instance has a cosmetic and
that cosmetic.product exists (e.g., check rc.cosmetic && rc.cosmetic.product)
and only then invoke handlePurchase(rc.cosmetic.product, rc.colorPalette?.name);
reference the onPurchase attribute where rc is typed as ResolvedCosmetic and the
handlePurchase function to locate the change and consider an early
return/continue or conditional noop to keep behavior identical if either value
is missing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 80a6b35d-ebe1-4637-b8e9-a9cffd82a76f

📥 Commits

Reviewing files that changed from the base of the PR and between b76165a and a545e16.

📒 Files selected for processing (17)
  • src/client/AccountModal.ts
  • src/client/Cosmetics.ts
  • src/client/FlagInputModal.ts
  • src/client/Matchmaking.ts
  • src/client/PatternInput.ts
  • src/client/Store.ts
  • src/client/TerritoryPatternsModal.ts
  • src/client/TokenLoginModal.ts
  • src/client/components/CosmeticButton.ts
  • src/client/components/CosmeticInfo.ts
  • src/client/components/FlagButton.ts
  • src/client/components/PatternButton.ts
  • src/client/components/PatternPreview.ts
  • src/client/graphics/layers/WinModal.ts
  • src/core/CosmeticSchemas.ts
  • tests/Privilege.test.ts
  • tests/ResolveCosmetics.test.ts
💤 Files with no reviewable changes (5)
  • src/client/TokenLoginModal.ts
  • src/client/AccountModal.ts
  • src/client/Matchmaking.ts
  • src/client/components/FlagButton.ts
  • src/client/components/PatternButton.ts
✅ Files skipped from review due to trivial changes (4)
  • src/client/PatternInput.ts
  • tests/Privilege.test.ts
  • src/core/CosmeticSchemas.ts
  • tests/ResolveCosmetics.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/Store.ts

Comment on lines +100 to +104
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("2D context not supported");
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.

⚠️ Potential issue | 🟡 Minor

Throwing on missing 2D context may crash in non-browser environments.

ctx.getContext("2d") can return null in workers or SSR. Throwing here is abrupt. Consider returning "" like other error paths.

Suggested fix
   const ctx = canvas.getContext("2d");
-  if (!ctx) throw new Error("2D context not supported");
+  if (!ctx) {
+    console.error("2D context not supported");
+    return "";
+  }
📝 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
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("2D context not supported");
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
if (!ctx) {
console.error("2D context not supported");
return "";
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/components/PatternPreview.ts` around lines 100 - 104, The code in
PatternPreview that creates a canvas and does const ctx =
canvas.getContext("2d") should not throw when ctx is null (e.g., SSR/workers);
instead handle the null like other error paths by returning an empty string.
Replace the throw in the block that checks ctx with an early return of "" (or
the component's existing empty-string error return path), ensuring the
function/component (PatternPreview) continues to return "" when ctx is null and
does not crash non-browser environments.

Comment thread src/client/Cosmetics.ts
@evanpelle evanpelle marked this pull request as ready for review April 10, 2026 03:47
@evanpelle evanpelle force-pushed the main-cosmetics-refactor branch from a545e16 to 947a3f9 Compare April 10, 2026 03:48
@evanpelle evanpelle added this to the v31 milestone Apr 10, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/client/Cosmetics.ts (1)

232-244: ⚠️ Potential issue | 🟠 Major

Do not emit a colored variant after its palette lookup failed.

If cp exists but cosmetics.colorPalettes?.[cp.name] is missing, this still returns a resolved entry with relationship for the colored variant but colorPalette: null. Downstream code then previews/selects the base pattern and drops the palette name from checkout.

Proposed fix
       const resolvedPalette = cp
         ? (cosmetics.colorPalettes?.[cp.name] ?? null)
         : null;
+      if (cp && !resolvedPalette) {
+        console.warn(
+          `Missing color palette "${cp.name}" for pattern "${patternKey}"`,
+        );
+        continue;
+      }
       const key = cp
         ? `pattern:${patternKey}:${cp.name}`
         : `pattern:${patternKey}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/Cosmetics.ts` around lines 232 - 244, The current code pushes a
colored variant even when the palette lookup fails (cp exists but
cosmetics.colorPalettes?.[cp.name] is undefined), resulting in colorPalette:
null; update the logic around cp/resolvedPalette so that you only push the
pattern entry when either cp is null (base variant) or when cp is present AND
resolvedPalette is non-null; reference the variables cp, resolvedPalette,
cosmetics.colorPalettes, pattern, patternKey and the result.push call — skip
emitting the colored variant if resolvedPalette is null to avoid downstream
preview/checkout issues.
src/client/components/PatternPreview.ts (2)

17-22: ⚠️ Potential issue | 🟡 Minor

Fall back to the blank preview when URL generation fails.

generatePreviewDataUrl(...) already returns "" on decode errors. Rendering <img src=""> here gives a broken preview instead of the existing blank state.

Proposed fix
 export function renderPatternPreview(
   pattern: PlayerPattern | null,
   width: number,
   height: number,
 ): TemplateResult {
   if (pattern === null) {
     return renderBlankPreview();
   }
+  const src = generatePreviewDataUrl(pattern, width, height);
+  if (!src) {
+    return renderBlankPreview();
+  }
   return html`<img
-    src="${generatePreviewDataUrl(pattern, width, height)}"
+    src="${src}"
     alt="Pattern preview"
     class="w-full h-full object-contain [image-rendering:pixelated] pointer-events-none"
     draggable="false"
   />`;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/components/PatternPreview.ts` around lines 17 - 22, The
PatternPreview component currently renders an <img> with src set from
generatePreviewDataUrl(pattern, width, height) even when that helper returns an
empty string; change PatternPreview to check the returned dataUrl first and only
render the <img> when dataUrl is non-empty, otherwise render the existing blank
preview placeholder (the same element shown when no pattern exists) so you avoid
creating a broken <img src="">; locate this logic in PatternPreview and use
generatePreviewDataUrl(...) result to branch the render.

100-104: ⚠️ Potential issue | 🟡 Minor

Handle missing 2D canvas context like the other recoverable preview errors.

Throwing here can crash tests or non-browser-like environments. This helper already uses "" as the safe failure result, so the null-context case should do the same.

Proposed fix
   const ctx = canvas.getContext("2d");
-  if (!ctx) throw new Error("2D context not supported");
+  if (!ctx) {
+    console.error("2D context not supported");
+    return "";
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/components/PatternPreview.ts` around lines 100 - 104, In the
canvas setup in PatternPreview.ts where you call canvas.getContext("2d") and
assign to ctx, avoid throwing on a null 2D context; instead handle it like other
recoverable preview errors by returning the safe failure result (an empty
string) from the surrounding helper (the function that produces the preview
image) rather than throwing; locate the canvas creation and the ctx null check
(the canvas.getContext("2d") call and the if (!ctx) branch) and replace the
throw new Error(...) with code that returns "" so tests and non-browser
environments don't crash.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/components/CosmeticButton.ts`:
- Around line 29-31: handleClick currently always calls onSelect(this.resolved)
which routes purchasable items into selection; change handleClick to branch on
this.resolved.relationship: when relationship === 'purchasable' do not call
onSelect but instead invoke a dedicated purchase handler (e.g., call a new prop
like onPurchase or onStartCheckout with this.resolved) to start checkout, and
when relationship is anything else call this.onSelect?.(this.resolved); update
the component props to accept and use that new purchase handler (and ensure
callers wired to start checkout in the store).

---

Duplicate comments:
In `@src/client/components/PatternPreview.ts`:
- Around line 17-22: The PatternPreview component currently renders an <img>
with src set from generatePreviewDataUrl(pattern, width, height) even when that
helper returns an empty string; change PatternPreview to check the returned
dataUrl first and only render the <img> when dataUrl is non-empty, otherwise
render the existing blank preview placeholder (the same element shown when no
pattern exists) so you avoid creating a broken <img src="">; locate this logic
in PatternPreview and use generatePreviewDataUrl(...) result to branch the
render.
- Around line 100-104: In the canvas setup in PatternPreview.ts where you call
canvas.getContext("2d") and assign to ctx, avoid throwing on a null 2D context;
instead handle it like other recoverable preview errors by returning the safe
failure result (an empty string) from the surrounding helper (the function that
produces the preview image) rather than throwing; locate the canvas creation and
the ctx null check (the canvas.getContext("2d") call and the if (!ctx) branch)
and replace the throw new Error(...) with code that returns "" so tests and
non-browser environments don't crash.

In `@src/client/Cosmetics.ts`:
- Around line 232-244: The current code pushes a colored variant even when the
palette lookup fails (cp exists but cosmetics.colorPalettes?.[cp.name] is
undefined), resulting in colorPalette: null; update the logic around
cp/resolvedPalette so that you only push the pattern entry when either cp is
null (base variant) or when cp is present AND resolvedPalette is non-null;
reference the variables cp, resolvedPalette, cosmetics.colorPalettes, pattern,
patternKey and the result.push call — skip emitting the colored variant if
resolvedPalette is null to avoid downstream preview/checkout issues.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6ebe7bab-7a02-49dd-a386-2b403f1633a2

📥 Commits

Reviewing files that changed from the base of the PR and between a545e16 and 947a3f9.

📒 Files selected for processing (17)
  • src/client/AccountModal.ts
  • src/client/Cosmetics.ts
  • src/client/FlagInputModal.ts
  • src/client/Matchmaking.ts
  • src/client/PatternInput.ts
  • src/client/Store.ts
  • src/client/TerritoryPatternsModal.ts
  • src/client/TokenLoginModal.ts
  • src/client/components/CosmeticButton.ts
  • src/client/components/CosmeticInfo.ts
  • src/client/components/FlagButton.ts
  • src/client/components/PatternButton.ts
  • src/client/components/PatternPreview.ts
  • src/client/graphics/layers/WinModal.ts
  • src/core/CosmeticSchemas.ts
  • tests/Privilege.test.ts
  • tests/ResolveCosmetics.test.ts
💤 Files with no reviewable changes (5)
  • src/client/TokenLoginModal.ts
  • src/client/Matchmaking.ts
  • src/client/AccountModal.ts
  • src/client/components/FlagButton.ts
  • src/client/components/PatternButton.ts
✅ Files skipped from review due to trivial changes (4)
  • tests/Privilege.test.ts
  • src/core/CosmeticSchemas.ts
  • src/client/PatternInput.ts
  • tests/ResolveCosmetics.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/components/CosmeticInfo.ts

Comment on lines +29 to +31
private handleClick() {
this.onSelect?.(this.resolved);
}
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.

⚠️ Potential issue | 🟠 Major

Route clicks by relationship, not always through selection.

Right now a normal click always calls onSelect. In the store, that means a purchasable cosmetic can be “selected” and close the modal instead of starting checkout. This component already has resolved.relationship, so it should enforce the split here.

Proposed fix
   private handleClick() {
-    this.onSelect?.(this.resolved);
+    if (this.resolved.relationship === "purchasable") {
+      this.onPurchase?.(this.resolved);
+      return;
+    }
+    this.onSelect?.(this.resolved);
   }
📝 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
private handleClick() {
this.onSelect?.(this.resolved);
}
private handleClick() {
if (this.resolved.relationship === "purchasable") {
this.onPurchase?.(this.resolved);
return;
}
this.onSelect?.(this.resolved);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/components/CosmeticButton.ts` around lines 29 - 31, handleClick
currently always calls onSelect(this.resolved) which routes purchasable items
into selection; change handleClick to branch on this.resolved.relationship: when
relationship === 'purchasable' do not call onSelect but instead invoke a
dedicated purchase handler (e.g., call a new prop like onPurchase or
onStartCheckout with this.resolved) to start checkout, and when relationship is
anything else call this.onSelect?.(this.resolved); update the component props to
accept and use that new purchase handler (and ensure callers wired to start
checkout in the store).

@evanpelle evanpelle force-pushed the main-cosmetics-refactor branch from 2273837 to ff7b9a2 Compare April 10, 2026 04:05
@evanpelle evanpelle merged commit d5a2cc0 into main Apr 10, 2026
10 checks passed
@evanpelle evanpelle deleted the main-cosmetics-refactor branch April 10, 2026 04:07
@github-project-automation github-project-automation Bot moved this from Development to Complete in OpenFront Release Management Apr 10, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/client/Cosmetics.ts (1)

232-244: ⚠️ Potential issue | 🟠 Major

Do not emit a keyed palette variant when the palette lookup fails.

If cp exists but cosmetics.colorPalettes[cp.name] is missing, this still returns pattern:${patternKey}:${cp.name} with colorPalette: null. Callers then preview it without colors and pass undefined for colorPaletteName at checkout, which can target the wrong variant.

🛠️ Proposed fix
       const resolvedPalette = cp
-        ? (cosmetics.colorPalettes?.[cp.name] ?? null)
+        ? cosmetics.colorPalettes?.[cp.name]
         : null;
+      if (cp && !resolvedPalette) {
+        console.warn(
+          `Missing color palette "${cp.name}" for pattern "${patternKey}"`,
+        );
+        continue;
+      }
       const key = cp
         ? `pattern:${patternKey}:${cp.name}`
         : `pattern:${patternKey}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/Cosmetics.ts` around lines 232 - 244, The code currently builds a
keyed variant even when the palette lookup fails (cp exists but
cosmetics.colorPalettes[cp.name] is missing); change the emission logic so you
only produce a keyed variant when the lookup succeeds. In the block that
computes resolvedPalette and key (using cp, resolvedPalette,
cosmetics.colorPalettes, patternKey and result.push), if cp is truthy but
resolvedPalette is null/undefined, fall back to the unkeyed form
(`pattern:${patternKey}`) (or skip emitting a palette-specific entry) and ensure
colorPalette remains unset/null; only use the `pattern:${patternKey}:${cp.name}`
key when resolvedPalette is present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/components/CosmeticButton.ts`:
- Around line 65-70: In CosmeticButton's image error handler (the `@error`
callback), replace the hardcoded fallback path "/flags/xx.svg" with the result
of calling assetUrl('flags/xx.svg') so the fallback uses the same asset base
resolution as the primary flag; update the handler that currently uses img.src =
fallback to compute const fallback = assetUrl('flags/xx.svg') and assign that
value, ensuring the error handler references the same assetUrl function used
elsewhere in the component.

---

Duplicate comments:
In `@src/client/Cosmetics.ts`:
- Around line 232-244: The code currently builds a keyed variant even when the
palette lookup fails (cp exists but cosmetics.colorPalettes[cp.name] is
missing); change the emission logic so you only produce a keyed variant when the
lookup succeeds. In the block that computes resolvedPalette and key (using cp,
resolvedPalette, cosmetics.colorPalettes, patternKey and result.push), if cp is
truthy but resolvedPalette is null/undefined, fall back to the unkeyed form
(`pattern:${patternKey}`) (or skip emitting a palette-specific entry) and ensure
colorPalette remains unset/null; only use the `pattern:${patternKey}:${cp.name}`
key when resolvedPalette is present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 828fa498-d3fc-4e2c-ad11-b7a7ed787a78

📥 Commits

Reviewing files that changed from the base of the PR and between 947a3f9 and 2273837.

📒 Files selected for processing (17)
  • src/client/AccountModal.ts
  • src/client/Cosmetics.ts
  • src/client/FlagInputModal.ts
  • src/client/Matchmaking.ts
  • src/client/PatternInput.ts
  • src/client/Store.ts
  • src/client/TerritoryPatternsModal.ts
  • src/client/TokenLoginModal.ts
  • src/client/components/CosmeticButton.ts
  • src/client/components/CosmeticInfo.ts
  • src/client/components/FlagButton.ts
  • src/client/components/PatternButton.ts
  • src/client/components/PatternPreview.ts
  • src/client/graphics/layers/WinModal.ts
  • src/core/CosmeticSchemas.ts
  • tests/Privilege.test.ts
  • tests/ResolveCosmetics.test.ts
💤 Files with no reviewable changes (5)
  • src/client/TokenLoginModal.ts
  • src/client/Matchmaking.ts
  • src/client/AccountModal.ts
  • src/client/components/FlagButton.ts
  • src/client/components/PatternButton.ts
✅ Files skipped from review due to trivial changes (3)
  • tests/Privilege.test.ts
  • src/core/CosmeticSchemas.ts
  • src/client/PatternInput.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/client/components/CosmeticInfo.ts
  • src/client/TerritoryPatternsModal.ts
  • src/client/components/PatternPreview.ts

Comment on lines +65 to +70
@error=${(e: Event) => {
const img = e.currentTarget as HTMLImageElement;
const fallback = "/flags/xx.svg";
if (img.src && !img.src.endsWith(fallback)) {
img.src = fallback;
}
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.

⚠️ Potential issue | 🟡 Minor

Use assetUrl() for the fallback flag image too.

The main flag URL can come from assetUrl(...), but the error handler falls back to a raw root path. On non-root deployments the first URL can work while the fallback still 404s.

🛠️ Proposed fix
+import { assetUrl } from "../../core/AssetUrls";
 import { Flag, Pattern } from "../../core/CosmeticSchemas";
 ...
-        const fallback = "/flags/xx.svg";
+        const fallback = assetUrl("flags/xx.svg");
📝 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
@error=${(e: Event) => {
const img = e.currentTarget as HTMLImageElement;
const fallback = "/flags/xx.svg";
if (img.src && !img.src.endsWith(fallback)) {
img.src = fallback;
}
`@error`=${(e: Event) => {
const img = e.currentTarget as HTMLImageElement;
const fallback = assetUrl("flags/xx.svg");
if (img.src && !img.src.endsWith(fallback)) {
img.src = fallback;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/components/CosmeticButton.ts` around lines 65 - 70, In
CosmeticButton's image error handler (the `@error` callback), replace the
hardcoded fallback path "/flags/xx.svg" with the result of calling
assetUrl('flags/xx.svg') so the fallback uses the same asset base resolution as
the primary flag; update the handler that currently uses img.src = fallback to
compute const fallback = assetUrl('flags/xx.svg') and assign that value,
ensuring the error handler references the same assetUrl function used elsewhere
in the component.

@github-project-automation github-project-automation Bot moved this from Complete to Development in OpenFront Release Management Apr 10, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
src/client/components/PatternPreview.ts (2)

17-22: ⚠️ Potential issue | 🟡 Minor

Handle preview generation failure before rendering <img>.

generatePreviewDataUrl(...) can return "" (Line 85). Rendering src="" still creates a broken image path; fallback to renderBlankPreview() instead.

Suggested fix
 export function renderPatternPreview(
   pattern: PlayerPattern | null,
   width: number,
   height: number,
 ): TemplateResult {
   if (pattern === null) {
     return renderBlankPreview();
   }
+  const src = generatePreviewDataUrl(pattern, width, height);
+  if (!src) {
+    return renderBlankPreview();
+  }
   return html`<img
-    src="${generatePreviewDataUrl(pattern, width, height)}"
+    src="${src}"
     alt="Pattern preview"
     class="w-full h-full object-contain [image-rendering:pixelated] pointer-events-none"
     draggable="false"
   />`;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/components/PatternPreview.ts` around lines 17 - 22, In
PatternPreview, avoid rendering an <img> with an empty src by calling
generatePreviewDataUrl(pattern, width, height) into a variable and, if it
returns an empty string, call renderBlankPreview() instead of rendering the
<img>; otherwise render the <img> with the generated data URL. Update the render
path in the component (where generatePreviewDataUrl is currently inlined) to
branch on the returned string and use renderBlankPreview() as the fallback.

100-104: ⚠️ Potential issue | 🟡 Minor

Avoid throwing when 2D context is unavailable.

Returning "" matches your existing recoverable error path and avoids hard crashes in unsupported environments.

Suggested fix
   const ctx = canvas.getContext("2d");
-  if (!ctx) throw new Error("2D context not supported");
+  if (!ctx) {
+    console.error("2D context not supported");
+    return "";
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/components/PatternPreview.ts` around lines 100 - 104, In the
PatternPreview rendering code where you create a canvas and call
getContext("2d") (variables canvas, canvas.width/height, ctx), avoid throwing
when ctx is null; instead follow the existing recoverable error path and return
an empty string ("") so unsupported environments don't crash—replace the `if
(!ctx) throw new Error("2D context not supported")` with logic that returns ""
(and optionally logs a debug message) from the surrounding function.
src/client/components/CosmeticButton.ts (2)

29-31: ⚠️ Potential issue | 🟠 Major

Route click by relationship instead of always selecting.

Right now a normal click always calls onSelect, so purchasable cosmetics do not reliably start checkout from button click.

Suggested fix
   private handleClick() {
-    this.onSelect?.(this.resolved);
+    if (this.resolved.relationship === "purchasable") {
+      this.onPurchase?.(this.resolved);
+      return;
+    }
+    this.onSelect?.(this.resolved);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/components/CosmeticButton.ts` around lines 29 - 31, The click
handler always calls onSelect(this.resolved); change handleClick to route based
on this.resolved.relationship: if relationship === 'purchasable' call a
purchase/checkout handler (e.g. this.onPurchase?.(this.resolved) or
this.onStartCheckout?.(this.resolved)), otherwise call
this.onSelect?.(this.resolved); update the component props to accept the new
handler name you choose and ensure existing purchasers use it.

65-70: ⚠️ Potential issue | 🟡 Minor

Use assetUrl() for the fallback flag path.

Hardcoding "/flags/xx.svg" can break when assets are served from a non-root base path.

Suggested fix
+import { assetUrl } from "../../core/AssetUrls";
 import { Flag, Pattern } from "../../core/CosmeticSchemas";
 ...
-        const fallback = "/flags/xx.svg";
+        const fallback = assetUrl("flags/xx.svg");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/components/CosmeticButton.ts` around lines 65 - 70, In
CosmeticButton's image error handler replace the hardcoded fallback
"/flags/xx.svg" with the asset helper so the path respects the app base: set
img.src = assetUrl('/flags/xx.svg') (and add the corresponding import for
assetUrl from your project's asset helper/Lit static helper). Update the `@error`
handler code in CosmeticButton.ts to use assetUrl('/flags/xx.svg') instead of
the literal string and ensure the assetUrl import is added at the top of the
file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/components/PatternPreview.ts`:
- Around line 88-89: PatternPreview currently calls decoder.scaledWidth() and
decoder.scaledHeight(), but PatternDecoder exposes width, height and scale
fields; replace those calls by computing scaled sizes from decoder.width,
decoder.height and decoder.scale (e.g. scaledWidth = Math.round(decoder.width *
decoder.scale) and scaledHeight = Math.round(decoder.height * decoder.scale))
and update any downstream uses to accept these numeric values instead of method
calls (references: PatternPreview's use of
decoder.scaledWidth/decoder.scaledHeight and PatternDecoder's width/height/scale
fields).

---

Duplicate comments:
In `@src/client/components/CosmeticButton.ts`:
- Around line 29-31: The click handler always calls onSelect(this.resolved);
change handleClick to route based on this.resolved.relationship: if relationship
=== 'purchasable' call a purchase/checkout handler (e.g.
this.onPurchase?.(this.resolved) or this.onStartCheckout?.(this.resolved)),
otherwise call this.onSelect?.(this.resolved); update the component props to
accept the new handler name you choose and ensure existing purchasers use it.
- Around line 65-70: In CosmeticButton's image error handler replace the
hardcoded fallback "/flags/xx.svg" with the asset helper so the path respects
the app base: set img.src = assetUrl('/flags/xx.svg') (and add the corresponding
import for assetUrl from your project's asset helper/Lit static helper). Update
the `@error` handler code in CosmeticButton.ts to use assetUrl('/flags/xx.svg')
instead of the literal string and ensure the assetUrl import is added at the top
of the file.

In `@src/client/components/PatternPreview.ts`:
- Around line 17-22: In PatternPreview, avoid rendering an <img> with an empty
src by calling generatePreviewDataUrl(pattern, width, height) into a variable
and, if it returns an empty string, call renderBlankPreview() instead of
rendering the <img>; otherwise render the <img> with the generated data URL.
Update the render path in the component (where generatePreviewDataUrl is
currently inlined) to branch on the returned string and use renderBlankPreview()
as the fallback.
- Around line 100-104: In the PatternPreview rendering code where you create a
canvas and call getContext("2d") (variables canvas, canvas.width/height, ctx),
avoid throwing when ctx is null; instead follow the existing recoverable error
path and return an empty string ("") so unsupported environments don't
crash—replace the `if (!ctx) throw new Error("2D context not supported")` with
logic that returns "" (and optionally logs a debug message) from the surrounding
function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 845fcbf5-2e11-4067-a33a-ed48cdd33b1e

📥 Commits

Reviewing files that changed from the base of the PR and between 2273837 and ff7b9a2.

📒 Files selected for processing (16)
  • src/client/AccountModal.ts
  • src/client/Cosmetics.ts
  • src/client/FlagInputModal.ts
  • src/client/Matchmaking.ts
  • src/client/PatternInput.ts
  • src/client/Store.ts
  • src/client/TerritoryPatternsModal.ts
  • src/client/TokenLoginModal.ts
  • src/client/components/CosmeticButton.ts
  • src/client/components/CosmeticInfo.ts
  • src/client/components/FlagButton.ts
  • src/client/components/PatternButton.ts
  • src/client/components/PatternPreview.ts
  • src/client/graphics/layers/WinModal.ts
  • tests/Privilege.test.ts
  • tests/ResolveCosmetics.test.ts
💤 Files with no reviewable changes (5)
  • src/client/Matchmaking.ts
  • src/client/TokenLoginModal.ts
  • src/client/AccountModal.ts
  • src/client/components/FlagButton.ts
  • src/client/components/PatternButton.ts
✅ Files skipped from review due to trivial changes (2)
  • tests/Privilege.test.ts
  • src/client/PatternInput.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/client/components/CosmeticInfo.ts
  • src/client/TerritoryPatternsModal.ts
  • src/client/Store.ts

Comment on lines +88 to +89
const scaledWidth = decoder.scaledWidth();
const scaledHeight = decoder.scaledHeight();
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.

⚠️ Potential issue | 🔴 Critical

Fix scaled size computation; PatternDecoder does not expose these methods.

This code calls decoder.scaledWidth() / decoder.scaledHeight(), but src/core/PatternDecoder.ts exposes width, height, and scale fields. This is a blocker if left as-is.

Suggested fix
-  const scaledWidth = decoder.scaledWidth();
-  const scaledHeight = decoder.scaledHeight();
+  const scaledWidth = decoder.width << decoder.scale;
+  const scaledHeight = decoder.height << decoder.scale;
📝 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
const scaledWidth = decoder.scaledWidth();
const scaledHeight = decoder.scaledHeight();
const scaledWidth = decoder.width << decoder.scale;
const scaledHeight = decoder.height << decoder.scale;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/components/PatternPreview.ts` around lines 88 - 89, PatternPreview
currently calls decoder.scaledWidth() and decoder.scaledHeight(), but
PatternDecoder exposes width, height and scale fields; replace those calls by
computing scaled sizes from decoder.width, decoder.height and decoder.scale
(e.g. scaledWidth = Math.round(decoder.width * decoder.scale) and scaledHeight =
Math.round(decoder.height * decoder.scale)) and update any downstream uses to
accept these numeric values instead of method calls (references:
PatternPreview's use of decoder.scaledWidth/decoder.scaledHeight and
PatternDecoder's width/height/scale fields).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

1 participant