fix : correct color

This commit is contained in:
2026-10-02 22:12:43 +07:00 Unverified
parent 78750aef95
commit f71c00d2ab
14 changed files with 79 additions and 51 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { rarityGradientClass } from "@/lib/catalog/rarity"; import { characterRarityGradientClass } from "@/lib/catalog/rarity";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
type Character = { key: string; name: string; aliases: string[]; element: string | null; rarity: number | null; imageUrl: string }; type Character = { key: string; name: string; aliases: string[]; element: string | null; rarity: number | null; imageUrl: string };
@@ -85,7 +85,7 @@ export function CreateGuideFlow({ characters, existingKeys }: { characters: Char
const disabled = existing.has(character.key); const disabled = existing.has(character.key);
return ( return (
<Card key={character.key} className="overflow-hidden py-0"> <Card key={character.key} className="overflow-hidden py-0">
<div className={cn("relative aspect-square", rarityGradientClass(character.rarity))}><Image src={character.imageUrl} alt={character.name} fill className="object-cover" /></div> <div className={cn("relative aspect-square", characterRarityGradientClass(character.key, character.rarity))}><Image src={character.imageUrl} alt={character.name} fill className="object-cover" /></div>
<CardHeader className="pt-4"> <CardHeader className="pt-4">
<CardTitle>{character.name}</CardTitle> <CardTitle>{character.name}</CardTitle>
<CardDescription>{character.element ?? "Unknown element"}</CardDescription> <CardDescription>{character.element ?? "Unknown element"}</CardDescription>
+3 -2
View File
@@ -19,7 +19,7 @@ import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, Pagi
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import type { GlossaryKind } from "@/lib/glossary"; import type { GlossaryKind } from "@/lib/glossary";
import { rarityGradientClass } from "@/lib/catalog/rarity"; import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
type CatalogItem = { key: string; name: string; imageUrl: string; rarity?: number | null }; type CatalogItem = { key: string; name: string; imageUrl: string; rarity?: number | null };
@@ -113,6 +113,7 @@ export function GlossaryManager({
onValueChange={(key) => setItem(catalogs[kind].find((value) => value.key === key) ?? null)} onValueChange={(key) => setItem(catalogs[kind].find((value) => value.key === key) ?? null)}
placeholder="ค้นหารายการ…" placeholder="ค้นหารายการ…"
rarityBackground rarityBackground
characterBackground={kind === "character"}
/> />
</Field> </Field>
<Field> <Field>
@@ -154,7 +155,7 @@ export function GlossaryManager({
{catalogItem ? ( {catalogItem ? (
<span className={cn( <span className={cn(
"relative size-10 shrink-0 overflow-hidden rounded-md bg-muted", "relative size-10 shrink-0 overflow-hidden rounded-md bg-muted",
rarityGradientClass(catalogItem.rarity), entry.kind === "character" ? characterRarityGradientClass(catalogItem.key, catalogItem.rarity) : rarityGradientClass(catalogItem.rarity),
)}> )}>
<Image src={catalogItem.imageUrl} alt="" fill sizes="40px" className="object-contain" /> <Image src={catalogItem.imageUrl} alt="" fill sizes="40px" className="object-contain" />
</span> </span>
+3 -2
View File
@@ -162,7 +162,7 @@ import {
} from "@/lib/guides/stats"; } from "@/lib/guides/stats";
import { elementBackgroundColor, elementIconFilter } from "@/lib/catalog/element"; import { elementBackgroundColor, elementIconFilter } from "@/lib/catalog/element";
import { supportsWeaponType } from "@/lib/catalog/lunaris"; import { supportsWeaponType } from "@/lib/catalog/lunaris";
import { rarityGradientClass } from "@/lib/catalog/rarity"; import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity";
import { isCustomTravelerKey } from "@/lib/catalog/travelers"; import { isCustomTravelerKey } from "@/lib/catalog/travelers";
import { import {
applyPresetFields, applyPresetFields,
@@ -3911,7 +3911,7 @@ function TeamEditor({
return ( return (
<div key={member.characterKey} className="min-w-0 text-center"> <div key={member.characterKey} className="min-w-0 text-center">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<div className={cn("relative size-16 shrink-0 overflow-hidden rounded-lg bg-muted", rarityGradientClass(isCustomTravelerKey(member.characterKey) ? 5 : character?.rarity))}> <div className={cn("relative size-16 shrink-0 overflow-hidden rounded-lg bg-muted", characterRarityGradientClass(member.characterKey, isCustomTravelerKey(member.characterKey) ? 5 : character?.rarity))}>
{character ? <Image src={character.imageUrl} alt={character.name} fill sizes="64px" className="object-contain" /> : null} {character ? <Image src={character.imageUrl} alt={character.name} fill sizes="64px" className="object-contain" /> : null}
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
@@ -4162,6 +4162,7 @@ function ExtraCatalogItemsEditor({
placeholder={`เลือก${labels[kind]}`} placeholder={`เลือก${labels[kind]}`}
showSelectedImage showSelectedImage
rarityBackground rarityBackground
characterBackground={kind === "character"}
/> />
</div> </div>
</div> </div>
+1
View File
@@ -219,6 +219,7 @@ function ItemPicker({
placeholder={`Search ${kind}…`} placeholder={`Search ${kind}…`}
emptyLabel="No matches" emptyLabel="No matches"
rarityBackground rarityBackground
characterBackground={kind === "character"}
/> />
</Field> </Field>
) : null} ) : null}
+12 -7
View File
@@ -13,7 +13,7 @@ import {
ComboboxTrigger, ComboboxTrigger,
FuzzyCombobox, FuzzyCombobox,
} from "@/components/ui/combobox"; } from "@/components/ui/combobox";
import { rarityGradientClass } from "@/lib/catalog/rarity"; import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export interface AdminCatalogItem { export interface AdminCatalogItem {
@@ -50,6 +50,7 @@ type CommonSelectProps<Item extends AdminCatalogItem> = {
emptyLabel?: string; emptyLabel?: string;
invalid?: boolean; invalid?: boolean;
rarityBackground?: boolean; rarityBackground?: boolean;
characterBackground?: boolean;
searchNames?: (item: Item) => string[]; searchNames?: (item: Item) => string[];
}; };
@@ -69,6 +70,7 @@ export function CatalogDropdown<Item extends AdminCatalogItem>({
emptyLabel = "ไม่พบรายการ", emptyLabel = "ไม่พบรายการ",
invalid = false, invalid = false,
rarityBackground = false, rarityBackground = false,
characterBackground = false,
showSelectedImage = false, showSelectedImage = false,
showClear = true, showClear = true,
label, label,
@@ -82,7 +84,7 @@ export function CatalogDropdown<Item extends AdminCatalogItem>({
<div <div
className={cn( className={cn(
"relative size-10 shrink-0 overflow-hidden rounded-md", "relative size-10 shrink-0 overflow-hidden rounded-md",
rarityBackground && rarityGradientClass(selected.rarity), rarityBackground && (characterBackground ? characterRarityGradientClass(selected.key, selected.rarity) : rarityGradientClass(selected.rarity)),
)} )}
> >
<Image src={selected.imageUrl} alt="" fill sizes="40px" className="object-contain" /> <Image src={selected.imageUrl} alt="" fill sizes="40px" className="object-contain" />
@@ -110,7 +112,7 @@ export function CatalogDropdown<Item extends AdminCatalogItem>({
<ComboboxList> <ComboboxList>
{(item) => ( {(item) => (
<ComboboxItem key={item.key} value={item}> <ComboboxItem key={item.key} value={item}>
<CatalogOptionImage item={item} rarityBackground={rarityBackground} /> <CatalogOptionImage item={item} rarityBackground={rarityBackground} characterBackground={characterBackground} />
<span className="whitespace-nowrap">{item.name}</span> <span className="whitespace-nowrap">{item.name}</span>
</ComboboxItem> </ComboboxItem>
)} )}
@@ -138,6 +140,7 @@ function CatalogSlotSelect<Item extends AdminCatalogItem>({
emptyLabel = "ไม่พบรายการ", emptyLabel = "ไม่พบรายการ",
invalid = false, invalid = false,
rarityBackground = true, rarityBackground = true,
characterBackground = false,
searchNames = (item) => [item.name, ...(item.aliases ?? [])], searchNames = (item) => [item.name, ...(item.aliases ?? [])],
className, className,
}: CatalogSlotSelectProps<Item>) { }: CatalogSlotSelectProps<Item>) {
@@ -158,7 +161,7 @@ function CatalogSlotSelect<Item extends AdminCatalogItem>({
aria-invalid={invalid} aria-invalid={invalid}
className={cn( className={cn(
"relative flex aspect-square w-full items-center justify-center overflow-hidden rounded-md border bg-muted p-0 outline-none focus-visible:ring-2 focus-visible:ring-ring aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 [&>svg]:hidden", "relative flex aspect-square w-full items-center justify-center overflow-hidden rounded-md border bg-muted p-0 outline-none focus-visible:ring-2 focus-visible:ring-ring aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 [&>svg]:hidden",
rarityBackground && selected && rarityGradientClass(selected.rarity), rarityBackground && selected && (characterBackground ? characterRarityGradientClass(selected.key, selected.rarity) : rarityGradientClass(selected.rarity)),
className, className,
)} )}
> >
@@ -174,7 +177,7 @@ function CatalogSlotSelect<Item extends AdminCatalogItem>({
<ComboboxList> <ComboboxList>
{(item) => ( {(item) => (
<ComboboxItem key={item.key} value={item}> <ComboboxItem key={item.key} value={item}>
<CatalogOptionImage item={item} rarityBackground={rarityBackground} /> <CatalogOptionImage item={item} rarityBackground={rarityBackground} characterBackground={characterBackground} />
<span className="whitespace-nowrap">{item.name}</span> <span className="whitespace-nowrap">{item.name}</span>
</ComboboxItem> </ComboboxItem>
)} )}
@@ -216,7 +219,7 @@ function DomainCatalogSelect<Item extends AdminCatalogItem>({
} }
export function CharacterSelect<Item extends AdminCatalogItem>(props: DomainSelectProps<Item>) { export function CharacterSelect<Item extends AdminCatalogItem>(props: DomainSelectProps<Item>) {
return <DomainCatalogSelect {...props} />; return <DomainCatalogSelect {...props} characterBackground />;
} }
export function WeaponSelect<Item extends AdminCatalogItem>(props: DomainSelectProps<Item>) { export function WeaponSelect<Item extends AdminCatalogItem>(props: DomainSelectProps<Item>) {
@@ -318,15 +321,17 @@ export function ArtifactSelect<Item extends AdminCatalogItem>(
function CatalogOptionImage({ function CatalogOptionImage({
item, item,
rarityBackground, rarityBackground,
characterBackground = false,
}: { }: {
item: AdminCatalogItem; item: AdminCatalogItem;
rarityBackground: boolean; rarityBackground: boolean;
characterBackground?: boolean;
}) { }) {
return ( return (
<div <div
className={cn( className={cn(
"relative size-8 shrink-0 overflow-hidden rounded-md", "relative size-8 shrink-0 overflow-hidden rounded-md",
rarityBackground && rarityGradientClass(item.rarity), rarityBackground && (characterBackground ? characterRarityGradientClass(item.key, item.rarity) : rarityGradientClass(item.rarity)),
)} )}
> >
<Image src={item.imageUrl} alt="" fill sizes="32px" className="object-contain" /> <Image src={item.imageUrl} alt="" fill sizes="32px" className="object-contain" />
+6 -6
View File
@@ -2,15 +2,15 @@ import Image from "next/image";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { elementBackgroundColor } from "@/lib/catalog/element"; import { elementBackgroundColor } from "@/lib/catalog/element";
import { rarityGradientClass } from "@/lib/catalog/rarity"; import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity";
import type { CommissionRequest } from "@/lib/commission/request"; import type { CommissionRequest } from "@/lib/commission/request";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
type Item = { key: string; name: string; imageUrl: string; rarity?: number | null; element?: string | null }; type Item = { key: string; name: string; imageUrl: string; rarity?: number | null; element?: string | null };
type Catalog = { characters: Item[]; weapons: Item[]; artifacts: Item[] }; type Catalog = { characters: Item[]; weapons: Item[]; artifacts: Item[] };
function ItemImage({ item, size = "small" }: { item: Item; size?: "small" | "large" }) { function ItemImage({ item, size = "small", character = false }: { item: Item; size?: "small" | "large"; character?: boolean }) {
return <div title={item.name} className={cn("relative aspect-square min-w-0 overflow-hidden rounded-lg", rarityGradientClass(item.rarity))}> return <div title={item.name} className={cn("relative aspect-square min-w-0 overflow-hidden rounded-lg", character ? characterRarityGradientClass(item.key, item.rarity) : rarityGradientClass(item.rarity))}>
<Image src={item.imageUrl} alt={item.name} fill sizes={size === "large" ? "160px" : "64px"} className="object-contain" /> <Image src={item.imageUrl} alt={item.name} fill sizes={size === "large" ? "160px" : "64px"} className="object-contain" />
</div>; </div>;
} }
@@ -30,7 +30,7 @@ function CommissionTeamCard({ team, index, catalog }: { team: CommissionRequest[
const artifact = artifacts.get(member.artifactKey); const artifact = artifacts.get(member.artifactKey);
return <div key={memberIndex} className="min-w-0 rounded-lg p-1 lg:p-1.5" style={{ backgroundColor: elementBackgroundColor(character?.element) }}> return <div key={memberIndex} className="min-w-0 rounded-lg p-1 lg:p-1.5" style={{ backgroundColor: elementBackgroundColor(character?.element) }}>
<div className="grid grid-cols-1 gap-1 lg:grid-cols-[2fr_1fr]"> <div className="grid grid-cols-1 gap-1 lg:grid-cols-[2fr_1fr]">
{character ? <ItemImage item={character} size="large" /> : <div className="aspect-square rounded-lg bg-muted" />} {character ? <ItemImage item={character} size="large" character /> : <div className="aspect-square rounded-lg bg-muted" />}
<div className="grid min-w-0 grid-cols-2 gap-0.5 lg:grid-cols-1 lg:grid-rows-2 lg:gap-1"> <div className="grid min-w-0 grid-cols-2 gap-0.5 lg:grid-cols-1 lg:grid-rows-2 lg:gap-1">
{weapon ? <ItemImage item={weapon} /> : <div title={member.weaponKey} className="aspect-square rounded-lg bg-muted" />} {weapon ? <ItemImage item={weapon} /> : <div title={member.weaponKey} className="aspect-square rounded-lg bg-muted" />}
{artifact ? <ItemImage item={artifact} /> : <div title={member.artifactKey} className="aspect-square rounded-lg bg-muted" />} {artifact ? <ItemImage item={artifact} /> : <div title={member.artifactKey} className="aspect-square rounded-lg bg-muted" />}
@@ -77,7 +77,7 @@ export function CommissionRequestSummary({ request, catalog, amountBaht }: {
: <p key={key} className="w-16 wrap-break-word text-xs">{key}</p>; : <p key={key} className="w-16 wrap-break-word text-xs">{key}</p>;
})} })}
</div> </div>
{character ? <div className="w-16 min-w-0"><ItemImage item={character} size="large" /><p className="mt-1 truncate text-center text-xs" title={character.name}>{character.name}</p></div> : null} {character ? <div className="w-16 min-w-0"><ItemImage item={character} size="large" character /><p className="mt-1 truncate text-center text-xs" title={character.name}>{character.name}</p></div> : null}
</CardContent> </CardContent>
</Card>; </Card>;
})} })}
@@ -89,7 +89,7 @@ export function CommissionRequestSummary({ request, catalog, amountBaht }: {
</CardHeader> </CardHeader>
<CardContent className="grid grid-cols-[minmax(0,1fr)_64px] items-center gap-3"> <CardContent className="grid grid-cols-[minmax(0,1fr)_64px] items-center gap-3">
<div className="flex min-w-0 flex-wrap gap-2">{set.levels.map((level) => <Badge key={level} variant="outline">C{level}</Badge>)}</div> <div className="flex min-w-0 flex-wrap gap-2">{set.levels.map((level) => <Badge key={level} variant="outline">C{level}</Badge>)}</div>
{character ? <div className="w-16 min-w-0"><ItemImage item={character} size="large" /><p className="mt-1 truncate text-center text-xs" title={character.name}>{character.name}</p></div> : null} {character ? <div className="w-16 min-w-0"><ItemImage item={character} size="large" character /><p className="mt-1 truncate text-center text-xs" title={character.name}>{character.name}</p></div> : null}
</CardContent> </CardContent>
</Card>; </Card>;
})} })}
+17 -18
View File
@@ -776,8 +776,24 @@ export function CommissionTicketChat({
mine ? "items-end" : "items-start", mine ? "items-end" : "items-start",
)} )}
> >
<div
className={cn(
"w-full px-1 text-center text-xs font-medium text-muted-foreground",
mine ? "translate-x-5" : "-translate-x-5",
)}
>
<time
dateTime={sentAt.toISOString()}
title={sentAt.toLocaleString("th-TH")}
>
{sentAt.toLocaleTimeString("th-TH", {
hour: "2-digit",
minute: "2-digit",
})}
</time>
</div>
<MessageHeader className="px-1"> <MessageHeader className="px-1">
{mine ? "คุณ" : message.authorName} {message.authorName}
</MessageHeader> </MessageHeader>
<div <div
className={cn( className={cn(
@@ -947,22 +963,6 @@ export function CommissionTicketChat({
))} ))}
</div> </div>
)} )}
<MessageFooter
className={cn(
"w-full justify-center gap-1 px-1 group-data-[align=end]/message:justify-center",
mine ? "translate-x-5" : "-translate-x-5",
)}
>
<time
dateTime={sentAt.toISOString()}
title={sentAt.toLocaleString("th-TH")}
>
{sentAt.toLocaleTimeString("th-TH", {
hour: "2-digit",
minute: "2-digit",
})}
</time>
</MessageFooter>
</MessageContent> </MessageContent>
</Message> </Message>
); );
@@ -970,7 +970,6 @@ export function CommissionTicketChat({
{visibleOptimisticMessages.map((message) => ( {visibleOptimisticMessages.map((message) => (
<Message key={message.localId} align="end"> <Message key={message.localId} align="end">
<MessageContent className="w-full items-end gap-1"> <MessageContent className="w-full items-end gap-1">
<MessageHeader className="px-1">คุณ</MessageHeader>
<Bubble <Bubble
variant={message.imageUrl ? "ghost" : "muted"} variant={message.imageUrl ? "ghost" : "muted"}
className="max-w-[min(82%,36rem)]" className="max-w-[min(82%,36rem)]"
+1 -1
View File
@@ -21,7 +21,7 @@ export async function CommissionTicketDetail({ id, admin }: { id: string; admin:
const catalog = await getCommissionLabels(data.checkout.request); const catalog = await getCommissionLabels(data.checkout.request);
return <div className="flex min-h-0 flex-1 flex-col gap-3"> return <div className="flex min-h-0 flex-1 flex-col gap-3">
<CommissionTicketPanels summary={ <CommissionTicketPanels summary={
<Card className="h-full min-h-0"><CardHeader className="shrink-0"><CardTitle>คำขอที่ชำระเงินแล้ว - ฿{data.checkout.amountBaht}</CardTitle></CardHeader> <Card className="h-full min-h-0"><CardHeader className="shrink-0"><CardTitle>รายละเอียดคำขอ - ฿{data.checkout.amountBaht}</CardTitle></CardHeader>
<CardContent className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto">{catalog && <CommissionRequestSummary request={data.checkout.request} catalog={catalog} amountBaht={data.checkout.amountBaht} />} <CardContent className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto">{catalog && <CommissionRequestSummary request={data.checkout.request} catalog={catalog} amountBaht={data.checkout.amountBaht} />}
<p className="text-sm text-muted-foreground">การชำระเงิน: {data.payment.transRef} - {data.payment.transferredAt.toLocaleString("th-TH")}</p> <p className="text-sm text-muted-foreground">การชำระเงิน: {data.payment.transRef} - {data.payment.transferredAt.toLocaleString("th-TH")}</p>
<CommissionSlipDialog src={publicMediaUrl(data.payment.slipObjectKey)} /> <CommissionSlipDialog src={publicMediaUrl(data.payment.slipObjectKey)} />
+2 -2
View File
@@ -7,7 +7,7 @@ import {
StygianBossTab, StygianBossTab,
} from "@/components/stygian/stygian-boss-display"; } from "@/components/stygian/stygian-boss-display";
import { elementIconFilter } from "@/lib/catalog/element"; import { elementIconFilter } from "@/lib/catalog/element";
import { rarityGradientClass } from "@/lib/catalog/rarity"; import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity";
import type { getActiveCatalog } from "@/lib/guides/queries"; import type { getActiveCatalog } from "@/lib/guides/queries";
import type { getStygianSchedule } from "@/lib/stygian/repository"; import type { getStygianSchedule } from "@/lib/stygian/repository";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -47,7 +47,7 @@ function RecommendationIcon({ item, catalog, teamLayout = false }: { item: Item;
return ( return (
<div className={cn("flex min-w-0 flex-col gap-1 rounded-lg border bg-card p-1", teamLayout && "w-full sm:p-2", teamLayout && hasEquipment && "sm:w-[164px]", teamLayout && !hasEquipment && "sm:w-[116px]", teamLayout && !hasEquipment && item.constellationLevel === null && "aspect-square justify-center")}> <div className={cn("flex min-w-0 flex-col gap-1 rounded-lg border bg-card p-1", teamLayout && "w-full sm:p-2", teamLayout && hasEquipment && "sm:w-[164px]", teamLayout && !hasEquipment && "sm:w-[116px]", teamLayout && !hasEquipment && item.constellationLevel === null && "aspect-square justify-center")}>
<div className={teamLayout && hasEquipment ? "flex flex-col gap-1 sm:grid sm:grid-cols-[100px_40px] sm:gap-2" : "flex flex-col gap-1"}> <div className={teamLayout && hasEquipment ? "flex flex-col gap-1 sm:grid sm:grid-cols-[100px_40px] sm:gap-2" : "flex flex-col gap-1"}>
<div className={cn("relative aspect-square overflow-hidden rounded-lg", teamLayout && "sm:size-[100px]", teamLayout && hasEquipment && "sm:row-span-2", rarityGradientClass(value.rarity))}> <div className={cn("relative aspect-square overflow-hidden rounded-lg", teamLayout && "sm:size-[100px]", teamLayout && hasEquipment && "sm:row-span-2", characterRarityGradientClass(item.catalogKey, value.rarity))}>
{value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes={teamLayout ? "(min-width: 640px) 128px, 25vw" : "128px"} className="object-contain" /> : null} {value.imageUrl ? <Image src={value.imageUrl} alt={value.name} fill sizes={teamLayout ? "(min-width: 640px) 128px, 25vw" : "128px"} className="object-contain" /> : null}
</div> </div>
{hasEquipment ? <div className={teamLayout ? "grid grid-cols-2 gap-1 sm:row-span-2 sm:grid-cols-1 sm:gap-2 sm:self-center" : "grid grid-cols-2 gap-1"}> {hasEquipment ? <div className={teamLayout ? "grid grid-cols-2 gap-1 sm:row-span-2 sm:grid-cols-1 sm:gap-2 sm:self-center" : "grid grid-cols-2 gap-1"}>
+4 -3
View File
@@ -25,7 +25,7 @@ import {
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { elementBackgroundColor } from "@/lib/catalog/element"; import { elementBackgroundColor } from "@/lib/catalog/element";
import { rarityGradientClass } from "@/lib/catalog/rarity"; import { characterRarityGradientClass, rarityGradientClass } from "@/lib/catalog/rarity";
import { isCustomTravelerKey } from "@/lib/catalog/travelers"; import { isCustomTravelerKey } from "@/lib/catalog/travelers";
import type { PublicGuide } from "@/lib/guides/queries"; import type { PublicGuide } from "@/lib/guides/queries";
import { teamConstellationLabel, teamRefinementLabel } from "@/lib/guides/team-tools"; import { teamConstellationLabel, teamRefinementLabel } from "@/lib/guides/team-tools";
@@ -226,7 +226,7 @@ export function TeamExportImage({ team }: { team: TeamExportData }) {
const percent = total > 0 ? damage / total * 100 : 0; const percent = total > 0 ? damage / total * 100 : 0;
return ( return (
<div key={member.position} className="flex h-43.5 items-center gap-6 rounded-3xl p-3.5" style={{ backgroundColor: elementBackgroundColor(member.character.element) }}> <div key={member.position} className="flex h-43.5 items-center gap-6 rounded-3xl p-3.5" style={{ backgroundColor: elementBackgroundColor(member.character.element) }}>
<div className={cn("relative size-35 shrink-0 overflow-hidden rounded-3xl border-[3px] border-white/20", rarityGradientClass(isCustomTravelerKey(member.character.key) ? 5 : member.character.rarity))}> <div className={cn("relative size-35 shrink-0 overflow-hidden rounded-3xl border-[3px] border-white/20", characterRarityGradientClass(member.character.key, isCustomTravelerKey(member.character.key) ? 5 : member.character.rarity))}>
<Image src={member.character.imageUrl} alt="" fill loading="eager" sizes="140px" className="object-contain" /> <Image src={member.character.imageUrl} alt="" fill loading="eager" sizes="140px" className="object-contain" />
</div> </div>
<div className="grid w-44 shrink-0 grid-cols-2 gap-3"> <div className="grid w-44 shrink-0 grid-cols-2 gap-3">
@@ -316,7 +316,8 @@ function TeamCard({
exporting exporting
? "col-span-2 row-span-2" ? "col-span-2 row-span-2"
: "sm:col-span-2 sm:row-span-2", : "sm:col-span-2 sm:row-span-2",
rarityGradientClass( characterRarityGradientClass(
member.character.key,
isCustomTravelerKey(member.character.key) isCustomTravelerKey(member.character.key)
? 5 ? 5
: member.character.rarity, : member.character.rarity,
+15 -3
View File
@@ -1,11 +1,13 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { rarityGradient, rarityGradientClass, rarityTextClass, rarityTextColor } from "@/lib/catalog/rarity"; import { characterRarityGradientClass, rarityGradient, rarityGradientClass, rarityTextClass, rarityTextColor } from "@/lib/catalog/rarity";
describe("catalog rarity backgrounds", () => { describe("catalog rarity backgrounds", () => {
it("maps every weapon and artifact rarity to its Tailwind gradient", () => { it("maps every weapon and artifact rarity to its Tailwind gradient", () => {
expect(rarityGradientClass(1)).toContain("rgba(23,25,28,1)"); expect(rarityGradientClass(1)).toBe("bg-[#697177] bg-[linear-gradient(90deg,rgba(105,113,119,1)_0%,rgba(137,144,147,1)_100%)]");
expect(rarityGradientClass(2)).toContain("rgba(11,43,24,1)"); expect(rarityGradientClass(2)).toBe("bg-[#5D8972] bg-[linear-gradient(90deg,rgba(93,137,114,1)_0%,rgba(121,167,127,1)_100%)]");
expect(rarityGradient(1)).toBe("linear-gradient(90deg, rgba(105, 113, 119, 1) 0%, rgba(137, 144, 147, 1) 100%)");
expect(rarityGradient(2)).toBe("linear-gradient(90deg, rgba(93, 137, 114, 1) 0%, rgba(121, 167, 127, 1) 100%)");
expect(rarityGradientClass(3)).toBe("bg-[#4D7691] bg-[linear-gradient(90deg,rgba(77,118,145,1)_0%,rgba(71,135,165,1)_100%)]"); expect(rarityGradientClass(3)).toBe("bg-[#4D7691] bg-[linear-gradient(90deg,rgba(77,118,145,1)_0%,rgba(71,135,165,1)_100%)]");
expect(rarityGradientClass(4)).toBe("bg-[#6D6A9C] bg-[linear-gradient(90deg,rgba(109,106,156,1)_0%,rgba(150,122,179,1)_100%)]"); expect(rarityGradientClass(4)).toBe("bg-[#6D6A9C] bg-[linear-gradient(90deg,rgba(109,106,156,1)_0%,rgba(150,122,179,1)_100%)]");
expect(rarityGradientClass(5)).toBe("bg-[#A67B34] bg-[linear-gradient(90deg,rgba(166,123,52,1)_0%,rgba(178,123,44,1)_100%)]"); expect(rarityGradientClass(5)).toBe("bg-[#A67B34] bg-[linear-gradient(90deg,rgba(166,123,52,1)_0%,rgba(178,123,44,1)_100%)]");
@@ -26,4 +28,14 @@ describe("catalog rarity backgrounds", () => {
expect(rarityTextClass(undefined)).toBeUndefined(); expect(rarityTextClass(undefined)).toBeUndefined();
expect(rarityTextColor(undefined)).toBe("#17171c"); expect(rarityTextColor(undefined)).toBe("#17171c");
}); });
it("uses red for special characters without changing other rarity backgrounds", () => {
for (const key of ["10000062", "10000117", "10000118"]) {
expect(characterRarityGradientClass(key, 5)).toBe("bg-[#9A424E] bg-[linear-gradient(90deg,#9A424E_0%,#B85D67_100%)]");
expect(characterRarityGradientClass(key, null)).toBe(characterRarityGradientClass(key, 5));
}
expect(characterRarityGradientClass("ordinary-character", 5)).toBe(rarityGradientClass(5));
expect(characterRarityGradientClass("ordinary-character", 1)).toBe(rarityGradientClass(1));
expect(characterRarityGradientClass("ordinary-character", 2)).toBe(rarityGradientClass(2));
});
}); });
+11 -4
View File
@@ -1,14 +1,17 @@
const rarityGradientClasses: Record<number, string> = { const rarityGradientClasses: Record<number, string> = {
1: "bg-[linear-gradient(90deg,rgba(23,25,28,1)_0%,rgba(19,20,23,1)_100%)]", 1: "bg-[#697177] bg-[linear-gradient(90deg,rgba(105,113,119,1)_0%,rgba(137,144,147,1)_100%)]",
2: "bg-[linear-gradient(90deg,rgba(11,43,24,1)_0%,rgba(7,18,13,1)_100%)]", 2: "bg-[#5D8972] bg-[linear-gradient(90deg,rgba(93,137,114,1)_0%,rgba(121,167,127,1)_100%)]",
3: "bg-[#4D7691] bg-[linear-gradient(90deg,rgba(77,118,145,1)_0%,rgba(71,135,165,1)_100%)]", 3: "bg-[#4D7691] bg-[linear-gradient(90deg,rgba(77,118,145,1)_0%,rgba(71,135,165,1)_100%)]",
4: "bg-[#6D6A9C] bg-[linear-gradient(90deg,rgba(109,106,156,1)_0%,rgba(150,122,179,1)_100%)]", 4: "bg-[#6D6A9C] bg-[linear-gradient(90deg,rgba(109,106,156,1)_0%,rgba(150,122,179,1)_100%)]",
5: "bg-[#A67B34] bg-[linear-gradient(90deg,rgba(166,123,52,1)_0%,rgba(178,123,44,1)_100%)]", 5: "bg-[#A67B34] bg-[linear-gradient(90deg,rgba(166,123,52,1)_0%,rgba(178,123,44,1)_100%)]",
}; };
const specialCharacterKeys = new Set(["10000062", "10000117", "10000118"]);
const specialCharacterGradientClass = "bg-[#9A424E] bg-[linear-gradient(90deg,#9A424E_0%,#B85D67_100%)]";
const rarityGradients: Record<number, string> = { const rarityGradients: Record<number, string> = {
1: "linear-gradient(90deg, rgba(23, 25, 28, 1) 0%, rgba(19, 20, 23, 1) 100%)", 1: "linear-gradient(90deg, rgba(105, 113, 119, 1) 0%, rgba(137, 144, 147, 1) 100%)",
2: "linear-gradient(90deg, rgba(11, 43, 24, 1) 0%, rgba(7, 18, 13, 1) 100%)", 2: "linear-gradient(90deg, rgba(93, 137, 114, 1) 0%, rgba(121, 167, 127, 1) 100%)",
3: "linear-gradient(90deg, rgba(77, 118, 145, 1) 0%, rgba(71, 135, 165, 1) 100%)", 3: "linear-gradient(90deg, rgba(77, 118, 145, 1) 0%, rgba(71, 135, 165, 1) 100%)",
4: "linear-gradient(90deg, rgba(109, 106, 156, 1) 0%, rgba(150, 122, 179, 1) 100%)", 4: "linear-gradient(90deg, rgba(109, 106, 156, 1) 0%, rgba(150, 122, 179, 1) 100%)",
5: "linear-gradient(90deg, rgba(166, 123, 52, 1) 0%, rgba(178, 123, 44, 1) 100%)", 5: "linear-gradient(90deg, rgba(166, 123, 52, 1) 0%, rgba(178, 123, 44, 1) 100%)",
@@ -30,6 +33,10 @@ export function rarityGradientClass(rarity: number | null | undefined): string |
return rarity ? rarityGradientClasses[rarity] : undefined; return rarity ? rarityGradientClasses[rarity] : undefined;
} }
export function characterRarityGradientClass(key: string | null | undefined, rarity: number | null | undefined): string | undefined {
return key && specialCharacterKeys.has(key) ? specialCharacterGradientClass : rarityGradientClass(rarity);
}
export function rarityGradient(rarity: number | null | undefined): string { export function rarityGradient(rarity: number | null | undefined): string {
return (rarity && rarityGradients[rarity]) || "#17383d"; return (rarity && rarityGradients[rarity]) || "#17383d";
} }
+1
View File
@@ -182,6 +182,7 @@ function rarity(value: string | undefined): number | null {
QUALITY_BLUE: 3, QUALITY_BLUE: 3,
QUALITY_PURPLE: 4, QUALITY_PURPLE: 4,
QUALITY_ORANGE: 5, QUALITY_ORANGE: 5,
QUALITY_ORANGE_SP: 5,
}[value] ?? null }[value] ?? null
); );
} }
+1 -1
View File
@@ -1,7 +1,7 @@
import * as z from "zod"; import * as z from "zod";
export const lunarisVersionSchema = z.string().regex(/^\d+(?:\.\d+)+$/u); export const lunarisVersionSchema = z.string().regex(/^\d+(?:\.\d+)+$/u);
export const catalogFormatVersion = 2; export const catalogFormatVersion = 3;
export interface LunarisVersionChange { export interface LunarisVersionChange {
previousVersion: string; previousVersion: string;