fix(showcase): responsive card, full localization, in-game stat icons and clean PNG export

Layout: card now fills its container (3/2/1 columns) with no horizontal scrollbar.

i18n: every modal/showcase/console string follows the selected language, named fallbacks for th/en plus English fallback for other locales, localized character names and light-cone passive text.

Showcase stats use the official in-game property icons (same set as the overview tab) instead of emoji, including the element DMG icon.

Export: fixed 1200px clone with poster metrics, cover-cropped splash art (html2canvas ignores object-fit), transparent background so the rounded corners have no dark wedges, and a two-pass measurement inside the capture iframe so the PNG is never cropped.
This commit is contained in:
CAtlas0784 committed 2026-11-01 14:49:56 +07:00
1 parent d9b7a4ce1b
commit 76986eed4a
9 files changed
+775 -187

No files matched your search

+207
View File
@@ -7044,6 +7044,213 @@ for (const [language, description] of Object.entries(ITEM_CATALOG_INTERACTION_DE
TRANSLATIONS[language].itemCatalogDescription = description;
}
// Strings that used to be hardcoded in the character modal, showcase card,
// dashboard console and camera panel. Only th/en are defined here; every other
// language falls back to en through t(), so no Thai leaks into other locales.
const MODAL_AND_TOOL_TRANSLATIONS = {
th: {
relicSlotNotEquipped: 'ยังไม่ได้สวมใส่',
relicSlotCreateHint: '+ คลิกเพื่อเลือกหรือสร้าง (แนะนำ: {stat})',
relicSlotUnequip: 'ถอดชิ้นนี้',
relicSubstatFallback: '4 ออปชั่นรอง (คลิกเพื่อปรับแต่ง)',
relicSetNoBonus: 'ยังไม่มีเอฟเฟกต์เซ็ตทำงาน (ต้องใส่เซ็ตเดียวกันอย่างน้อย 2 หรือ 4 ชิ้น)',
relicSet4Fallback: 'เปิดใช้งานเอฟเฟกต์ครบ 4 ชิ้น',
relicSet2Fallback: 'เปิดใช้งานเอฟเฟกต์ 2 ชิ้น',
overviewRelicCount: '{count}/6 ชิ้นสวมใส่ (ยังไม่มีโบนัสเซ็ต)',
relicPickerEditTitle: '🔧 แก้ไขรีลิกส์: {set} ({slot}) - {name}',
relicPickerEditSubtitle: 'ปรับแต่งเซ็ต สเตตัสหลัก และออฟรองของรีลิกส์ชิ้นนี้ (#{id})',
relicPickerEditing: '✏️ กำลังแก้ไขรีลิกส์ #{id}',
relicPickerSaveChanges: '💾 บันทึกการแก้ไขออฟรองชิ้นนี้',
relicPickerEquipNow: '⚡ สวมใส่ชิ้นนี้ทันที (+15 5★ ออฟที่เลือก)',
relicPickerInInventory: 'ชิ้นในคลัง',
relicPickerEditThis: '✏️ แก้ไขออฟรองชิ้นนี้',
relicPickerEditThisTitle: 'โหลดข้อมูลชิ้นนี้ลงมาแก้ไขออฟรอง',
relicPickerRecommended: 'ออฟรองแนะนำ 4 แถวเทพ ปรับแต่งได้ตามต้องการด้านล่าง',
substatMainConflict: ' (ตรงกับ Main Stat)',
substatAlreadyChosen: ' (เลือกแล้ว)',
substatSlotLabel: 'ออฟรอง {n}',
substatRolls: '+{value} ({count} โรล)',
substatDecreaseRoll: 'ลดโรล',
substatIncreaseRoll: 'เพิ่มโรล',
relicEmptyInventoryTitle: 'ไม่มีรีลิกส์สำหรับช่องนี้ในคลัง',
relicEmptyInventoryHint: 'สามารถสลับไปที่แท็บ "⚡ สวมใส่ / สร้างชิ้นแนะนำ" เพื่อสร้างชิ้นใหม่ได้ทันที!',
relicEquippedNow: 'กำลังสวมใส่อยู่',
relicWornBy: 'สวมใส่โดย: {name}',
relicFreeInInventory: 'ว่างในคลัง',
relicInUse: 'ใช้งานอยู่',
relicSelectThis: 'เลือกชิ้นนี้',
lcNoneEquipped: 'ยังไม่ได้สวมใส่ Light Cone',
lcSelectPrompt: 'กรุณาเลือก Light Cone ที่ต้องการสวมใส่',
lcSamePathGroup: '⭐ Light Cones สาย {path}',
lcOtherPathGroup: '🗡️ Light Cones สายอื่นๆ',
lcNoResults: 'ไม่พบกรวยแสงตามเงื่อนไขที่ค้นหา',
lcEquippedBadge: 'สวมใส่อยู่',
lcSignatureBadge: 'ประจำตัว',
lcSelectTitle: '🗡️ เลือกสวมใส่กรวยแสง: {name}',
eqSkillFallbackTitle: 'สกิลพิเศษประจำกรวยแสง',
eqSkillFallbackDesc: 'เพิ่มพลังต่อสู้และเสริมกลไกเฉพาะสายของตัวละครเมื่อติดตั้ง',
eqEquippedByLabel: 'ผู้สวมใส่ (Equipped By):',
eqSaveWearer: '💾 บันทึกสวมใส่',
eqDelete: '🗑️ ลบ',
eqDeleteTitle: 'ลบออกจากคลัง',
eqSlotN: 'ชิ้นที่ {n}',
eqMainStatLabel: 'สเตตัสหลัก (Main Stat)',
eqSubstatsLabel: 'ออฟรอง (Substats):',
eqNoSubstats: 'ไม่มีข้อมูลออฟรอง',
eqEditSubstats: '🔧 ปรับแต่งออฟรอง / สเตตัส (Edit Substats)',
eqSet2Fallback: 'เพิ่มประสิทธิภาพตามเซ็ตที่กำหนด',
eqSet4Fallback: 'ปลดล็อกเอฟเฟกต์ต่อสู้ขั้นสูง',
showcaseActiveSets: 'โบนัสเซ็ตที่ทำงาน',
showcaseEquippedRelics: 'รีลิกส์ที่สวมใส่ (+15)',
showcaseBuildTag: 'ภาพรวมบิลด์',
showcaseRankLevel: 'S{rank} • Lv. {level}/80',
consolePreviewHint: 'พิมพ์ / เพื่อเลือกคำสั่ง',
consoleDescriptionHint: 'เลือกคำสั่งเพื่อดูตัวอย่างและช่องที่ต้องใส่',
consoleArgumentHint: '↑ ↓ เลื่อน · Tab เติมค่าที่เลือก · Esc ปิดรายการ',
consoleKeyboardHint: '↑ ↓ เลื่อนรายการ · Tab เติมค่า · Shift+Tab ย้อนรายการ · Esc ปิด · Enter ส่งคำสั่ง',
cameraHint: 'เลือกโหมด แล้วกด Tab ในเกมเพื่อเปิดหรือปิดกล้อง',
cameraMode: 'โหมดกล้อง',
cameraFaceCamDistance: 'ระยะ FaceCam',
cameraSave: 'บันทึกกล้อง',
cameraFaceCamExperimental: 'FaceCam (ทดลอง)',
cameraDistanceError: 'ระยะ FaceCam ต้องอยู่ระหว่าง 0.5 ถึง 5',
cameraSaved: 'บันทึกกล้องแล้ว',
lcPassiveFallback: 'เพิ่มค่าสถานะพื้นฐานตามสาย <strong>{path}</strong> และเพิ่มประสิทธิภาพความเสียหายและการสนับสนุน <strong>{value}%</strong> เมื่อเข้าสู่การต่อสู้',
slotHeadShort: 'ส่วนหัว',
slotHandsShort: 'ส่วนมือ',
slotBodyShort: 'ลำตัว',
slotFeetShort: 'รองเท้า',
slotSphereShort: 'ลูกแก้ว',
slotRopeShort: 'สร้อย',
traceFallbackA2: 'ร่องรอย A2',
traceFallbackA4: 'ร่องรอย A4',
traceFallbackA6: 'ร่องรอย A6',
printSavePdf: 'พิมพ์ / บันทึกเป็น PDF หรือรูปภาพ',
printClose: 'ปิดหน้าต่าง',
exRankNoData: 'ไม่มีข้อมูล Ex-Rank',
exRankEnabled: 'เปิดใช้งานแล้ว',
exRankDisabled: 'ปิดการใช้งาน',
jsonStatusLoading: 'กำลังโหลด {file}...',
jsonStatusMemory: '{file} (โหลดจากหน่วยความจำ/แคช)',
jsonStatusServer: '{file} (โหลดจากเซิร์ฟเวอร์)',
jsonStatusBrowserCache: '{file} (โหลดจากแคชของเบราว์เซอร์)',
jsonStatusCache: '{file} (โหลดจากแคช)',
jsonStatusDump: '{file} (โครงสร้าง Firefly dump)',
jsonOfflineNoCache: 'Offline: ไม่มีแคช สามารถอัปโหลดหรือพิมพ์ JSON เองได้',
jsonOfflinePaste: 'Offline: สามารถพิมพ์หรือวาง JSON เองได้',
jsonCacheUnavailable: 'ไม่สามารถตรวจสอบได้',
copyCardName: 'ชื่อ',
copyCardPath: 'สาย',
copyCardElement: 'ธาตุ',
copyCardLevel: 'เลเวล',
copyCardEidolon: 'Eidolon',
copyCardLightCone: 'กรวยแสง',
copyCardGenerated: 'สร้างโดย GaC-Srtool'
},
en: {
relicSlotNotEquipped: 'Not equipped',
relicSlotCreateHint: '+ Click to choose or create (recommended: {stat})',
relicSlotUnequip: 'Unequip this piece',
relicSubstatFallback: '4 substats (click to customize)',
relicSetNoBonus: 'No set effect active (equip at least 2 or 4 pieces of the same set)',
relicSet4Fallback: '4-piece set effect active',
relicSet2Fallback: '2-piece set effect active',
overviewRelicCount: '{count}/6 equipped (no set bonus yet)',
relicPickerEditTitle: '🔧 Edit relic: {set} ({slot}) - {name}',
relicPickerEditSubtitle: 'Customize the set, main stat and substats of this relic (#{id})',
relicPickerEditing: '✏️ Editing relic #{id}',
relicPickerSaveChanges: '💾 Save substat changes',
relicPickerEquipNow: '⚡ Equip this piece now (+15 5★ with selected stats)',
relicPickerInInventory: 'In inventory',
relicPickerEditThis: '✏️ Edit this piece',
relicPickerEditThisTitle: 'Load this piece to edit its substats',
relicPickerRecommended: 'Recommended 4 substats - customize them below',
substatMainConflict: ' (matches main stat)',
substatAlreadyChosen: ' (already chosen)',
substatSlotLabel: 'Substat {n}',
substatRolls: '+{value} ({count} rolls)',
substatDecreaseRoll: 'Decrease rolls',
substatIncreaseRoll: 'Increase rolls',
relicEmptyInventoryTitle: 'No relics for this slot in the inventory',
relicEmptyInventoryHint: 'Switch to the "⚡ Equip / Create recommended" tab to create a new piece.',
relicEquippedNow: 'Currently equipped',
relicWornBy: 'Equipped by: {name}',
relicFreeInInventory: 'Free in inventory',
relicInUse: 'In use',
relicSelectThis: 'Select this piece',
lcNoneEquipped: 'No Light Cone equipped',
lcSelectPrompt: 'Choose a Light Cone to equip',
lcSamePathGroup: '⭐ {path} Light Cones',
lcOtherPathGroup: '🗡️ Other paths',
lcNoResults: 'No Light Cones match your search',
lcEquippedBadge: 'Equipped',
lcSignatureBadge: 'Signature',
lcSelectTitle: '🗡️ Choose a Light Cone: {name}',
eqSkillFallbackTitle: 'Light Cone passive',
eqSkillFallbackDesc: 'Boosts combat power and reinforces the character path mechanics when equipped',
eqEquippedByLabel: 'Equipped by:',
eqSaveWearer: '💾 Save equipment',
eqDelete: '🗑️ Delete',
eqDeleteTitle: 'Remove from inventory',
eqSlotN: 'Piece {n}',
eqMainStatLabel: 'Main stat',
eqSubstatsLabel: 'Substats:',
eqNoSubstats: 'No substat data',
eqEditSubstats: '🔧 Edit substats',
eqSet2Fallback: 'Improves performance based on the set',
eqSet4Fallback: 'Unlocks advanced combat effects',
showcaseActiveSets: 'Active Set Bonuses',
showcaseEquippedRelics: 'Equipped Relics (+15)',
showcaseBuildTag: 'Build Overview',
showcaseRankLevel: 'S{rank} • Lv. {level}/80',
consolePreviewHint: 'Type / to choose a command',
consoleDescriptionHint: 'Select a command to see an example and its arguments',
consoleArgumentHint: '↑ ↓ navigate · Tab fill selected · Esc close',
consoleKeyboardHint: '↑ ↓ navigate · Tab fill · Shift+Tab previous · Esc close · Enter send',
cameraHint: 'Choose a mode, then press Tab in game to toggle the camera',
cameraMode: 'Camera mode',
cameraFaceCamDistance: 'FaceCam distance',
cameraSave: 'Save camera',
cameraFaceCamExperimental: 'FaceCam (experimental)',
cameraDistanceError: 'FaceCam distance must be between 0.5 and 5',
cameraSaved: 'Camera saved',
lcPassiveFallback: 'Increases basic stats following the <strong>{path}</strong> path and boosts damage and support performance by <strong>{value}%</strong> in battle',
slotHeadShort: 'Head',
slotHandsShort: 'Hands',
slotBodyShort: 'Body',
slotFeetShort: 'Feet',
slotSphereShort: 'Planar Sphere',
slotRopeShort: 'Link Rope',
traceFallbackA2: 'Trace A2',
traceFallbackA4: 'Trace A4',
traceFallbackA6: 'Trace A6',
printSavePdf: 'Print / save as PDF or image',
printClose: 'Close window',
exRankNoData: 'No Ex-Rank data',
exRankEnabled: 'Enabled',
exRankDisabled: 'Disabled',
jsonStatusLoading: 'Loading {file}...',
jsonStatusMemory: '{file} (from memory/cache)',
jsonStatusServer: '{file} (from server)',
jsonStatusBrowserCache: '{file} (from browser cache)',
jsonStatusCache: '{file} (from cache)',
jsonStatusDump: '{file} (Firefly dump structure)',
jsonOfflineNoCache: 'Offline: no cache. You can upload or paste JSON.',
jsonOfflinePaste: 'Offline: you can paste or type JSON.',
jsonCacheUnavailable: 'Unavailable',
copyCardName: 'Name',
copyCardPath: 'Path',
copyCardElement: 'Element',
copyCardLevel: 'Level',
copyCardEidolon: 'Eidolon',
copyCardLightCone: 'Light Cone',
copyCardGenerated: 'Generated via GaC-Srtool'
}
};
for (const [language, additions] of Object.entries(MODAL_AND_TOOL_TRANSLATIONS)) {
if (TRANSLATIONS[language]) Object.assign(TRANSLATIONS[language], additions);
}
let currentLang = 'th';
if (typeof localStorage !== 'undefined') {
currentLang = localStorage.getItem('gac_srtool_lang') || 'th';