fix: enforce semantic contrast across admin
Local Build and Deploy / deploy (push) Successful in 44s
Local Build and Deploy / deploy (push) Successful in 44s
This commit is contained in:
1 parent
45c8178cfa
commit
f0b4bc1630
86 files changed
+438
-338
No files matched your search
@@ -61,10 +61,10 @@ export default async function AdminAds() {
|
|||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img src={ad.image} alt="" style={{ maxHeight: 40, maxWidth: 120 }} />
|
<img src={ad.image} alt="" style={{ maxHeight: 40, maxWidth: 120 }} />
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
<code>{ad.image}</code>
|
<code>{ad.image}</code>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{ad.createdAt ? ad.createdAt.toISOString().slice(0, 10) : "—"}
|
{ad.createdAt ? ad.createdAt.toISOString().slice(0, 10) : "—"}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export default async function AdminAlerts() {
|
|||||||
|
|
||||||
<form action={sendHotelAlert} className="admin-card mb-6">
|
<form action={sendHotelAlert} className="admin-card mb-6">
|
||||||
<h3 className="mt-0">{t("form.send")}</h3>
|
<h3 className="mt-0">{t("form.send")}</h3>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs theme-text-muted dark:theme-text-muted">
|
||||||
{t.rich("form.description", {
|
{t.rich("form.description", {
|
||||||
code: (chunks) => <code>{chunks}</code>,
|
code: (chunks) => <code>{chunks}</code>,
|
||||||
})}
|
})}
|
||||||
@@ -134,7 +134,7 @@ export default async function AdminAlerts() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{alerts.map((a) => (
|
{alerts.map((a) => (
|
||||||
<tr key={String(a.id)}>
|
<tr key={String(a.id)}>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{fromDate(a.createdAt)}
|
{fromDate(a.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default async function AdminApplications() {
|
|||||||
{t("rankLabel", { rank: a.rankId })}
|
{t("rankLabel", { rank: a.rankId })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 m-0 mb-2">
|
<p className="text-xs theme-text-muted dark:theme-text-muted m-0 mb-2">
|
||||||
{formatDate(a.createdAt)}
|
{formatDate(a.createdAt)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-[var(--color-text)] whitespace-pre-wrap m-0 mb-3">{a.content}</p>
|
<p className="text-sm text-[var(--color-text)] whitespace-pre-wrap m-0 mb-3">{a.content}</p>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export default async function EditArticle({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error ? <p className="text-xs text-red-600 dark:text-red-400 mb-4">Error: {error}</p> : null}
|
{error ? <p className="text-xs text-[var(--color-error-text-readable)] dark:text-[var(--color-error-text-readable)] mb-4">Error: {error}</p> : null}
|
||||||
<ArticleForm
|
<ArticleForm
|
||||||
action={async (fd) => {
|
action={async (fd) => {
|
||||||
"use server";
|
"use server";
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default async function NewArticle({ searchParams }: { searchParams: Promi
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error ? <p className="text-xs text-red-600 dark:text-red-400 mb-4">Error: {error}</p> : null}
|
{error ? <p className="text-xs text-[var(--color-error-text-readable)] dark:text-[var(--color-error-text-readable)] mb-4">Error: {error}</p> : null}
|
||||||
<ArticleForm action={createArticle} />
|
<ArticleForm action={createArticle} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export default async function AdminArticles({ searchParams }: { searchParams: Pr
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error ? <p className="text-xs text-red-600 dark:text-red-400 mb-4">Error: {error}</p> : null}
|
{error ? <p className="text-xs text-[var(--color-error-text-readable)] dark:text-[var(--color-error-text-readable)] mb-4">Error: {error}</p> : null}
|
||||||
|
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||||
<StatusCard label={t("articlesShown")} value={articles.length} icon="📰" />
|
<StatusCard label={t("articlesShown")} value={articles.length} icon="📰" />
|
||||||
@@ -62,10 +62,10 @@ export default async function AdminArticles({ searchParams }: { searchParams: Pr
|
|||||||
<td>
|
<td>
|
||||||
<Link href={`/admin/articles/${a.id}`}>{a.title}</Link>
|
<Link href={`/admin/articles/${a.id}`}>{a.title}</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
<code>{a.slug}</code>
|
<code>{a.slug}</code>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{a.createdAt ? a.createdAt.toISOString().slice(0, 10) : "—"}
|
{a.createdAt ? a.createdAt.toISOString().slice(0, 10) : "—"}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ export default async function AdminBadges({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{uploaded && (
|
{uploaded && (
|
||||||
<div className="mb-4 p-3 rounded-lg bg-green-600/10 text-green-600 dark:text-green-400 text-xs">
|
<div className="mb-4 p-3 rounded-lg bg-[var(--color-success-subtle)] text-[var(--color-success-text-readable)] dark:text-[var(--color-success-text-readable)] text-xs">
|
||||||
{t("uploadedMessage", { code: uploaded })}
|
{t("uploadedMessage", { code: uploaded })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-4 p-3 rounded-lg bg-red-600/10 text-red-600 dark:text-red-400 text-xs">
|
<div className="mb-4 p-3 rounded-lg bg-[var(--color-error-subtle)] text-[var(--color-error-text-readable)] dark:text-[var(--color-error-text-readable)] text-xs">
|
||||||
{t("errorMessage", { error })}
|
{t("errorMessage", { error })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -121,7 +121,7 @@ export default async function AdminBadges({
|
|||||||
<code>{b.badgeKey}</code>
|
<code>{b.badgeKey}</code>
|
||||||
</td>
|
</td>
|
||||||
<td>{b.badgeName}</td>
|
<td>{b.badgeName}</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400">{b.badgeDescription}</td>
|
<td className="text-sm theme-text-muted dark:theme-text-muted">{b.badgeDescription}</td>
|
||||||
<td>
|
<td>
|
||||||
<form action={giveBadge} className="flex gap-2 items-center">
|
<form action={giveBadge} className="flex gap-2 items-center">
|
||||||
<input type="hidden" name="code" value={b.badgeKey} />
|
<input type="hidden" name="code" value={b.badgeKey} />
|
||||||
|
|||||||
@@ -104,11 +104,11 @@ export default async function AdminBans() {
|
|||||||
<td>{b.banReason || "—"}</td>
|
<td>{b.banReason || "—"}</td>
|
||||||
<td>
|
<td>
|
||||||
{b.banExpire === 0 ? (
|
{b.banExpire === 0 ? (
|
||||||
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]">
|
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-error-subtle)] text-[var(--color-text)]">
|
||||||
{t("permanent")}
|
{t("permanent")}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<span className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{fromUnix(b.banExpire)}
|
{fromUnix(b.banExpire)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export default async function AdminCalendarCampaign({ params }: { params: Promis
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
|
<p className="text-xs theme-text-muted dark:theme-text-muted mb-4">
|
||||||
<Link href="/admin/calendar">← {t("title")}</Link>
|
<Link href="/admin/calendar">← {t("title")}</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ export default async function AdminCalendarCampaign({ params }: { params: Promis
|
|||||||
) : null}
|
) : null}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="m-0 mb-1">{campaign.name || `#${campaign.id}`}</h1>
|
<h1 className="m-0 mb-1">{campaign.name || `#${campaign.id}`}</h1>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 m-0">
|
<p className="text-xs theme-text-muted dark:theme-text-muted m-0">
|
||||||
{t("detail.campaignMeta", {
|
{t("detail.campaignMeta", {
|
||||||
id: campaign.id,
|
id: campaign.id,
|
||||||
days: campaign.totalDays,
|
days: campaign.totalDays,
|
||||||
@@ -135,36 +135,36 @@ export default async function AdminCalendarCampaign({ params }: { params: Promis
|
|||||||
<tr key={r.id} className="border-t border-[var(--border-subtle)]">
|
<tr key={r.id} className="border-t border-[var(--border-subtle)]">
|
||||||
<td>{r.id}</td>
|
<td>{r.id}</td>
|
||||||
<td>
|
<td>
|
||||||
{r.productName || <span className="text-sm text-gray-500 dark:text-gray-400">—</span>}
|
{r.productName || <span className="text-sm theme-text-muted dark:theme-text-muted">—</span>}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{r.itemId > 0 ? r.itemId : "—"}
|
{r.itemId > 0 ? r.itemId : "—"}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{r.credits > 0 ? (
|
{r.credits > 0 ? (
|
||||||
r.credits
|
r.credits
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">—</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">—</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{r.pixels > 0 ? (
|
{r.pixels > 0 ? (
|
||||||
r.pixels
|
r.pixels
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">—</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">—</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{r.points > 0 ? (
|
{r.points > 0 ? (
|
||||||
`${r.points} (${t("detail.typeLabel", { type: r.pointsType })})`
|
`${r.points} (${t("detail.typeLabel", { type: r.pointsType })})`
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">—</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">—</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{r.badge || "—"}
|
{r.badge || "—"}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{r.subscriptionType ? `${r.subscriptionType} · ${r.subscriptionDays}d` : "—"}
|
{r.subscriptionType ? `${r.subscriptionType} · ${r.subscriptionDays}d` : "—"}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -173,7 +173,7 @@ export default async function AdminCalendarCampaign({ params }: { params: Promis
|
|||||||
</table>
|
</table>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-4">
|
<p className="text-xs theme-text-muted dark:theme-text-muted mt-4">
|
||||||
{t("detail.rewardCount", { count: rewards.length })}
|
{t("detail.rewardCount", { count: rewards.length })}
|
||||||
</p>
|
</p>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -100,15 +100,15 @@ export default async function AdminCalendar() {
|
|||||||
<div className="flex items-center justify-between gap-2 mb-1">
|
<div className="flex items-center justify-between gap-2 mb-1">
|
||||||
<h3 className="m-0">{c.name || `#${c.id}`}</h3>
|
<h3 className="m-0">{c.name || `#${c.id}`}</h3>
|
||||||
<span
|
<span
|
||||||
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${enabled ? "bg-green-600/20 text-[var(--color-text)]" : "bg-red-500/22 text-[var(--color-text)]"}`}
|
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${enabled ? "bg-[var(--color-success-subtle)] text-[var(--color-text)]" : "bg-[var(--color-error-subtle)] text-[var(--color-text)]"}`}
|
||||||
>
|
>
|
||||||
{enabled ? t("statusEnabled") : t("statusDisabled")}
|
{enabled ? t("statusEnabled") : t("statusDisabled")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 m-0">
|
<p className="text-xs theme-text-muted dark:theme-text-muted m-0">
|
||||||
{t("campaignMeta", { id: c.id, days: c.totalDays, rewards: count })}
|
{t("campaignMeta", { id: c.id, days: c.totalDays, rewards: count })}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
<p className="text-xs theme-text-muted dark:theme-text-muted mt-1">
|
||||||
{t("startsAt", { date: fmtDate(c.startTimestamp) })}
|
{t("startsAt", { date: fmtDate(c.startTimestamp) })}
|
||||||
{c.lockExpired === "1" ? ` · ${t("locksExpired")}` : ""}
|
{c.lockExpired === "1" ? ` · ${t("locksExpired")}` : ""}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function CatalogDetailTabs({
|
|||||||
const furniSet = new Set(furniDataIdList)
|
const furniSet = new Set(furniDataIdList)
|
||||||
const missing = baseItems.filter((b) => !furniSet.has(b.id)).length
|
const missing = baseItems.filter((b) => !furniSet.has(b.id)).length
|
||||||
return missing > 0 ? (
|
return missing > 0 ? (
|
||||||
<span className="ml-1 rounded-full bg-orange-500 text-white text-[10px] px-1.5 py-0.5 leading-none font-medium">
|
<span className="ml-1 rounded-full bg-[var(--color-warning-subtle)] text-[var(--color-warning-text-readable)] text-[10px] px-1.5 py-0.5 leading-none font-medium">
|
||||||
{missing}
|
{missing}
|
||||||
</span>
|
</span>
|
||||||
) : null
|
) : null
|
||||||
|
|||||||
@@ -943,7 +943,7 @@ export function CatalogItemsTable({
|
|||||||
|
|
||||||
{/* Unsaved changes banner */}
|
{/* Unsaved changes banner */}
|
||||||
{hasUnsavedChanges && (
|
{hasUnsavedChanges && (
|
||||||
<div className="flex items-center gap-2 rounded-md border border-yellow-300 bg-yellow-50 dark:border-yellow-700 dark:bg-yellow-950/30 px-3 py-2 text-sm text-yellow-800 dark:text-yellow-200">
|
<div className="flex items-center gap-2 rounded-md border border-[var(--color-warning-border)] bg-[var(--color-warning-subtle)] px-3 py-2 text-sm text-[var(--color-warning-text-readable)]">
|
||||||
<span className="flex-1">
|
<span className="flex-1">
|
||||||
You have unsaved changes (
|
You have unsaved changes (
|
||||||
{modifiedItems.length > 0 ? `${modifiedItems.length} item(s) modified` : ''}
|
{modifiedItems.length > 0 ? `${modifiedItems.length} item(s) modified` : ''}
|
||||||
@@ -1051,7 +1051,7 @@ export function CatalogItemsTable({
|
|||||||
<SortableRow
|
<SortableRow
|
||||||
key={item.id}
|
key={item.id}
|
||||||
id={item.id}
|
id={item.id}
|
||||||
className={cn(modified && 'bg-yellow-50 dark:bg-yellow-950/20')}
|
className={cn(modified && 'bg-[var(--color-warning-subtle)] dark:bg-[var(--color-warning-subtle)]')}
|
||||||
>
|
>
|
||||||
{({ dragHandleProps }) => (
|
{({ dragHandleProps }) => (
|
||||||
<>
|
<>
|
||||||
@@ -1111,7 +1111,7 @@ export function CatalogItemsTable({
|
|||||||
className="h-8 text-xs w-16"
|
className="h-8 text-xs w-16"
|
||||||
title="Credits"
|
title="Credits"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-yellow-600">c</span>
|
<span className="text-xs text-[var(--color-warning-text-readable)]">c</span>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={edit?.costPoints ?? item.costPoints}
|
value={edit?.costPoints ?? item.costPoints}
|
||||||
@@ -1132,7 +1132,7 @@ export function CatalogItemsTable({
|
|||||||
) : (
|
) : (
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
{item.costCredits > 0 && (
|
{item.costCredits > 0 && (
|
||||||
<span className="text-yellow-600">{item.costCredits}c </span>
|
<span className="text-[var(--color-warning-text-readable)]">{item.costCredits}c </span>
|
||||||
)}
|
)}
|
||||||
{item.costPoints > 0 && (
|
{item.costPoints > 0 && (
|
||||||
<span className={pts.color}>
|
<span className={pts.color}>
|
||||||
@@ -1191,7 +1191,7 @@ export function CatalogItemsTable({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-7 w-7 text-green-600"
|
className="h-7 w-7 text-[var(--color-success-text-readable)]"
|
||||||
onClick={() => handleSaveOne(item)}
|
onClick={() => handleSaveOne(item)}
|
||||||
disabled={isSaving || isPending}
|
disabled={isSaving || isPending}
|
||||||
title="Save this item"
|
title="Save this item"
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ export function MovePageCombobox({
|
|||||||
value === page.id ? 'opacity-100' : 'opacity-0',
|
value === page.id ? 'opacity-100' : 'opacity-0',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Star className="mr-1.5 h-3 w-3 text-amber-500" />
|
<Star className="mr-1.5 h-3 w-3 text-[var(--color-warning-text-readable)]" />
|
||||||
<span className="text-muted-foreground font-mono text-xs mr-1.5">
|
<span className="text-muted-foreground font-mono text-xs mr-1.5">
|
||||||
#{page.id}
|
#{page.id}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ export interface InlineEdit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const POINTS_TYPES: Record<number, { label: string; color: string }> = {
|
export const POINTS_TYPES: Record<number, { label: string; color: string }> = {
|
||||||
0: { label: 'Duckets', color: 'text-blue-500' },
|
0: { label: 'Duckets', color: 'text-[var(--color-info-text-readable)]' },
|
||||||
5: { label: 'Diamonds', color: 'text-purple-500' },
|
5: { label: 'Diamonds', color: 'text-purple-500' },
|
||||||
101: { label: 'Snowflakes', color: 'text-cyan-400' },
|
101: { label: 'Snowflakes', color: 'text-[var(--color-info-text-readable)]' },
|
||||||
103: { label: 'Hearts', color: 'text-pink-500' },
|
103: { label: 'Hearts', color: 'text-pink-500' },
|
||||||
104: { label: 'Stars', color: 'text-amber-400' },
|
104: { label: 'Stars', color: 'text-[var(--color-warning-text-readable)]' },
|
||||||
105: { label: 'Shells', color: 'text-orange-400' },
|
105: { label: 'Shells', color: 'text-[var(--color-warning-text-readable)]' },
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_NEW_ITEM = {
|
export const DEFAULT_NEW_ITEM = {
|
||||||
@@ -81,5 +81,5 @@ export const DEFAULT_NEW_ITEM = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getPointsLabel(type: number) {
|
export function getPointsLabel(type: number) {
|
||||||
return POINTS_TYPES[type] || { label: `Type ${type}`, color: 'text-gray-500' }
|
return POINTS_TYPES[type] || { label: `Type ${type}`, color: 'theme-text-muted' }
|
||||||
}
|
}
|
||||||
@@ -271,7 +271,7 @@ export function CatalogTranslateTab({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{missingItems.length > 0 && (
|
{missingItems.length > 0 && (
|
||||||
<div className="flex items-center gap-1.5 mt-2 text-xs text-orange-600 dark:text-orange-400">
|
<div className="flex items-center gap-1.5 mt-2 text-xs text-[var(--color-warning-text-readable)] dark:text-[var(--color-warning-text-readable)]">
|
||||||
<AlertTriangle className="h-3.5 w-3.5" />
|
<AlertTriangle className="h-3.5 w-3.5" />
|
||||||
<span>
|
<span>
|
||||||
{missingItems.length} item(s) missing from FurnitureData.json — will be
|
{missingItems.length} item(s) missing from FurnitureData.json — will be
|
||||||
@@ -399,9 +399,9 @@ export function CatalogTranslateTab({
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
className={
|
className={
|
||||||
isChanged
|
isChanged
|
||||||
? 'bg-yellow-50 dark:bg-yellow-950/20'
|
? 'bg-[var(--color-warning-subtle)] dark:bg-[var(--color-warning-subtle)]'
|
||||||
: isMissing
|
: isMissing
|
||||||
? 'bg-orange-50/50 dark:bg-orange-950/10'
|
? 'bg-[var(--color-warning-subtle)] dark:bg-[var(--color-warning-subtle)]'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -442,7 +442,7 @@ export function CatalogTranslateTab({
|
|||||||
value={editedNames[item.id] || ''}
|
value={editedNames[item.id] || ''}
|
||||||
onChange={(e) => handleNameChange(item.id, e.target.value)}
|
onChange={(e) => handleNameChange(item.id, e.target.value)}
|
||||||
disabled={!canEdit}
|
disabled={!canEdit}
|
||||||
className={`h-8 text-sm ${nameChanged ? 'border-yellow-400 dark:border-yellow-600' : ''}`}
|
className={`h-8 text-sm ${nameChanged ? 'border-[var(--color-warning-border)] dark:border-[var(--color-warning-border)]' : ''}`}
|
||||||
/>
|
/>
|
||||||
{currentCatalogName && currentCatalogName !== item.publicName && (
|
{currentCatalogName && currentCatalogName !== item.publicName && (
|
||||||
<p className="text-[11px] text-muted-foreground mt-0.5 truncate">
|
<p className="text-[11px] text-muted-foreground mt-0.5 truncate">
|
||||||
@@ -456,7 +456,7 @@ export function CatalogTranslateTab({
|
|||||||
onChange={(e) => handleDescChange(item.id, e.target.value)}
|
onChange={(e) => handleDescChange(item.id, e.target.value)}
|
||||||
disabled={!canEdit}
|
disabled={!canEdit}
|
||||||
placeholder="No description"
|
placeholder="No description"
|
||||||
className={`h-8 text-sm ${descChanged ? 'border-yellow-400 dark:border-yellow-600' : ''}`}
|
className={`h-8 text-sm ${descChanged ? 'border-[var(--color-warning-border)] dark:border-[var(--color-warning-border)]' : ''}`}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -464,7 +464,7 @@ export function CatalogTranslateTab({
|
|||||||
{isChanged ? (
|
{isChanged ? (
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="text-xs bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"
|
className="text-xs bg-[var(--color-warning-subtle)] text-[var(--color-warning-text-readable)]"
|
||||||
>
|
>
|
||||||
Modified
|
Modified
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -474,7 +474,7 @@ export function CatalogTranslateTab({
|
|||||||
{isMissing ? (
|
{isMissing ? (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<AlertTriangle className="h-3.5 w-3.5 text-orange-500 flex-shrink-0" />
|
<AlertTriangle className="h-3.5 w-3.5 text-[var(--color-warning-text-readable)] flex-shrink-0" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Missing from FurnitureData.json</p>
|
<p>Missing from FurnitureData.json</p>
|
||||||
@@ -486,7 +486,7 @@ export function CatalogTranslateTab({
|
|||||||
) : (
|
) : (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<CheckCircle2 className="h-3.5 w-3.5 text-green-500 flex-shrink-0" />
|
<CheckCircle2 className="h-3.5 w-3.5 text-[var(--color-success-text-readable)] flex-shrink-0" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>In FurnitureData.json</TooltipContent>
|
<TooltipContent>In FurnitureData.json</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export function BcPageDetail({ page, items, canEdit }: BcPageDetailProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
<span className="text-muted-foreground">/</span>
|
<span className="text-muted-foreground">/</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<HardHat className="h-5 w-5 text-amber-500" />
|
<HardHat className="h-5 w-5 text-[var(--color-warning-text-readable)]" />
|
||||||
<h1 className="text-2xl font-bold tracking-tight">{page.caption}</h1>
|
<h1 className="text-2xl font-bold tracking-tight">{page.caption}</h1>
|
||||||
<Badge variant="outline" className="font-mono text-xs">
|
<Badge variant="outline" className="font-mono text-xs">
|
||||||
#{page.id}
|
#{page.id}
|
||||||
@@ -306,7 +306,7 @@ export function BcPageDetail({ page, items, canEdit }: BcPageDetailProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-6 w-6 p-0 text-red-500 hover:text-red-600"
|
className="h-6 w-6 p-0 text-[var(--color-error-text-readable)] hover:text-[var(--color-error-text-readable)]"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!confirm(`Delete item "${item.catalogName}"?`)) return
|
if (!confirm(`Delete item "${item.catalogName}"?`)) return
|
||||||
run(() => deleteBcItem({ id: item.id }), {
|
run(() => deleteBcItem({ id: item.id }), {
|
||||||
|
|||||||
@@ -565,7 +565,7 @@ export default async function CommandoCentrum() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{activity.map((a) => (
|
{activity.map((a) => (
|
||||||
<tr key={String(a.id)}>
|
<tr key={String(a.id)}>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{fromDate(a.createdAt)}
|
{fromDate(a.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
<td>{nameById.get(Number(a.userId)) ?? `#${a.userId}`}</td>
|
<td>{nameById.get(Number(a.userId)) ?? `#${a.userId}`}</td>
|
||||||
@@ -574,7 +574,7 @@ export default async function CommandoCentrum() {
|
|||||||
{a.action}
|
{a.action}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400">{a.description}</td>
|
<td className="text-sm theme-text-muted dark:theme-text-muted">{a.description}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -608,15 +608,15 @@ export default async function CommandoCentrum() {
|
|||||||
: t("empty");
|
: t("empty");
|
||||||
return (
|
return (
|
||||||
<tr key={e.id}>
|
<tr key={e.id}>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{formatTimestamp(e.timestamp)}
|
{formatTimestamp(e.timestamp)}
|
||||||
</td>
|
</td>
|
||||||
<td>{e.type}</td>
|
<td>{e.type}</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{e.version}
|
{e.version}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<pre className="text-xs p-2 bg-gray-50 dark:bg-black/20 rounded overflow-auto max-h-[160px]">
|
<pre className="text-xs p-2 bg-[var(--color-background)] text-[var(--color-text-readable)] rounded overflow-auto max-h-[160px]">
|
||||||
{snippet}
|
{snippet}
|
||||||
</pre>
|
</pre>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default async function AdminEmailTemplates() {
|
|||||||
<input name="variables" placeholder={t("form.variablesPlaceholder")} className="min-w-[200px]" />
|
<input name="variables" placeholder={t("form.variablesPlaceholder")} className="min-w-[200px]" />
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<input type="checkbox" name="isActive" defaultChecked className="w-auto" />
|
<input type="checkbox" name="isActive" defaultChecked className="w-auto" />
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">{t("form.active")}</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">{t("form.active")}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -94,7 +94,7 @@ export default async function AdminEmailTemplates() {
|
|||||||
defaultChecked={template.isActive}
|
defaultChecked={template.isActive}
|
||||||
className="w-auto"
|
className="w-auto"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">{t("form.active")}</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">{t("form.active")}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -109,7 +109,7 @@ export default async function AdminEmailTemplates() {
|
|||||||
{t("save")}
|
{t("save")}
|
||||||
</button>
|
</button>
|
||||||
{template.updatedAt ? (
|
{template.updatedAt ? (
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
<span className="text-xs theme-text-muted dark:theme-text-muted">
|
||||||
{t("updated", {
|
{t("updated", {
|
||||||
date: new Date(template.updatedAt).toISOString().slice(0, 16).replace("T", " "),
|
date: new Date(template.updatedAt).toISOString().slice(0, 16).replace("T", " "),
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default async function AdminEmulator() {
|
|||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<h1>{t("title")}</h1>
|
<h1>{t("title")}</h1>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
<p className="text-xs theme-text-muted dark:theme-text-muted mb-3">
|
||||||
{t.rich("subtitle", {
|
{t.rich("subtitle", {
|
||||||
code: (chunks) => <code>{chunks}</code>,
|
code: (chunks) => <code>{chunks}</code>,
|
||||||
})}
|
})}
|
||||||
@@ -51,7 +51,7 @@ export default async function AdminEmulator() {
|
|||||||
|
|
||||||
<section className="mt-6">
|
<section className="mt-6">
|
||||||
<h2 className="admin-section-title">{t("settings")}</h2>
|
<h2 className="admin-section-title">{t("settings")}</h2>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
<p className="text-xs theme-text-muted dark:theme-text-muted mb-3">
|
||||||
{t("keysCount", { count: settings.length })}
|
{t("keysCount", { count: settings.length })}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -93,13 +93,13 @@ export default async function AdminEmulator() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{settings.length === 0 ? (
|
{settings.length === 0 ? (
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{t("noSettings")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted">{t("noSettings")}</p>
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mt-6">
|
<section className="mt-6">
|
||||||
<h2 className="admin-section-title">{t("texts")}</h2>
|
<h2 className="admin-section-title">{t("texts")}</h2>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
<p className="text-xs theme-text-muted dark:theme-text-muted mb-3">
|
||||||
{t("keysCount", { count: textsCount })}
|
{t("keysCount", { count: textsCount })}
|
||||||
{textsTruncated ? ` (${t("showingFirst", { count: texts.length })})` : ""}.
|
{textsTruncated ? ` (${t("showingFirst", { count: texts.length })})` : ""}.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export function FaviconForm({ currentUrl }: { currentUrl: string | null }) {
|
|||||||
name="file"
|
name="file"
|
||||||
accept=".png,.jpg,.jpeg,.gif,.webp,.ico,.svg"
|
accept=".png,.jpg,.jpeg,.gif,.webp,.ico,.svg"
|
||||||
required
|
required
|
||||||
className="block w-full text-sm text-[var(--color-text)] file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-[var(--color-primary)] file:text-white hover:file:bg-[var(--color-primary-hover)] cursor-pointer"
|
className="block w-full text-sm text-[var(--color-text-readable)] file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-[var(--color-primary)] file:text-[var(--color-primary-foreground-readable)] cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-[var(--color-text-muted)]">{t("uploadHint")}</p>
|
<p className="mt-1 text-xs text-[var(--color-text-muted)]">{t("uploadHint")}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,8 +112,8 @@ export function FaviconForm({ currentUrl }: { currentUrl: string | null }) {
|
|||||||
<div
|
<div
|
||||||
className={`mt-4 p-3 rounded-lg text-sm ${
|
className={`mt-4 p-3 rounded-lg text-sm ${
|
||||||
message.type === "success"
|
message.type === "success"
|
||||||
? "bg-green-500/10 text-green-600 dark:text-green-400"
|
? "bg-[var(--color-success-subtle)] text-[var(--color-success-text-readable)] dark:text-[var(--color-success-text-readable)]"
|
||||||
: "bg-red-500/10 text-red-600 dark:text-red-400"
|
: "bg-[var(--color-error-subtle)] text-[var(--color-error-text-readable)] dark:text-[var(--color-error-text-readable)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{message.text}
|
{message.text}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export default async function EditHelpQuestion({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error ? (
|
{error ? (
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4" style={{ color: "#dc2626" }}>
|
<p className="text-xs theme-text-muted dark:theme-text-muted mb-4" style={{ color: "#dc2626" }}>
|
||||||
Error: {error}
|
Error: {error}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default async function NewHelpQuestion({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error ? (
|
{error ? (
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4" style={{ color: "#dc2626" }}>
|
<p className="text-xs theme-text-muted dark:theme-text-muted mb-4" style={{ color: "#dc2626" }}>
|
||||||
Error: {error}
|
Error: {error}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -60,16 +60,16 @@ export default async function AdminHelpQuestions() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{entries.map((e) => (
|
{entries.map((e) => (
|
||||||
<tr key={String(e.id)}>
|
<tr key={String(e.id)}>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">{e.position}</td>
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">{e.position}</td>
|
||||||
<td>
|
<td>
|
||||||
<Link href={`/admin/help-questions/${e.id}`}>{e.name}</Link>
|
<Link href={`/admin/help-questions/${e.id}`}>{e.name}</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{e.buttonText || "—"}
|
{e.buttonText || "—"}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${e.smallBox ? "bg-green-600/20 text-[var(--color-text)]" : "bg-[var(--color-primary)]/18 text-[var(--color-text)]"}`}
|
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${e.smallBox ? "bg-[var(--color-success-subtle)] text-[var(--color-text)]" : "bg-[var(--color-primary)]/18 text-[var(--color-text)]"}`}
|
||||||
>
|
>
|
||||||
{e.smallBox ? "Yes" : "No"}
|
{e.smallBox ? "Yes" : "No"}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -202,8 +202,8 @@ export function ImportBadgesClient() {
|
|||||||
<span className="text-muted-foreground">results</span>
|
<span className="text-muted-foreground">results</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
|
<CheckCircle2 className="h-4 w-4 text-[var(--color-success-text-readable)]" />
|
||||||
<span className="font-medium text-emerald-600">{importedCount}</span>
|
<span className="font-medium text-[var(--color-success-text-readable)]">{importedCount}</span>
|
||||||
<span className="text-muted-foreground">imported</span>
|
<span className="text-muted-foreground">imported</span>
|
||||||
</div>
|
</div>
|
||||||
{importableBadges.length > 0 && (
|
{importableBadges.length > 0 && (
|
||||||
@@ -419,7 +419,7 @@ export function ImportBadgesClient() {
|
|||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
{badge.alreadyImported ? (
|
{badge.alreadyImported ? (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Badge className="text-[10px] px-1.5 py-0 bg-emerald-500/15 text-emerald-500 border-0 gap-0.5 flex-1 justify-center">
|
<Badge className="text-[10px] px-1.5 py-0 bg-[var(--color-success-subtle)] text-[var(--color-success-text-readable)] border-0 gap-0.5 flex-1 justify-center">
|
||||||
<Check className="w-2.5 h-2.5" />
|
<Check className="w-2.5 h-2.5" />
|
||||||
Imported
|
Imported
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -546,8 +546,8 @@ function FurniGrid({ source }: FurniGridProps) {
|
|||||||
<span className="text-muted-foreground">total</span>
|
<span className="text-muted-foreground">total</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
<Check className="h-4 w-4 text-emerald-500" />
|
<Check className="h-4 w-4 text-[var(--color-success-text-readable)]" />
|
||||||
<span className="font-medium text-emerald-600">{stats.present}</span>
|
<span className="font-medium text-[var(--color-success-text-readable)]">{stats.present}</span>
|
||||||
<span className="text-muted-foreground">present</span>
|
<span className="text-muted-foreground">present</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
@@ -766,7 +766,7 @@ function FurniGrid({ source }: FurniGridProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
{it.present ? (
|
{it.present ? (
|
||||||
<Badge className="text-[10px] px-1.5 py-0 bg-emerald-500/15 text-emerald-500 border-0 gap-0.5 w-full justify-center">
|
<Badge className="text-[10px] px-1.5 py-0 bg-[var(--color-success-subtle)] text-[var(--color-success-text-readable)] border-0 gap-0.5 w-full justify-center">
|
||||||
<Check className="w-2.5 h-2.5" />
|
<Check className="w-2.5 h-2.5" />
|
||||||
Present
|
Present
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -257,8 +257,8 @@ function LibsView() {
|
|||||||
<span className="text-muted-foreground">libraries</span>
|
<span className="text-muted-foreground">libraries</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
<Check className="h-4 w-4 text-emerald-500" />
|
<Check className="h-4 w-4 text-[var(--color-success-text-readable)]" />
|
||||||
<span className="font-medium text-emerald-600">{importedCount}</span>
|
<span className="font-medium text-[var(--color-success-text-readable)]">{importedCount}</span>
|
||||||
<span className="text-muted-foreground">on this page</span>
|
<span className="text-muted-foreground">on this page</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
@@ -400,7 +400,7 @@ function LibsView() {
|
|||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
{p.nitroExists ? (
|
{p.nitroExists ? (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Badge className="text-[10px] px-1.5 py-0 bg-emerald-500/15 text-emerald-500 border-0 gap-0.5 flex-1 justify-center">
|
<Badge className="text-[10px] px-1.5 py-0 bg-[var(--color-success-subtle)] text-[var(--color-success-text-readable)] border-0 gap-0.5 flex-1 justify-center">
|
||||||
<Check className="w-2.5 h-2.5" />
|
<Check className="w-2.5 h-2.5" />
|
||||||
Imported
|
Imported
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -591,8 +591,8 @@ function SetsView() {
|
|||||||
<span className="text-muted-foreground">sets</span>
|
<span className="text-muted-foreground">sets</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
<Check className="h-4 w-4 text-emerald-500" />
|
<Check className="h-4 w-4 text-[var(--color-success-text-readable)]" />
|
||||||
<span className="font-medium text-emerald-600">{presentCount}</span>
|
<span className="font-medium text-[var(--color-success-text-readable)]">{presentCount}</span>
|
||||||
<span className="text-muted-foreground">on this page</span>
|
<span className="text-muted-foreground">on this page</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
@@ -737,19 +737,19 @@ function SetsView() {
|
|||||||
{it.gender}
|
{it.gender}
|
||||||
</Badge>
|
</Badge>
|
||||||
{it.club > 0 && (
|
{it.club > 0 && (
|
||||||
<Badge className="text-[10px] px-1.5 py-0 bg-yellow-500/20 text-yellow-600 border-0">
|
<Badge className="text-[10px] px-1.5 py-0 bg-[var(--color-warning-subtle)] text-[var(--color-warning-text-readable)] border-0">
|
||||||
HC
|
HC
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{it.sellable && (
|
{it.sellable && (
|
||||||
<Badge className="text-[10px] px-1.5 py-0 bg-blue-500/15 text-blue-500 border-0">
|
<Badge className="text-[10px] px-1.5 py-0 bg-[var(--color-info-subtle)] text-[var(--color-info-text-readable)] border-0">
|
||||||
$
|
$
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
{it.present ? (
|
{it.present ? (
|
||||||
<Badge className="text-[10px] px-1.5 py-0 bg-emerald-500/15 text-emerald-500 border-0 gap-0.5 w-full justify-center">
|
<Badge className="text-[10px] px-1.5 py-0 bg-[var(--color-success-subtle)] text-[var(--color-success-text-readable)] border-0 gap-0.5 w-full justify-center">
|
||||||
<Check className="w-2.5 h-2.5" />
|
<Check className="w-2.5 h-2.5" />
|
||||||
Imported
|
Imported
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -185,8 +185,8 @@ export function ImportEffectsClient() {
|
|||||||
<span className="text-muted-foreground">effects</span>
|
<span className="text-muted-foreground">effects</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
<Check className="h-4 w-4 text-emerald-500" />
|
<Check className="h-4 w-4 text-[var(--color-success-text-readable)]" />
|
||||||
<span className="font-medium text-emerald-600">{importedCount}</span>
|
<span className="font-medium text-[var(--color-success-text-readable)]">{importedCount}</span>
|
||||||
<span className="text-muted-foreground">imported</span>
|
<span className="text-muted-foreground">imported</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
@@ -337,7 +337,7 @@ export function ImportEffectsClient() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{e.type === 'dance' ? (
|
{e.type === 'dance' ? (
|
||||||
<Music className="w-7 h-7 text-amber-500/80" />
|
<Music className="w-7 h-7 text-[var(--color-warning-text-readable)]/80" />
|
||||||
) : (
|
) : (
|
||||||
<Sparkles className="w-7 h-7 text-violet-500/80" />
|
<Sparkles className="w-7 h-7 text-violet-500/80" />
|
||||||
)}
|
)}
|
||||||
@@ -351,7 +351,7 @@ export function ImportEffectsClient() {
|
|||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
{imported ? (
|
{imported ? (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Badge className="text-[10px] px-1.5 py-0 bg-emerald-500/15 text-emerald-500 border-0 gap-0.5 flex-1 justify-center">
|
<Badge className="text-[10px] px-1.5 py-0 bg-[var(--color-success-subtle)] text-[var(--color-success-text-readable)] border-0 gap-0.5 flex-1 justify-center">
|
||||||
<Check className="w-2.5 h-2.5" />
|
<Check className="w-2.5 h-2.5" />
|
||||||
Imported
|
Imported
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -883,11 +883,11 @@ export function ImportFurniClient() {
|
|||||||
<div className="text-[10px] text-muted-foreground">on this page</div>
|
<div className="text-[10px] text-muted-foreground">on this page</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border bg-card p-3">
|
<div className="rounded-lg border bg-card p-3">
|
||||||
<div className="flex items-center gap-2 text-xs text-amber-500">
|
<div className="flex items-center gap-2 text-xs text-[var(--color-warning-text-readable)]">
|
||||||
<FileWarning className="w-3.5 h-3.5" />
|
<FileWarning className="w-3.5 h-3.5" />
|
||||||
Missing .nitro
|
Missing .nitro
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-bold mt-1 text-amber-500">
|
<div className="text-xl font-bold mt-1 text-[var(--color-warning-text-readable)]">
|
||||||
{stats.missingNitro.toLocaleString()}
|
{stats.missingNitro.toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -913,12 +913,12 @@ export function ImportFurniClient() {
|
|||||||
<div className="rounded-lg border p-4 space-y-3 bg-card">
|
<div className="rounded-lg border p-4 space-y-3 bg-card">
|
||||||
<h3 className="text-sm font-semibold">Import Complete</h3>
|
<h3 className="text-sm font-semibold">Import Complete</h3>
|
||||||
<div className="flex gap-4 text-sm">
|
<div className="flex gap-4 text-sm">
|
||||||
<span className="text-emerald-500">{batchSummary.succeeded} succeeded</span>
|
<span className="text-[var(--color-success-text-readable)]">{batchSummary.succeeded} succeeded</span>
|
||||||
{batchSummary.failed > 0 && (
|
{batchSummary.failed > 0 && (
|
||||||
<span className="text-destructive">{batchSummary.failed} failed</span>
|
<span className="text-destructive">{batchSummary.failed} failed</span>
|
||||||
)}
|
)}
|
||||||
{batchSummary.warnings > 0 && (
|
{batchSummary.warnings > 0 && (
|
||||||
<span className="text-amber-500">{batchSummary.warnings} with warnings</span>
|
<span className="text-[var(--color-warning-text-readable)]">{batchSummary.warnings} with warnings</span>
|
||||||
)}
|
)}
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{(batchSummary.duration / 1000).toFixed(1)}s
|
{(batchSummary.duration / 1000).toFixed(1)}s
|
||||||
@@ -937,14 +937,14 @@ export function ImportFurniClient() {
|
|||||||
{item.status === 'failed' ? (
|
{item.status === 'failed' ? (
|
||||||
<XCircle className="w-3 h-3 text-destructive mt-0.5 shrink-0" />
|
<XCircle className="w-3 h-3 text-destructive mt-0.5 shrink-0" />
|
||||||
) : (
|
) : (
|
||||||
<AlertCircle className="w-3 h-3 text-amber-500 mt-0.5 shrink-0" />
|
<AlertCircle className="w-3 h-3 text-[var(--color-warning-text-readable)] mt-0.5 shrink-0" />
|
||||||
)}
|
)}
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<span className="font-mono">{item.classname}</span>
|
<span className="font-mono">{item.classname}</span>
|
||||||
{item.message && <p className="text-destructive">{item.message}</p>}
|
{item.message && <p className="text-destructive">{item.message}</p>}
|
||||||
{item.warnings?.map((w, i) => (
|
{item.warnings?.map((w, i) => (
|
||||||
// biome-ignore lint/suspicious/noArrayIndexKey: warnings list is append-only and not reordered
|
// biome-ignore lint/suspicious/noArrayIndexKey: warnings list is append-only and not reordered
|
||||||
<p key={i} className="text-amber-500">
|
<p key={i} className="text-[var(--color-warning-text-readable)]">
|
||||||
{w}
|
{w}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
@@ -987,7 +987,7 @@ export function ImportFurniClient() {
|
|||||||
{progressStats.completed}/{progressStats.total} completed
|
{progressStats.completed}/{progressStats.total} completed
|
||||||
</span>
|
</span>
|
||||||
{progressStats.done > 0 && (
|
{progressStats.done > 0 && (
|
||||||
<span className="text-emerald-500">{progressStats.done} OK</span>
|
<span className="text-[var(--color-success-text-readable)]">{progressStats.done} OK</span>
|
||||||
)}
|
)}
|
||||||
{progressStats.failed > 0 && (
|
{progressStats.failed > 0 && (
|
||||||
<span className="text-destructive">{progressStats.failed} failed</span>
|
<span className="text-destructive">{progressStats.failed} failed</span>
|
||||||
@@ -999,7 +999,7 @@ export function ImportFurniClient() {
|
|||||||
key={item.classname}
|
key={item.classname}
|
||||||
className="flex items-center gap-2 text-xs py-1 px-2 rounded hover:bg-muted/50"
|
className="flex items-center gap-2 text-xs py-1 px-2 rounded hover:bg-muted/50"
|
||||||
>
|
>
|
||||||
{item.status === 'done' && <Check className="w-3 h-3 text-emerald-500 shrink-0" />}
|
{item.status === 'done' && <Check className="w-3 h-3 text-[var(--color-success-text-readable)] shrink-0" />}
|
||||||
{item.status === 'failed' && (
|
{item.status === 'failed' && (
|
||||||
<XCircle className="w-3 h-3 text-destructive shrink-0" />
|
<XCircle className="w-3 h-3 text-destructive shrink-0" />
|
||||||
)}
|
)}
|
||||||
@@ -1049,7 +1049,7 @@ export function ImportFurniClient() {
|
|||||||
<span>
|
<span>
|
||||||
{completed}/{arr.length}
|
{completed}/{arr.length}
|
||||||
</span>
|
</span>
|
||||||
{done > 0 && <span className="text-emerald-500">{done} OK</span>}
|
{done > 0 && <span className="text-[var(--color-success-text-readable)]">{done} OK</span>}
|
||||||
{failed > 0 && <span className="text-destructive">{failed} failed</span>}
|
{failed > 0 && <span className="text-destructive">{failed} failed</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1062,7 +1062,7 @@ export function ImportFurniClient() {
|
|||||||
<h3 className="text-sm font-semibold">Regeneration Complete</h3>
|
<h3 className="text-sm font-semibold">Regeneration Complete</h3>
|
||||||
<div className="flex gap-4 text-sm">
|
<div className="flex gap-4 text-sm">
|
||||||
{regenSummary.succeeded > 0 && (
|
{regenSummary.succeeded > 0 && (
|
||||||
<span className="text-emerald-500">{regenSummary.succeeded} regenerated</span>
|
<span className="text-[var(--color-success-text-readable)]">{regenSummary.succeeded} regenerated</span>
|
||||||
)}
|
)}
|
||||||
{regenSummary.failed > 0 && (
|
{regenSummary.failed > 0 && (
|
||||||
<span className="text-destructive">{regenSummary.failed} failed</span>
|
<span className="text-destructive">{regenSummary.failed} failed</span>
|
||||||
@@ -1305,7 +1305,7 @@ export function ImportFurniClient() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={regenAllMissing}
|
onClick={regenAllMissing}
|
||||||
disabled={batchRegenerating || regeneratingNitro.size > 0}
|
disabled={batchRegenerating || regeneratingNitro.size > 0}
|
||||||
className="h-8 text-amber-500 border-amber-500/30 hover:bg-amber-500/10"
|
className="h-8 text-[var(--color-warning-text-readable)] border-[var(--color-warning-border)] hover:bg-[var(--color-warning-subtle)]"
|
||||||
>
|
>
|
||||||
{batchRegenerating ? (
|
{batchRegenerating ? (
|
||||||
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||||
@@ -1428,11 +1428,11 @@ export function ImportFurniClient() {
|
|||||||
<td className="px-2 py-1.5">
|
<td className="px-2 py-1.5">
|
||||||
{item.alreadyImported ? (
|
{item.alreadyImported ? (
|
||||||
item.nitroExists ? (
|
item.nitroExists ? (
|
||||||
<Badge className="text-[10px] bg-emerald-500/15 text-emerald-500 border-0">
|
<Badge className="text-[10px] bg-[var(--color-success-subtle)] text-[var(--color-success-text-readable)] border-0">
|
||||||
OK
|
OK
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge className="text-[10px] bg-amber-500/15 text-amber-500 border-0">
|
<Badge className="text-[10px] bg-[var(--color-warning-subtle)] text-[var(--color-warning-text-readable)] border-0">
|
||||||
No .nitro
|
No .nitro
|
||||||
</Badge>
|
</Badge>
|
||||||
)
|
)
|
||||||
@@ -1471,7 +1471,7 @@ export function ImportFurniClient() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-6 w-6 p-0 text-amber-500"
|
className="h-6 w-6 p-0 text-[var(--color-warning-text-readable)]"
|
||||||
onClick={() => regenNitro(item)}
|
onClick={() => regenNitro(item)}
|
||||||
disabled={regeneratingNitro.has(item.classname)}
|
disabled={regeneratingNitro.has(item.classname)}
|
||||||
>
|
>
|
||||||
@@ -1576,12 +1576,12 @@ export function ImportFurniClient() {
|
|||||||
{item.alreadyImported && (
|
{item.alreadyImported && (
|
||||||
<>
|
<>
|
||||||
{item.nitroExists ? (
|
{item.nitroExists ? (
|
||||||
<Badge className="text-[10px] px-1.5 py-0 bg-emerald-500/15 text-emerald-500 border-emerald-500/20 gap-0.5 shrink-0">
|
<Badge className="text-[10px] px-1.5 py-0 bg-[var(--color-success-subtle)] text-[var(--color-success-text-readable)] border-[var(--color-success-border)] gap-0.5 shrink-0">
|
||||||
<Check className="w-2.5 h-2.5" />
|
<Check className="w-2.5 h-2.5" />
|
||||||
Imported
|
Imported
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge className="text-[10px] px-1.5 py-0 bg-amber-500/15 text-amber-500 border-amber-500/20 gap-0.5 shrink-0">
|
<Badge className="text-[10px] px-1.5 py-0 bg-[var(--color-warning-subtle)] text-[var(--color-warning-text-readable)] border-[var(--color-warning-border)] gap-0.5 shrink-0">
|
||||||
<AlertCircle className="w-2.5 h-2.5" />
|
<AlertCircle className="w-2.5 h-2.5" />
|
||||||
No .nitro
|
No .nitro
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -1603,7 +1603,7 @@ export function ImportFurniClient() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-5 w-5 p-0 shrink-0 text-amber-500"
|
className="h-5 w-5 p-0 shrink-0 text-[var(--color-warning-text-readable)]"
|
||||||
title="Regenerate .nitro file"
|
title="Regenerate .nitro file"
|
||||||
disabled={regeneratingNitro.has(item.classname)}
|
disabled={regeneratingNitro.has(item.classname)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -1770,7 +1770,7 @@ export function ImportFurniClient() {
|
|||||||
)}
|
)}
|
||||||
{reorgPreview.repriced > 0 && (
|
{reorgPreview.repriced > 0 && (
|
||||||
<div className="rounded-lg border p-3 text-center">
|
<div className="rounded-lg border p-3 text-center">
|
||||||
<p className="text-lg font-bold text-amber-500">{reorgPreview.repriced}</p>
|
<p className="text-lg font-bold text-[var(--color-warning-text-readable)]">{reorgPreview.repriced}</p>
|
||||||
<p className="text-xs text-muted-foreground">Prices to update</p>
|
<p className="text-xs text-muted-foreground">Prices to update</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ function CopyClassname({ value }: { value: string }) {
|
|||||||
>
|
>
|
||||||
<span className="truncate">{value}</span>
|
<span className="truncate">{value}</span>
|
||||||
{copied ? (
|
{copied ? (
|
||||||
<Check className="w-3 h-3 text-green-500 shrink-0" />
|
<Check className="w-3 h-3 text-[var(--color-success-text-readable)] shrink-0" />
|
||||||
) : (
|
) : (
|
||||||
<Copy className="w-3 h-3 shrink-0" />
|
<Copy className="w-3 h-3 shrink-0" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -178,8 +178,8 @@ export function ImportPetsClient() {
|
|||||||
<span className="text-muted-foreground">pets</span>
|
<span className="text-muted-foreground">pets</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
<Check className="h-4 w-4 text-emerald-500" />
|
<Check className="h-4 w-4 text-[var(--color-success-text-readable)]" />
|
||||||
<span className="font-medium text-emerald-600">{importedCount}</span>
|
<span className="font-medium text-[var(--color-success-text-readable)]">{importedCount}</span>
|
||||||
<span className="text-muted-foreground">imported</span>
|
<span className="text-muted-foreground">imported</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
@@ -313,7 +313,7 @@ export function ImportPetsClient() {
|
|||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
{p.nitroExists ? (
|
{p.nitroExists ? (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Badge className="text-[10px] px-1.5 py-0 bg-emerald-500/15 text-emerald-500 border-0 gap-0.5 flex-1 justify-center">
|
<Badge className="text-[10px] px-1.5 py-0 bg-[var(--color-success-subtle)] text-[var(--color-success-text-readable)] border-0 gap-0.5 flex-1 justify-center">
|
||||||
<Check className="w-2.5 h-2.5" />
|
<Check className="w-2.5 h-2.5" />
|
||||||
Imported
|
Imported
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ export function RepairClient() {
|
|||||||
{/* Table area */}
|
{/* Table area */}
|
||||||
{summary && summary.items.length === 0 && (
|
{summary && summary.items.length === 0 && (
|
||||||
<div className="rounded-md border bg-muted/30 p-10 text-center">
|
<div className="rounded-md border bg-muted/30 p-10 text-center">
|
||||||
<CheckCircle2 className="mx-auto mb-3 h-10 w-10 text-emerald-500" />
|
<CheckCircle2 className="mx-auto mb-3 h-10 w-10 text-[var(--color-success-text-readable)]" />
|
||||||
<p className="font-medium">All imported items have their assets on disk.</p>
|
<p className="font-medium">All imported items have their assets on disk.</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{summary.total} items scanned, nothing to repair.
|
{summary.total} items scanned, nothing to repair.
|
||||||
@@ -615,7 +615,7 @@ function StatCard({
|
|||||||
const tones = {
|
const tones = {
|
||||||
default: 'border-border',
|
default: 'border-border',
|
||||||
danger: 'border-destructive/40 bg-destructive/5',
|
danger: 'border-destructive/40 bg-destructive/5',
|
||||||
warn: 'border-amber-500/40 bg-amber-500/5',
|
warn: 'border-[var(--color-warning-border)] bg-[var(--color-warning-subtle)]',
|
||||||
info: 'border-sky-500/40 bg-sky-500/5',
|
info: 'border-sky-500/40 bg-sky-500/5',
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -631,7 +631,7 @@ function FileBadge({ present }: { present: boolean }) {
|
|||||||
return present ? (
|
return present ? (
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400"
|
className="border-[var(--color-success-border)] bg-[var(--color-success-subtle)] text-[var(--color-success-text-readable)] dark:text-[var(--color-success-text-readable)]"
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="mr-1 h-3 w-3" /> On disk
|
<CheckCircle2 className="mr-1 h-3 w-3" /> On disk
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -659,7 +659,7 @@ function StatusCell({ progress }: { progress?: ItemProgress }) {
|
|||||||
progress.iconRepaired ? 'icon ✓' : null,
|
progress.iconRepaired ? 'icon ✓' : null,
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
return (
|
return (
|
||||||
<span className="flex items-center gap-1 text-xs text-emerald-600 dark:text-emerald-400">
|
<span className="flex items-center gap-1 text-xs text-[var(--color-success-text-readable)] dark:text-[var(--color-success-text-readable)]">
|
||||||
<CheckCircle2 className="h-3 w-3" />
|
<CheckCircle2 className="h-3 w-3" />
|
||||||
{parts.length > 0 ? parts.join(' · ') : 'Repaired'}
|
{parts.length > 0 ? parts.join(' · ') : 'Repaired'}
|
||||||
</span>
|
</span>
|
||||||
@@ -668,7 +668,7 @@ function StatusCell({ progress }: { progress?: ItemProgress }) {
|
|||||||
if (progress.status === 'partial') {
|
if (progress.status === 'partial') {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400"
|
className="flex items-center gap-1 text-xs text-[var(--color-warning-text-readable)] dark:text-[var(--color-warning-text-readable)]"
|
||||||
title={progress.message}
|
title={progress.message}
|
||||||
>
|
>
|
||||||
<AlertCircle className="h-3 w-3" />
|
<AlertCircle className="h-3 w-3" />
|
||||||
|
|||||||
@@ -104,10 +104,10 @@ export default async function AdminIp() {
|
|||||||
{whitelist.map((w) => (
|
{whitelist.map((w) => (
|
||||||
<tr key={String(w.id)}>
|
<tr key={String(w.id)}>
|
||||||
<td>{w.ipAddress}</td>
|
<td>{w.ipAddress}</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{w.asn ?? "—"}
|
{w.asn ?? "—"}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{formatDate(w.createdAt)}
|
{formatDate(w.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -162,10 +162,10 @@ export default async function AdminIp() {
|
|||||||
{blacklist.map((b) => (
|
{blacklist.map((b) => (
|
||||||
<tr key={String(b.id)}>
|
<tr key={String(b.id)}>
|
||||||
<td>{b.ipAddress}</td>
|
<td>{b.ipAddress}</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{b.asn ?? "—"}
|
{b.asn ?? "—"}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{formatDate(b.createdAt)}
|
{formatDate(b.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -170,17 +170,17 @@ async function Sidebar({ staff }: { staff: { id: number; username: string; rank:
|
|||||||
const navGroups = getNavGroups(t);
|
const navGroups = getNavGroups(t);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="sticky top-0 self-start h-screen bg-gradient-to-b from-[#1e1b2e] to-[#151226] text-[#c8cbe0] shadow-xl flex flex-col">
|
<aside className="sticky top-0 self-start h-screen bg-[var(--admin-sidebar-background)] text-[var(--admin-sidebar-text-readable)] shadow-xl flex flex-col">
|
||||||
<div className="flex items-center gap-3 px-4 py-5 border-b border-white/[0.06]">
|
<div className="flex items-center gap-3 px-4 py-5 border-b border-[var(--border-subtle)]">
|
||||||
<span
|
<span
|
||||||
className="flex-none w-10 h-10 rounded-xl grid place-items-center font-extrabold text-base text-[#1a1a2e] bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-hover)] shadow-lg shadow-[var(--color-primary)]/20"
|
className="flex-none w-10 h-10 rounded-xl grid place-items-center font-extrabold text-base text-[var(--color-primary-foreground-readable)] bg-[var(--color-primary)] shadow-lg shadow-[var(--color-primary)]/20"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
{initial}
|
{initial}
|
||||||
</span>
|
</span>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="font-semibold text-sm text-white truncate">{staff.username}</div>
|
<div className="font-semibold text-sm text-[var(--admin-sidebar-text-readable)] truncate">{staff.username}</div>
|
||||||
<span className="inline-block mt-0.5 text-[0.6rem] font-bold uppercase tracking-widest text-[rgba(200,203,224,0.5)]">
|
<span className="inline-block mt-0.5 text-[0.6rem] font-bold uppercase tracking-widest text-[var(--admin-sidebar-muted-readable)]">
|
||||||
{t("rankLabel", { rank: staff.rank })}
|
{t("rankLabel", { rank: staff.rank })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -189,7 +189,7 @@ async function Sidebar({ staff }: { staff: { id: number; username: string; rank:
|
|||||||
<nav className="flex-1 overflow-y-auto px-3 py-4 space-y-1">
|
<nav className="flex-1 overflow-y-auto px-3 py-4 space-y-1">
|
||||||
{navGroups.map((group) => (
|
{navGroups.map((group) => (
|
||||||
<div key={group.label}>
|
<div key={group.label}>
|
||||||
<div className="flex items-center gap-1.5 px-2 py-2 text-[0.6rem] font-bold uppercase tracking-[0.12em] text-[rgba(200,203,224,0.3)]">
|
<div className="flex items-center gap-1.5 px-2 py-2 text-[0.6rem] font-bold uppercase tracking-[0.12em] text-[var(--admin-sidebar-muted-readable)]">
|
||||||
{group.icon}
|
{group.icon}
|
||||||
<span>{group.label}</span>
|
<span>{group.label}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,10 +204,10 @@ async function Sidebar({ staff }: { staff: { id: number; username: string; rank:
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="px-3 py-3 border-t border-white/[0.06]">
|
<div className="px-3 py-3 border-t border-[var(--border-subtle)]">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[#c8cbe0]/60 text-xs font-medium hover:text-white hover:bg-white/5 transition-all duration-150 no-underline"
|
className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[var(--admin-sidebar-muted-readable)] text-xs font-medium hover:text-[var(--admin-sidebar-text-readable)] hover:bg-[var(--color-primary)]/10 transition-all duration-150 no-underline"
|
||||||
>
|
>
|
||||||
<LogOut size={14} />
|
<LogOut size={14} />
|
||||||
<span>{t("backToSite")}</span>
|
<span>{t("backToSite")}</span>
|
||||||
@@ -233,7 +233,7 @@ async function Topbar({ staff }: { staff: { id: number; username: string; rank:
|
|||||||
<span className="text-xs text-[var(--color-text-muted)]">
|
<span className="text-xs text-[var(--color-text-muted)]">
|
||||||
<strong className="text-[var(--color-text)]">{staff.username}</strong>
|
<strong className="text-[var(--color-text)]">{staff.username}</strong>
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center gap-1.5 text-[0.6rem] font-bold uppercase tracking-wider text-[#1a1a2e] bg-[var(--color-primary)] rounded-full px-2.5 py-1">
|
<span className="inline-flex items-center gap-1.5 text-[0.6rem] font-bold uppercase tracking-wider text-[var(--color-primary-foreground-readable)] bg-[var(--color-primary)] rounded-full px-2.5 py-1">
|
||||||
{t("rankLabel", { rank: staff.rank })}
|
{t("rankLabel", { rank: staff.rank })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -140,10 +140,10 @@ export default async function AdminStaffActivities({
|
|||||||
<tbody>
|
<tbody>
|
||||||
{activities.map((a) => (
|
{activities.map((a) => (
|
||||||
<tr key={String(a.id)}>
|
<tr key={String(a.id)}>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{fromDate(a.createdAt)}
|
{fromDate(a.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{t("staffWithId", {
|
{t("staffWithId", {
|
||||||
id: Number(a.userId),
|
id: Number(a.userId),
|
||||||
name: nameById.get(Number(a.userId)) ?? "unknown",
|
name: nameById.get(Number(a.userId)) ?? "unknown",
|
||||||
@@ -157,14 +157,14 @@ export default async function AdminStaffActivities({
|
|||||||
<td>{a.description}</td>
|
<td>{a.description}</td>
|
||||||
<td>
|
<td>
|
||||||
{a.targetType && a.targetId ? (
|
{a.targetType && a.targetId ? (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
<span className="text-sm theme-text-muted dark:theme-text-muted">
|
||||||
{a.targetType === "user" ? t("userPrefix") : ""}#{String(a.targetId)}
|
{a.targetType === "user" ? t("userPrefix") : ""}#{String(a.targetId)}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">—</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">—</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{a.ipAddress || "—"}
|
{a.ipAddress || "—"}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -175,7 +175,7 @@ export default async function AdminStaffActivities({
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs theme-text-muted dark:theme-text-muted">
|
||||||
{t("pagination", { page, pages, total })}
|
{t("pagination", { page, pages, total })}
|
||||||
{page < pages ? (
|
{page < pages ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -65,13 +65,13 @@ export default async function AdminMaintenance() {
|
|||||||
<h2 className="admin-section-title">{t("currentState")}</h2>
|
<h2 className="admin-section-title">{t("currentState")}</h2>
|
||||||
<div className="admin-card flex items-center gap-3">
|
<div className="admin-card flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${enabled ? "bg-red-500/22 text-[var(--color-text)]" : "bg-green-600/20 text-[var(--color-text)]"}`}
|
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${enabled ? "bg-[var(--color-error-subtle)] text-[var(--color-text)]" : "bg-[var(--color-success-subtle)] text-[var(--color-text)]"}`}
|
||||||
>
|
>
|
||||||
{enabled ? t("enabled") : t("disabled")}
|
{enabled ? t("enabled") : t("disabled")}
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<strong>{t("maintenanceCurrently", { status: enabled ? t("enabled") : t("disabled") })}</strong>
|
<strong>{t("maintenanceCurrently", { status: enabled ? t("enabled") : t("disabled") })}</strong>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 m-0">
|
<p className="text-xs theme-text-muted dark:theme-text-muted mt-1 m-0">
|
||||||
{enabled ? t("onlyRankCanLogin", { rank: minRankValue }) : t("hotelOpenToEveryone")}
|
{enabled ? t("onlyRankCanLogin", { rank: minRankValue }) : t("hotelOpenToEveryone")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +94,7 @@ export default async function AdminMaintenance() {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="mb-4 max-w-[220px]">
|
<div className="mb-4 max-w-[220px]">
|
||||||
<label htmlFor="mt-min-rank" className="text-sm text-gray-500 dark:text-gray-400 block mb-1">
|
<label htmlFor="mt-min-rank" className="text-sm theme-text-muted dark:theme-text-muted block mb-1">
|
||||||
{t("minLoginRankLabel")}
|
{t("minLoginRankLabel")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -109,7 +109,7 @@ export default async function AdminMaintenance() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="mt-message" className="text-sm text-gray-500 dark:text-gray-400 block mb-1">
|
<label htmlFor="mt-message" className="text-sm theme-text-muted dark:theme-text-muted block mb-1">
|
||||||
{t("maintenanceMessageLabel")}
|
{t("maintenanceMessageLabel")}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default async function AdminMediaPage() {
|
|||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/admin/articles"
|
href="/admin/articles"
|
||||||
className="text-sm text-gray-500 dark:text-gray-400"
|
className="text-sm theme-text-muted dark:theme-text-muted"
|
||||||
style={{ fontSize: "0.88rem" }}
|
style={{ fontSize: "0.88rem" }}
|
||||||
>
|
>
|
||||||
← {t("articles")}
|
← {t("articles")}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ type PublicRow = {
|
|||||||
function YesNo({ on }: { on: boolean }) {
|
function YesNo({ on }: { on: boolean }) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${on ? "bg-green-600/20 text-[var(--color-text)]" : "bg-red-500/22 text-[var(--color-text)]"}`}
|
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${on ? "bg-[var(--color-success-subtle)] text-[var(--color-text)]" : "bg-[var(--color-error-subtle)] text-[var(--color-text)]"}`}
|
||||||
>
|
>
|
||||||
{on ? "Yes" : "No"}
|
{on ? "Yes" : "No"}
|
||||||
</span>
|
</span>
|
||||||
@@ -134,7 +134,7 @@ export default async function AdminNavigation() {
|
|||||||
{/* ── navigator_flatcats ───────────────────────────────── */}
|
{/* ── navigator_flatcats ───────────────────────────────── */}
|
||||||
<section className="mt-6">
|
<section className="mt-6">
|
||||||
<h2 className="admin-section-title">{t("flatCategories")}</h2>
|
<h2 className="admin-section-title">{t("flatCategories")}</h2>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs theme-text-muted dark:theme-text-muted">
|
||||||
<code>navigator_flatcats</code> · {flatcats.length} {t("rows")}
|
<code>navigator_flatcats</code> · {flatcats.length} {t("rows")}
|
||||||
</p>
|
</p>
|
||||||
{flatcats.length === 0 ? (
|
{flatcats.length === 0 ? (
|
||||||
@@ -158,9 +158,9 @@ export default async function AdminNavigation() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{flatcats.map((c) => (
|
{flatcats.map((c) => (
|
||||||
<tr key={c.id}>
|
<tr key={c.id}>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">{c.id}</td>
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">{c.id}</td>
|
||||||
<td>{c.caption || "—"}</td>
|
<td>{c.caption || "—"}</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{c.captionSave || "—"}
|
{c.captionSave || "—"}
|
||||||
</td>
|
</td>
|
||||||
<td>{c.minRank}</td>
|
<td>{c.minRank}</td>
|
||||||
@@ -184,7 +184,7 @@ export default async function AdminNavigation() {
|
|||||||
{/* ── navigator_publics ────────────────────────────────── */}
|
{/* ── navigator_publics ────────────────────────────────── */}
|
||||||
<section className="mt-6">
|
<section className="mt-6">
|
||||||
<h2 className="admin-section-title">{t("publicRooms")}</h2>
|
<h2 className="admin-section-title">{t("publicRooms")}</h2>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs theme-text-muted dark:theme-text-muted">
|
||||||
<code>navigator_publics</code> · {publics.length} {t("rows")}
|
<code>navigator_publics</code> · {publics.length} {t("rows")}
|
||||||
</p>
|
</p>
|
||||||
{publics.length === 0 ? (
|
{publics.length === 0 ? (
|
||||||
@@ -204,11 +204,11 @@ export default async function AdminNavigation() {
|
|||||||
<tr key={`${p.public_cat_id}-${p.room_id}-${i}`}>
|
<tr key={`${p.public_cat_id}-${p.room_id}-${i}`}>
|
||||||
<td>
|
<td>
|
||||||
{catNameById.get(p.public_cat_id) ?? "—"}{" "}
|
{catNameById.get(p.public_cat_id) ?? "—"}{" "}
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">#{p.public_cat_id}</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">#{p.public_cat_id}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{roomNameById.get(p.room_id) || t("unnamed")}{" "}
|
{roomNameById.get(p.room_id) || t("unnamed")}{" "}
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">#{p.room_id}</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">#{p.room_id}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<YesNo on={p.visible === "1"} />
|
<YesNo on={p.visible === "1"} />
|
||||||
|
|||||||
@@ -110,10 +110,10 @@ function PermToggle({
|
|||||||
'w-7 h-6 text-[11px] font-mono rounded transition-all font-medium',
|
'w-7 h-6 text-[11px] font-mono rounded transition-all font-medium',
|
||||||
v === value
|
v === value
|
||||||
? v === '0'
|
? v === '0'
|
||||||
? 'bg-zinc-500 text-white shadow-sm'
|
? 'bg-[var(--color-text-muted)] text-[var(--color-surface-text-readable)] shadow-sm'
|
||||||
: v === '1'
|
: v === '1'
|
||||||
? 'bg-emerald-500 text-white shadow-sm'
|
? 'bg-[var(--color-success)] text-[var(--color-success-foreground-readable)] shadow-sm'
|
||||||
: 'bg-amber-500 text-white shadow-sm'
|
: 'bg-[var(--color-warning)] text-[var(--color-warning-foreground-readable)] shadow-sm'
|
||||||
: 'bg-muted/60 text-muted-foreground/40 hover:bg-muted hover:text-muted-foreground',
|
: 'bg-muted/60 text-muted-foreground/40 hover:bg-muted hover:text-muted-foreground',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -291,7 +291,7 @@ export function RankEditClient({
|
|||||||
key={key}
|
key={key}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-between px-3 py-2 bg-card',
|
'flex items-center justify-between px-3 py-2 bg-card',
|
||||||
(perms[key] ?? '0') !== '0' && 'bg-emerald-500/5',
|
(perms[key] ?? '0') !== '0' && 'bg-[var(--color-success-subtle)]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="font-mono text-xs truncate mr-3">{key}</span>
|
<span className="font-mono text-xs truncate mr-3">{key}</span>
|
||||||
@@ -326,9 +326,9 @@ export function RankEditClient({
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{initialRank.id >= 7 ? (
|
{initialRank.id >= 7 ? (
|
||||||
<Crown className="h-6 w-6 text-yellow-500" />
|
<Crown className="h-6 w-6 text-[var(--color-warning-text-readable)]" />
|
||||||
) : initialRank.id >= 5 ? (
|
) : initialRank.id >= 5 ? (
|
||||||
<ShieldCheck className="h-6 w-6 text-blue-500" />
|
<ShieldCheck className="h-6 w-6 text-[var(--color-info-text-readable)]" />
|
||||||
) : (
|
) : (
|
||||||
<Shield className="h-6 w-6 text-muted-foreground" />
|
<Shield className="h-6 w-6 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
@@ -548,10 +548,10 @@ export function RankEditClient({
|
|||||||
<span className="w-3 h-3 rounded bg-zinc-500 inline-block" /> Off
|
<span className="w-3 h-3 rounded bg-zinc-500 inline-block" /> Off
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span className="w-3 h-3 rounded bg-emerald-500 inline-block" /> On
|
<span className="w-3 h-3 rounded bg-[var(--color-success-subtle)] inline-block" /> On
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span className="w-3 h-3 rounded bg-amber-500 inline-block" /> Owner
|
<span className="w-3 h-3 rounded bg-[var(--color-warning-subtle)] inline-block" /> Owner
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -660,7 +660,7 @@ export function RankEditClient({
|
|||||||
key={perm.id}
|
key={perm.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-3 rounded-lg border px-4 py-3 transition-all',
|
'flex items-center gap-3 rounded-lg border px-4 py-3 transition-all',
|
||||||
on ? 'border-emerald-500/40 bg-emerald-500/5' : 'hover:bg-muted/50',
|
on ? 'border-[var(--color-success-border)] bg-[var(--color-success-subtle)]' : 'hover:bg-muted/50',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Checkbox checked={on} onCheckedChange={() => toggleCms(perm.slug)} />
|
<Checkbox checked={on} onCheckedChange={() => toggleCms(perm.slug)} />
|
||||||
@@ -671,7 +671,7 @@ export function RankEditClient({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{on && (
|
{on && (
|
||||||
<Badge className="bg-emerald-500/15 text-emerald-600 border-emerald-500/30 text-[10px]">
|
<Badge className="bg-[var(--color-success-subtle)] text-[var(--color-success-text-readable)] border-[var(--color-success-border)] text-[10px]">
|
||||||
Active
|
Active
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -45,17 +45,17 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RANK_ICONS: Record<number, React.ReactNode> = {
|
const RANK_ICONS: Record<number, React.ReactNode> = {
|
||||||
7: <Crown className="h-4 w-4 text-yellow-500" />,
|
7: <Crown className="h-4 w-4 text-[var(--color-warning-text-readable)]" />,
|
||||||
6: <ShieldCheck className="h-4 w-4 text-red-500" />,
|
6: <ShieldCheck className="h-4 w-4 text-[var(--color-error-text-readable)]" />,
|
||||||
5: <Shield className="h-4 w-4 text-blue-500" />,
|
5: <Shield className="h-4 w-4 text-[var(--color-info-text-readable)]" />,
|
||||||
4: <UserCheck className="h-4 w-4 text-green-500" />,
|
4: <UserCheck className="h-4 w-4 text-[var(--color-success-text-readable)]" />,
|
||||||
}
|
}
|
||||||
|
|
||||||
const RANK_COLORS: Record<number, string> = {
|
const RANK_COLORS: Record<number, string> = {
|
||||||
7: 'border-yellow-500/50 bg-yellow-500/5',
|
7: 'border-[var(--color-warning-border)] bg-[var(--color-warning-subtle)]',
|
||||||
6: 'border-red-500/30 bg-red-500/5',
|
6: 'border-[var(--color-error-border)] bg-[var(--color-error-subtle)]',
|
||||||
5: 'border-blue-500/30 bg-blue-500/5',
|
5: 'border-[var(--color-info-border)] bg-[var(--color-info-subtle)]',
|
||||||
4: 'border-green-500/30 bg-green-500/5',
|
4: 'border-[var(--color-success-border)] bg-[var(--color-success-subtle)]',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PermissionsList({ ranks: initialRanks }: Props) {
|
export function PermissionsList({ ranks: initialRanks }: Props) {
|
||||||
|
|||||||
@@ -69,11 +69,11 @@ export default async function AdminPhotos() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong>#{String(p.id)}</strong>
|
<strong>#{String(p.id)}</strong>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
<span className="text-sm theme-text-muted dark:theme-text-muted">
|
||||||
{t("colRoom")} {p.roomId}
|
{t("colRoom")} {p.roomId}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400" style={{ margin: "0.3rem 0 0" }}>
|
<p className="text-xs theme-text-muted dark:theme-text-muted" style={{ margin: "0.3rem 0 0" }}>
|
||||||
{t("colUser")} {p.userId} ·{" "}
|
{t("colUser")} {p.userId} ·{" "}
|
||||||
{new Date(p.timestamp * 1000).toISOString().slice(0, 16).replace("T", " ")}
|
{new Date(p.timestamp * 1000).toISOString().slice(0, 16).replace("T", " ")}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export default async function AdminRadioApiKeysPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<nav className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
<nav className="text-xs theme-text-muted dark:theme-text-muted mb-2">
|
||||||
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/api-keys">{t("apiKeys")}</Link> ·{" "}
|
<Link href="/admin/radio/api-keys">{t("apiKeys")}</Link> ·{" "}
|
||||||
@@ -90,7 +90,7 @@ export default async function AdminRadioApiKeysPage() {
|
|||||||
|
|
||||||
{dbError ? (
|
{dbError ? (
|
||||||
<div className="admin-card" style={{ marginTop: "1rem" }}>
|
<div className="admin-card" style={{ marginTop: "1rem" }}>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 m-0">{t("apiKeysForm.dbError")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted m-0">{t("apiKeysForm.dbError")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||||
@@ -113,7 +113,7 @@ export default async function AdminRadioApiKeysPage() {
|
|||||||
|
|
||||||
<section className="mt-6">
|
<section className="mt-6">
|
||||||
<h2>{t("apiKeysForm.generateKey")}</h2>
|
<h2>{t("apiKeysForm.generateKey")}</h2>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{t("apiKeysForm.generateHint")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted">{t("apiKeysForm.generateHint")}</p>
|
||||||
<form action={createApiKey} className="admin-card">
|
<form action={createApiKey} className="admin-card">
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
@@ -186,28 +186,28 @@ export default async function AdminRadioApiKeysPage() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{keys.map((k) => (
|
{keys.map((k) => (
|
||||||
<tr key={String(k.id)}>
|
<tr key={String(k.id)}>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{String(k.id)}
|
{String(k.id)}
|
||||||
</td>
|
</td>
|
||||||
<td>{k.name}</td>
|
<td>{k.name}</td>
|
||||||
<td>
|
<td>
|
||||||
<code title={t("apiKeysForm.maskedHint")}>{maskKey(k.key)}</code>
|
<code title={t("apiKeysForm.maskedHint")}>{maskKey(k.key)}</code>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{k.allowedIps || t("apiKeysForm.any")}
|
{k.allowedIps || t("apiKeysForm.any")}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{k.rateLimit}/min
|
{k.rateLimit}/min
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{formatDate(k.lastUsedAt)}
|
{formatDate(k.lastUsedAt)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{formatDate(k.expiresAt)}
|
{formatDate(k.expiresAt)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${k.isActive ? "bg-green-600/20 text-[var(--color-text)]" : "bg-red-500/22 text-[var(--color-text)]"}`}
|
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${k.isActive ? "bg-[var(--color-success-subtle)] text-[var(--color-text)]" : "bg-[var(--color-error-subtle)] text-[var(--color-text)]"}`}
|
||||||
>
|
>
|
||||||
{k.isActive ? t("apiKeysForm.activeLabel") : t("apiKeysForm.inactiveLabel")}
|
{k.isActive ? t("apiKeysForm.activeLabel") : t("apiKeysForm.inactiveLabel")}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export default async function AdminRadioAutoDjPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<nav className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
<nav className="text-xs theme-text-muted dark:theme-text-muted mb-2">
|
||||||
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/banners">{t("banners.title")}</Link> ·{" "}
|
<Link href="/admin/radio/banners">{t("banners.title")}</Link> ·{" "}
|
||||||
@@ -93,7 +93,7 @@ export default async function AdminRadioAutoDjPage() {
|
|||||||
|
|
||||||
{dbError ? (
|
{dbError ? (
|
||||||
<div className="admin-card mb-6">
|
<div className="admin-card mb-6">
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 m-0">{t("autoDjForm.dbError")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted m-0">{t("autoDjForm.dbError")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||||
@@ -215,7 +215,7 @@ export default async function AdminRadioAutoDjPage() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{tracks.map((track) => (
|
{tracks.map((track) => (
|
||||||
<tr key={String(track.id)}>
|
<tr key={String(track.id)}>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{track.sortOrder}
|
{track.sortOrder}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -233,24 +233,24 @@ export default async function AdminRadioAutoDjPage() {
|
|||||||
<strong>{track.title}</strong>
|
<strong>{track.title}</strong>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{track.artist ?? "\u2014"}
|
{track.artist ?? "\u2014"}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{track.album ?? "\u2014"}
|
{track.album ?? "\u2014"}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{formatDuration(track.duration)}
|
{formatDuration(track.duration)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{track.playCount}
|
{track.playCount}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{formatDate(track.lastPlayedAt)}
|
{formatDate(track.lastPlayedAt)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${track.isActive ? "bg-green-600/20 text-[var(--color-text)]" : "bg-red-500/22 text-[var(--color-text)]"}`}
|
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${track.isActive ? "bg-[var(--color-success-subtle)] text-[var(--color-text)]" : "bg-[var(--color-error-subtle)] text-[var(--color-text)]"}`}
|
||||||
>
|
>
|
||||||
{track.isActive ? t("autoDjForm.activeLabel") : t("autoDjForm.inactiveLabel")}
|
{track.isActive ? t("autoDjForm.activeLabel") : t("autoDjForm.inactiveLabel")}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default async function AdminRadioBannersPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<nav className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
<nav className="text-xs theme-text-muted dark:theme-text-muted mb-2">
|
||||||
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/banners">{t("banners.title")}</Link> ·{" "}
|
<Link href="/admin/radio/banners">{t("banners.title")}</Link> ·{" "}
|
||||||
@@ -73,7 +73,7 @@ export default async function AdminRadioBannersPage() {
|
|||||||
|
|
||||||
{dbError ? (
|
{dbError ? (
|
||||||
<div className="admin-card mb-6">
|
<div className="admin-card mb-6">
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 m-0">{t("banners.dbError")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted m-0">{t("banners.dbError")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||||
@@ -157,10 +157,10 @@ export default async function AdminRadioBannersPage() {
|
|||||||
<div key={String(b.id)} className="admin-card">
|
<div key={String(b.id)} className="admin-card">
|
||||||
<div className="flex justify-between items-baseline gap-3">
|
<div className="flex justify-between items-baseline gap-3">
|
||||||
<strong>#{String(b.id)}</strong>
|
<strong>#{String(b.id)}</strong>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400 inline-flex gap-2 items-center">
|
<span className="text-sm theme-text-muted dark:theme-text-muted inline-flex gap-2 items-center">
|
||||||
{t("banners.byUser", { id: String(b.userId) })} · {formatDate(b.createdAt)} ·{" "}
|
{t("banners.byUser", { id: String(b.userId) })} · {formatDate(b.createdAt)} ·{" "}
|
||||||
<span
|
<span
|
||||||
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${b.isActive ? "bg-green-600/20 text-[var(--color-text)]" : "bg-red-500/22 text-[var(--color-text)]"}`}
|
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${b.isActive ? "bg-[var(--color-success-subtle)] text-[var(--color-text)]" : "bg-[var(--color-error-subtle)] text-[var(--color-text)]"}`}
|
||||||
>
|
>
|
||||||
{b.isActive ? t("banners.active") : t("banners.hidden")}
|
{b.isActive ? t("banners.active") : t("banners.hidden")}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function EmbedSnippet({
|
|||||||
{copied ? "Copied!" : "Copy"}
|
{copied ? "Copied!" : "Copy"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{description ? <p className="text-xs text-gray-500 dark:text-gray-400 mb-3">{description}</p> : null}
|
{description ? <p className="text-xs theme-text-muted dark:theme-text-muted mb-3">{description}</p> : null}
|
||||||
<textarea
|
<textarea
|
||||||
id={id}
|
id={id}
|
||||||
readOnly
|
readOnly
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default async function AdminRadioEmbedPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<nav className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
<nav className="text-xs theme-text-muted dark:theme-text-muted mb-2">
|
||||||
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/embed">{t("embed")}</Link> ·{" "}
|
<Link href="/admin/radio/embed">{t("embed")}</Link> ·{" "}
|
||||||
@@ -83,7 +83,7 @@ export default async function AdminRadioEmbedPage() {
|
|||||||
{!hasStream ? (
|
{!hasStream ? (
|
||||||
<section className="mt-6">
|
<section className="mt-6">
|
||||||
<h2 className="admin-section-title">{t("embedPage.noStreamUrl")}</h2>
|
<h2 className="admin-section-title">{t("embedPage.noStreamUrl")}</h2>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{t("embedPage.noStreamUrlText")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted">{t("embedPage.noStreamUrlText")}</p>
|
||||||
<p>
|
<p>
|
||||||
<Link href="/admin/radio/settings" className="btn btn-primary">
|
<Link href="/admin/radio/settings" className="btn btn-primary">
|
||||||
{t("embedPage.goToSettings")}
|
{t("embedPage.goToSettings")}
|
||||||
@@ -94,14 +94,14 @@ export default async function AdminRadioEmbedPage() {
|
|||||||
<>
|
<>
|
||||||
<section className="mt-6">
|
<section className="mt-6">
|
||||||
<h2 className="admin-section-title">{t("embedPage.livePreview")}</h2>
|
<h2 className="admin-section-title">{t("embedPage.livePreview")}</h2>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{t("embedPage.livePreviewText")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted">{t("embedPage.livePreviewText")}</p>
|
||||||
<div className="admin-card">
|
<div className="admin-card">
|
||||||
{}
|
{}
|
||||||
<audio controls preload="none" src={streamUrl} style={{ width: "100%" }}>
|
<audio controls preload="none" src={streamUrl} style={{ width: "100%" }}>
|
||||||
{t("embedPage.audioUnsupported")}
|
{t("embedPage.audioUnsupported")}
|
||||||
</audio>
|
</audio>
|
||||||
<p
|
<p
|
||||||
className="text-xs text-gray-500 dark:text-gray-400"
|
className="text-xs theme-text-muted dark:theme-text-muted"
|
||||||
style={{ margin: "0.5rem 0 0", wordBreak: "break-all" }}
|
style={{ margin: "0.5rem 0 0", wordBreak: "break-all" }}
|
||||||
>
|
>
|
||||||
{t("embedPage.source")}: <code>{streamUrl}</code>
|
{t("embedPage.source")}: <code>{streamUrl}</code>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export default async function AdminRadioHistoryPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<nav className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
<nav className="text-xs theme-text-muted dark:theme-text-muted mb-2">
|
||||||
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/banners">{t("banners.title")}</Link> ·{" "}
|
<Link href="/admin/radio/banners">{t("banners.title")}</Link> ·{" "}
|
||||||
@@ -93,7 +93,7 @@ export default async function AdminRadioHistoryPage() {
|
|||||||
|
|
||||||
{dbError ? (
|
{dbError ? (
|
||||||
<div className="admin-card mb-6">
|
<div className="admin-card mb-6">
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 m-0">{t("historyPage.dbError")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted m-0">{t("historyPage.dbError")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||||
@@ -133,25 +133,25 @@ export default async function AdminRadioHistoryPage() {
|
|||||||
<td>{String(h.id)}</td>
|
<td>{String(h.id)}</td>
|
||||||
<td>{names.get(String(h.userId)) ?? `#${String(h.userId)}`}</td>
|
<td>{names.get(String(h.userId)) ?? `#${String(h.userId)}`}</td>
|
||||||
<td>{h.showName ?? "\u2014"}</td>
|
<td>{h.showName ?? "\u2014"}</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{formatDate(h.startedAt)}
|
{formatDate(h.startedAt)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{h.endedAt ? (
|
{h.endedAt ? (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
<span className="text-sm theme-text-muted dark:theme-text-muted">
|
||||||
{formatDate(h.endedAt)}
|
{formatDate(h.endedAt)}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]">
|
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-success-subtle)] text-[var(--color-text)]">
|
||||||
{t("historyPage.onAir")}
|
{t("historyPage.onAir")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{formatDuration(h.startedAt, h.endedAt)}
|
{formatDuration(h.startedAt, h.endedAt)}
|
||||||
</td>
|
</td>
|
||||||
<td>{h.listenersCount}</td>
|
<td>{h.listenersCount}</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{h.notes ?? "\u2014"}
|
{h.notes ?? "\u2014"}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default async function AdminRadioModerationPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<nav className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
<nav className="text-xs theme-text-muted dark:theme-text-muted mb-2">
|
||||||
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/moderation">{t("moderation")}</Link> ·{" "}
|
<Link href="/admin/radio/moderation">{t("moderation")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
||||||
@@ -85,7 +85,7 @@ export default async function AdminRadioModerationPage() {
|
|||||||
<td>{String(s.id)}</td>
|
<td>{String(s.id)}</td>
|
||||||
<td>{String(s.userId)}</td>
|
<td>{String(s.userId)}</td>
|
||||||
<td>{s.message}</td>
|
<td>{s.message}</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{formatDate(s.createdAt)}
|
{formatDate(s.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export default async function AdminRadioMonitoringPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<nav className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
<nav className="text-xs theme-text-muted dark:theme-text-muted mb-2">
|
||||||
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/monitoring">{t("monitoring.title")}</Link> ·{" "}
|
<Link href="/admin/radio/monitoring">{t("monitoring.title")}</Link> ·{" "}
|
||||||
@@ -249,7 +249,7 @@ export default async function AdminRadioMonitoringPage() {
|
|||||||
|
|
||||||
<section className="mt-6">
|
<section className="mt-6">
|
||||||
<h2 className="admin-section-title">{t("monitoring.endpointDiagnostics")}</h2>
|
<h2 className="admin-section-title">{t("monitoring.endpointDiagnostics")}</h2>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{t("monitoring.diagnosticsLegend")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted">{t("monitoring.diagnosticsLegend")}</p>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{endpoints.map((ep) => {
|
{endpoints.map((ep) => {
|
||||||
let state: "ok" | "warn" | "danger";
|
let state: "ok" | "warn" | "danger";
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export default async function AdminRadioPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<nav className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
<nav className="text-xs theme-text-muted dark:theme-text-muted mb-2">
|
||||||
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/banners">{t("banners.title")}</Link> ·{" "}
|
<Link href="/admin/radio/banners">{t("banners.title")}</Link> ·{" "}
|
||||||
@@ -117,7 +117,7 @@ export default async function AdminRadioPage() {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<h1>{t("title")}</h1>
|
<h1>{t("title")}</h1>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{t("subtitle")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted">{t("subtitle")}</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||||
<StatusCard label={t("shouts")} value={shouts.length} icon="\u{1F4E3}" />
|
<StatusCard label={t("shouts")} value={shouts.length} icon="\u{1F4E3}" />
|
||||||
@@ -141,7 +141,7 @@ export default async function AdminRadioPage() {
|
|||||||
{t("shouts")} ({shouts.length})
|
{t("shouts")} ({shouts.length})
|
||||||
</h2>
|
</h2>
|
||||||
{shouts.length === 0 ? (
|
{shouts.length === 0 ? (
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{t("noShouts")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted">{t("noShouts")}</p>
|
||||||
) : (
|
) : (
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -156,12 +156,12 @@ export default async function AdminRadioPage() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{shouts.map((s) => (
|
{shouts.map((s) => (
|
||||||
<tr key={String(s.id)}>
|
<tr key={String(s.id)}>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{String(s.id)}
|
{String(s.id)}
|
||||||
</td>
|
</td>
|
||||||
<td>{String(s.userId)}</td>
|
<td>{String(s.userId)}</td>
|
||||||
<td>{s.message}</td>
|
<td>{s.message}</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400">{formatDate(s.createdAt)}</td>
|
<td className="text-sm theme-text-muted dark:theme-text-muted">{formatDate(s.createdAt)}</td>
|
||||||
<td>
|
<td>
|
||||||
<form action={deleteShout}>
|
<form action={deleteShout}>
|
||||||
<input type="hidden" name="id" value={String(s.id)} />
|
<input type="hidden" name="id" value={String(s.id)} />
|
||||||
@@ -182,7 +182,7 @@ export default async function AdminRadioPage() {
|
|||||||
{t("applications")} ({applications.length})
|
{t("applications")} ({applications.length})
|
||||||
</h2>
|
</h2>
|
||||||
{applications.length === 0 ? (
|
{applications.length === 0 ? (
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{t("noApplications")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted">{t("noApplications")}</p>
|
||||||
) : (
|
) : (
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -208,12 +208,12 @@ export default async function AdminRadioPage() {
|
|||||||
<td>{a.discordUsername ?? "\u2014"}</td>
|
<td>{a.discordUsername ?? "\u2014"}</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${statusBadgeClass(a.status) === "ok" ? "bg-green-600/20 text-[var(--color-text)]" : statusBadgeClass(a.status) === "danger" ? "bg-red-500/22 text-[var(--color-text)]" : "bg-[var(--color-primary)]/18 text-[var(--color-text)]"}`}
|
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${statusBadgeClass(a.status) === "ok" ? "bg-[var(--color-success-subtle)] text-[var(--color-text)]" : statusBadgeClass(a.status) === "danger" ? "bg-[var(--color-error-subtle)] text-[var(--color-text)]" : "bg-[var(--color-primary)]/18 text-[var(--color-text)]"}`}
|
||||||
>
|
>
|
||||||
{a.status}
|
{a.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400">{formatDate(a.createdAt)}</td>
|
<td className="text-sm theme-text-muted dark:theme-text-muted">{formatDate(a.createdAt)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -226,7 +226,7 @@ export default async function AdminRadioPage() {
|
|||||||
{t("schedules")} ({schedules.length})
|
{t("schedules")} ({schedules.length})
|
||||||
</h2>
|
</h2>
|
||||||
{schedules.length === 0 ? (
|
{schedules.length === 0 ? (
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{t("noSchedules")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted">{t("noSchedules")}</p>
|
||||||
) : (
|
) : (
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -251,7 +251,7 @@ export default async function AdminRadioPage() {
|
|||||||
<td>{sc.showName ?? "\u2014"}</td>
|
<td>{sc.showName ?? "\u2014"}</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${sc.isActive ? "bg-green-600/20 text-[var(--color-text)]" : "bg-red-500/22 text-[var(--color-text)]"}`}
|
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${sc.isActive ? "bg-[var(--color-success-subtle)] text-[var(--color-text)]" : "bg-[var(--color-error-subtle)] text-[var(--color-text)]"}`}
|
||||||
>
|
>
|
||||||
{sc.isActive ? t("active") : t("inactive")}
|
{sc.isActive ? t("active") : t("inactive")}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default async function AdminRadioPointsPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<nav className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
<nav className="text-xs theme-text-muted dark:theme-text-muted mb-2">
|
||||||
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/points">{t("points")}</Link> ·{" "}
|
<Link href="/admin/radio/points">{t("points")}</Link> ·{" "}
|
||||||
@@ -79,7 +79,7 @@ export default async function AdminRadioPointsPage({
|
|||||||
|
|
||||||
{saved === "1" ? (
|
{saved === "1" ? (
|
||||||
<p>
|
<p>
|
||||||
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]">
|
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-success-subtle)] text-[var(--color-text)]">
|
||||||
{t("pointsForm.saved")}
|
{t("pointsForm.saved")}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -124,7 +124,7 @@ export default async function AdminRadioPointsPage({
|
|||||||
<option value="1">{t("pointsForm.enabled")}</option>
|
<option value="1">{t("pointsForm.enabled")}</option>
|
||||||
<option value="0">{t("pointsForm.disabled")}</option>
|
<option value="0">{t("pointsForm.disabled")}</option>
|
||||||
</select>
|
</select>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400" style={{ margin: "0.2rem 0 0" }}>
|
<p className="text-xs theme-text-muted dark:theme-text-muted" style={{ margin: "0.2rem 0 0" }}>
|
||||||
{t("pointsForm.pointsEnabledHint")}
|
{t("pointsForm.pointsEnabledHint")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,7 +145,7 @@ export default async function AdminRadioPointsPage({
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400" style={{ margin: "0.2rem 0 0" }}>
|
<p className="text-xs theme-text-muted dark:theme-text-muted" style={{ margin: "0.2rem 0 0" }}>
|
||||||
{t("pointsForm.currencyHint")}
|
{t("pointsForm.currencyHint")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,7 +163,7 @@ export default async function AdminRadioPointsPage({
|
|||||||
defaultValue={perMinute}
|
defaultValue={perMinute}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400" style={{ margin: "0.2rem 0 0" }}>
|
<p className="text-xs theme-text-muted dark:theme-text-muted" style={{ margin: "0.2rem 0 0" }}>
|
||||||
{t("pointsForm.pointsPerMinuteHint")}
|
{t("pointsForm.pointsPerMinuteHint")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,7 +181,7 @@ export default async function AdminRadioPointsPage({
|
|||||||
defaultValue={maxPerDay}
|
defaultValue={maxPerDay}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400" style={{ margin: "0.2rem 0 0" }}>
|
<p className="text-xs theme-text-muted dark:theme-text-muted" style={{ margin: "0.2rem 0 0" }}>
|
||||||
{t("pointsForm.maxPerDayHint")}
|
{t("pointsForm.maxPerDayHint")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -199,7 +199,7 @@ export default async function AdminRadioPointsPage({
|
|||||||
defaultValue={minListeners}
|
defaultValue={minListeners}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400" style={{ margin: "0.2rem 0 0" }}>
|
<p className="text-xs theme-text-muted dark:theme-text-muted" style={{ margin: "0.2rem 0 0" }}>
|
||||||
{t("pointsForm.minListenersHint")}
|
{t("pointsForm.minListenersHint")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default async function AdminRadioRanksPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<nav className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
<nav className="text-xs theme-text-muted dark:theme-text-muted mb-2">
|
||||||
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/banners">{t("banners.title")}</Link> ·{" "}
|
<Link href="/admin/radio/banners">{t("banners.title")}</Link> ·{" "}
|
||||||
@@ -61,7 +61,7 @@ export default async function AdminRadioRanksPage() {
|
|||||||
|
|
||||||
{dbError ? (
|
{dbError ? (
|
||||||
<div className="admin-card mb-6">
|
<div className="admin-card mb-6">
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 m-0">{t("ranksForm.dbError")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted m-0">{t("ranksForm.dbError")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||||
@@ -122,7 +122,7 @@ export default async function AdminRadioRanksPage() {
|
|||||||
<div className="flex justify-between items-baseline gap-3 mb-2">
|
<div className="flex justify-between items-baseline gap-3 mb-2">
|
||||||
<strong>#{String(r.id)}</strong>
|
<strong>#{String(r.id)}</strong>
|
||||||
<span
|
<span
|
||||||
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${r.isActive ? "bg-green-600/20 text-[var(--color-text)]" : "bg-red-500/22 text-[var(--color-text)]"}`}
|
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${r.isActive ? "bg-[var(--color-success-subtle)] text-[var(--color-text)]" : "bg-[var(--color-error-subtle)] text-[var(--color-text)]"}`}
|
||||||
>
|
>
|
||||||
{r.isActive ? t("ranksForm.activeLabel") : t("ranksForm.inactiveLabel")}
|
{r.isActive ? t("ranksForm.activeLabel") : t("ranksForm.inactiveLabel")}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ export default async function AdminRadioSettingsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<nav className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
<nav className="text-xs theme-text-muted dark:theme-text-muted mb-2">
|
||||||
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
<Link href="/admin/radio">{t("title")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
<Link href="/admin/radio/settings">{t("settings")}</Link> ·{" "}
|
||||||
<Link href="/admin/radio/banners">{t("banners.title")}</Link> ·{" "}
|
<Link href="/admin/radio/banners">{t("banners.title")}</Link> ·{" "}
|
||||||
@@ -236,7 +236,7 @@ export default async function AdminRadioSettingsPage() {
|
|||||||
|
|
||||||
{dbError ? (
|
{dbError ? (
|
||||||
<div className="admin-card mb-6">
|
<div className="admin-card mb-6">
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 m-0">{t("settingsForm.dbError")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted m-0">{t("settingsForm.dbError")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||||
@@ -289,7 +289,7 @@ export default async function AdminRadioSettingsPage() {
|
|||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
/>
|
/>
|
||||||
{field.comment ? (
|
{field.comment ? (
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400" style={{ margin: "0.2rem 0 0" }}>
|
<p className="text-xs theme-text-muted dark:theme-text-muted" style={{ margin: "0.2rem 0 0" }}>
|
||||||
{field.comment}
|
{field.comment}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export default async function AdminRareValues() {
|
|||||||
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]">
|
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]">
|
||||||
{t("priorityLabel", { priority: cat.priority })}
|
{t("priorityLabel", { priority: cat.priority })}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
<span className="text-sm theme-text-muted dark:theme-text-muted">
|
||||||
{t("itemCount", { count: rows.length })}
|
{t("itemCount", { count: rows.length })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,7 +123,7 @@ export default async function AdminRareValues() {
|
|||||||
{rows.map((v) => (
|
{rows.map((v) => (
|
||||||
<tr key={String(v.id)}>
|
<tr key={String(v.id)}>
|
||||||
<td>{v.name}</td>
|
<td>{v.name}</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{v.itemId ?? "—"}
|
{v.itemId ?? "—"}
|
||||||
</td>
|
</td>
|
||||||
<td>{v.creditValue ?? "—"}</td>
|
<td>{v.creditValue ?? "—"}</td>
|
||||||
@@ -133,7 +133,7 @@ export default async function AdminRareValues() {
|
|||||||
{v.currencyType}
|
{v.currencyType}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{v.furnitureIcon}
|
{v.furnitureIcon}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -149,7 +149,7 @@ export default async function AdminRareValues() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{rows.length === 0 ? (
|
{rows.length === 0 ? (
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400" style={{ margin: "0.5rem 0 0" }}>
|
<p className="text-xs theme-text-muted dark:theme-text-muted" style={{ margin: "0.5rem 0 0" }}>
|
||||||
{t("noValuesInCategory")}
|
{t("noValuesInCategory")}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function getColumns(canEdit: boolean): DataTableColumn<RoomRow>[] {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<span className="flex items-center gap-1.5 text-sm">
|
<span className="flex items-center gap-1.5 text-sm">
|
||||||
{row.users > 0 && <Wifi className="h-3 w-3 text-green-500" />}
|
{row.users > 0 && <Wifi className="h-3 w-3 text-[var(--color-success-text-readable)]" />}
|
||||||
<span className={row.users > 0 ? 'font-medium' : 'text-muted-foreground'}>
|
<span className={row.users > 0 ? 'font-medium' : 'text-muted-foreground'}>
|
||||||
{row.users}
|
{row.users}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export default async function AdminShop() {
|
|||||||
<td>
|
<td>
|
||||||
{o.status ? (
|
{o.status ? (
|
||||||
<span
|
<span
|
||||||
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${state === "ok" ? "bg-green-600/20 text-[var(--color-text)]" : state === "danger" ? "bg-red-500/22 text-[var(--color-text)]" : "bg-[var(--color-primary)]/18 text-[var(--color-text)]"}`}
|
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${state === "ok" ? "bg-[var(--color-success-subtle)] text-[var(--color-text)]" : state === "danger" ? "bg-[var(--color-error-subtle)] text-[var(--color-text)]" : "bg-[var(--color-primary)]/18 text-[var(--color-text)]"}`}
|
||||||
>
|
>
|
||||||
{o.status}
|
{o.status}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -88,17 +88,17 @@ export default async function AdminSubscriptions() {
|
|||||||
const soon = s.expiresAt <= soonCutoff;
|
const soon = s.expiresAt <= soonCutoff;
|
||||||
return (
|
return (
|
||||||
<tr key={s.id}>
|
<tr key={s.id}>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">{s.id}</td>
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">{s.id}</td>
|
||||||
<td>{s.userId ?? "—"}</td>
|
<td>{s.userId ?? "—"}</td>
|
||||||
<td>{s.subscriptionType ?? "—"}</td>
|
<td>{s.subscriptionType ?? "—"}</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${soon ? "bg-[var(--color-primary)]/18 text-[var(--color-text)]" : "bg-green-600/20 text-[var(--color-text)]"}`}
|
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${soon ? "bg-[var(--color-primary)]/18 text-[var(--color-text)]" : "bg-[var(--color-success-subtle)] text-[var(--color-text)]"}`}
|
||||||
>
|
>
|
||||||
{soon ? t("statusExpiring") : t("statusActive")}
|
{soon ? t("statusExpiring") : t("statusActive")}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{fmt(s.expiresAt)}
|
{fmt(s.expiresAt)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default async function AdminTagsPage() {
|
|||||||
|
|
||||||
{dbError ? (
|
{dbError ? (
|
||||||
<div className="admin-card" style={{ marginBottom: "1.5rem" }}>
|
<div className="admin-card" style={{ marginBottom: "1.5rem" }}>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 m-0">{t("databaseError")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted m-0">{t("databaseError")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||||
@@ -130,7 +130,7 @@ export default async function AdminTagsPage() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<tr key={String(tag.id)}>
|
<tr key={String(tag.id)}>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{String(tag.id)}
|
{String(tag.id)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -146,21 +146,21 @@ export default async function AdminTagsPage() {
|
|||||||
{tag.name}
|
{tag.name}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
<code>{tag.backgroundColor}</code>
|
<code>{tag.backgroundColor}</code>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${
|
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${
|
||||||
(usage.get(String(tag.id)) ?? 0) > 0
|
(usage.get(String(tag.id)) ?? 0) > 0
|
||||||
? "bg-green-600/20 text-[var(--color-text)]"
|
? "bg-[var(--color-success-subtle)] text-[var(--color-text)]"
|
||||||
: "bg-[var(--color-primary)]/18 text-[var(--color-text)]"
|
: "bg-[var(--color-primary)]/18 text-[var(--color-text)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{usage.get(String(tag.id)) ?? 0}
|
{usage.get(String(tag.id)) ?? 0}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{formatDate(tag.createdAt)}
|
{formatDate(tag.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -64,25 +64,25 @@ export default async function AdminTeams() {
|
|||||||
<form action={createTeam} style={{ display: "grid", gap: "0.6rem" }}>
|
<form action={createTeam} style={{ display: "grid", gap: "0.6rem" }}>
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<label style={{ display: "grid", gap: "0.25rem" }}>
|
<label style={{ display: "grid", gap: "0.25rem" }}>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">{t("rankName")}</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">{t("rankName")}</span>
|
||||||
<input name="rankName" placeholder={t("rankNamePlaceholder")} required />
|
<input name="rankName" placeholder={t("rankNamePlaceholder")} required />
|
||||||
</label>
|
</label>
|
||||||
<label style={{ display: "grid", gap: "0.25rem" }}>
|
<label style={{ display: "grid", gap: "0.25rem" }}>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">{t("badgeCode")}</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">{t("badgeCode")}</span>
|
||||||
<input name="badge" placeholder={t("badgeCodePlaceholder")} />
|
<input name="badge" placeholder={t("badgeCodePlaceholder")} />
|
||||||
</label>
|
</label>
|
||||||
<label style={{ display: "grid", gap: "0.25rem" }}>
|
<label style={{ display: "grid", gap: "0.25rem" }}>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">{t("jobDescription")}</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">{t("jobDescription")}</span>
|
||||||
<input name="jobDescription" placeholder={t("jobDescriptionPlaceholder")} />
|
<input name="jobDescription" placeholder={t("jobDescriptionPlaceholder")} />
|
||||||
</label>
|
</label>
|
||||||
<label style={{ display: "grid", gap: "0.25rem" }}>
|
<label style={{ display: "grid", gap: "0.25rem" }}>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">{t("staffColor")}</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">{t("staffColor")}</span>
|
||||||
<input name="staffColor" defaultValue="#327fa8" placeholder={t("staffColorPlaceholder")} />
|
<input name="staffColor" defaultValue="#327fa8" placeholder={t("staffColorPlaceholder")} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}>
|
<label style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}>
|
||||||
<input type="checkbox" name="hiddenRank" style={{ width: "auto" }} />
|
<input type="checkbox" name="hiddenRank" style={{ width: "auto" }} />
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">{t("hiddenRank")}</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">{t("hiddenRank")}</span>
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" className="btn btn-primary">
|
<button type="submit" className="btn btn-primary">
|
||||||
@@ -113,14 +113,14 @@ export default async function AdminTeams() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{teams.map((team) => (
|
{teams.map((team) => (
|
||||||
<tr key={String(team.id)}>
|
<tr key={String(team.id)}>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{String(team.id)}
|
{String(team.id)}
|
||||||
</td>
|
</td>
|
||||||
<td>{team.rankName}</td>
|
<td>{team.rankName}</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{team.badge ?? "—"}
|
{team.badge ?? "—"}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{team.jobDescription ?? "—"}
|
{team.jobDescription ?? "—"}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -146,7 +146,7 @@ export default async function AdminTeams() {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${team.hiddenRank ? "bg-[var(--color-primary)]/18 text-[var(--color-text)]" : "bg-green-600/20 text-[var(--color-text)]"}`}
|
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${team.hiddenRank ? "bg-[var(--color-primary)]/18 text-[var(--color-text)]" : "bg-[var(--color-success-subtle)] text-[var(--color-text)]"}`}
|
||||||
>
|
>
|
||||||
{team.hiddenRank ? t("hidden") : t("visible")}
|
{team.hiddenRank ? t("hidden") : t("visible")}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -124,12 +124,12 @@ export default async function AdminTheme({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sp.saved ? (
|
{sp.saved ? (
|
||||||
<div className="mb-4 p-3 rounded-lg bg-green-600/10 text-green-600 dark:text-green-400 text-xs">
|
<div className="mb-4 p-3 rounded-lg theme-status-success text-xs">
|
||||||
{t("saved")}
|
{t("saved")}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{sp.preset ? (
|
{sp.preset ? (
|
||||||
<div className="mb-4 p-3 rounded-lg bg-green-600/10 text-green-600 dark:text-green-400 text-xs">
|
<div className="mb-4 p-3 rounded-lg theme-status-success text-xs">
|
||||||
{t("presetApplied", { preset: sp.preset })}
|
{t("presetApplied", { preset: sp.preset })}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -57,17 +57,17 @@ interface StaffMember {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_COLORS: Record<string, string> = {
|
const STATUS_COLORS: Record<string, string> = {
|
||||||
open: 'bg-blue-500/10 text-blue-500',
|
open: 'bg-[var(--color-info-subtle)] text-[var(--color-info-text-readable)]',
|
||||||
in_progress: 'bg-yellow-500/10 text-yellow-500',
|
in_progress: 'bg-[var(--color-warning-subtle)] text-[var(--color-warning-text-readable)]',
|
||||||
waiting: 'bg-orange-500/10 text-orange-500',
|
waiting: 'bg-[var(--color-warning-subtle)] text-[var(--color-warning-text-readable)]',
|
||||||
closed: 'bg-muted text-muted-foreground',
|
closed: 'bg-muted text-muted-foreground',
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRIORITY_COLORS: Record<string, string> = {
|
const PRIORITY_COLORS: Record<string, string> = {
|
||||||
low: 'text-muted-foreground',
|
low: 'text-muted-foreground',
|
||||||
normal: 'text-blue-500',
|
normal: 'text-[var(--color-info-text-readable)]',
|
||||||
high: 'text-orange-500',
|
high: 'text-[var(--color-warning-text-readable)]',
|
||||||
urgent: 'text-red-500',
|
urgent: 'text-[var(--color-error-text-readable)]',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AdminTicketDetail({
|
export function AdminTicketDetail({
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ import { canAccess, getAdminContext, PERMS } from '@/lib/permissions'
|
|||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
|
|
||||||
const STATUS_CONFIG = {
|
const STATUS_CONFIG = {
|
||||||
open: { label: 'Open', icon: AlertCircle, class: 'bg-blue-500/10 text-blue-500' },
|
open: { label: 'Open', icon: AlertCircle, class: 'bg-[var(--color-info-subtle)] text-[var(--color-info-text-readable)]' },
|
||||||
in_progress: { label: 'In Progress', icon: Loader2, class: 'bg-yellow-500/10 text-yellow-500' },
|
in_progress: { label: 'In Progress', icon: Loader2, class: 'bg-[var(--color-warning-subtle)] text-[var(--color-warning-text-readable)]' },
|
||||||
waiting: { label: 'Waiting', icon: Clock, class: 'bg-orange-500/10 text-orange-500' },
|
waiting: { label: 'Waiting', icon: Clock, class: 'bg-[var(--color-warning-subtle)] text-[var(--color-warning-text-readable)]' },
|
||||||
closed: { label: 'Closed', icon: CheckCircle2, class: 'bg-muted text-muted-foreground' },
|
closed: { label: 'Closed', icon: CheckCircle2, class: 'bg-muted text-muted-foreground' },
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const PRIORITY_COLORS = {
|
const PRIORITY_COLORS = {
|
||||||
low: '',
|
low: '',
|
||||||
normal: 'text-blue-500',
|
normal: 'text-[var(--color-info-text-readable)]',
|
||||||
high: 'text-orange-500',
|
high: 'text-[var(--color-warning-text-readable)]',
|
||||||
urgent: 'text-red-500 font-bold',
|
urgent: 'text-[var(--color-error-text-readable)] font-bold',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
@@ -77,7 +77,7 @@ export default async function AdminTicketsPage({ searchParams }: PageProps) {
|
|||||||
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-blue-500">Open</CardTitle>
|
<CardTitle className="text-sm font-medium text-[var(--color-info-text-readable)]">Open</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">{openCount}</div>
|
<div className="text-2xl font-bold">{openCount}</div>
|
||||||
@@ -85,7 +85,7 @@ export default async function AdminTicketsPage({ searchParams }: PageProps) {
|
|||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-yellow-500">In Progress</CardTitle>
|
<CardTitle className="text-sm font-medium text-[var(--color-warning-text-readable)]">In Progress</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">{inProgressCount}</div>
|
<div className="text-2xl font-bold">{inProgressCount}</div>
|
||||||
@@ -93,7 +93,7 @@ export default async function AdminTicketsPage({ searchParams }: PageProps) {
|
|||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-orange-500">Waiting</CardTitle>
|
<CardTitle className="text-sm font-medium text-[var(--color-warning-text-readable)]">Waiting</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">{waitingCount}</div>
|
<div className="text-2xl font-bold">{waitingCount}</div>
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export default async function AdminTransactions({
|
|||||||
const state = statusState(x.status);
|
const state = statusState(x.status);
|
||||||
return (
|
return (
|
||||||
<tr key={String(x.id)}>
|
<tr key={String(x.id)}>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{String(x.id)}
|
{String(x.id)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -141,18 +141,18 @@ export default async function AdminTransactions({
|
|||||||
<td>
|
<td>
|
||||||
{x.status ? (
|
{x.status ? (
|
||||||
<span
|
<span
|
||||||
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${state === "ok" ? "bg-green-600/20 text-[var(--color-text)]" : state === "danger" ? "bg-red-500/22 text-[var(--color-text)]" : "bg-[var(--color-primary)]/18 text-[var(--color-text)]"}`}
|
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${state === "ok" ? "bg-[var(--color-success-subtle)] text-[var(--color-text)]" : state === "danger" ? "bg-[var(--color-error-subtle)] text-[var(--color-text)]" : "bg-[var(--color-primary)]/18 text-[var(--color-text)]"}`}
|
||||||
>
|
>
|
||||||
{x.status}
|
{x.status}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">—</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">—</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{x.transactionId}
|
{x.transactionId}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{fromDate(x.createdAt)}
|
{fromDate(x.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -164,7 +164,7 @@ export default async function AdminTransactions({
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs theme-text-muted dark:theme-text-muted">
|
||||||
{t("pagination", { page, pages, total })}
|
{t("pagination", { page, pages, total })}
|
||||||
{page > 1 ? (
|
{page > 1 ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export function ClientTranslations({ files, activeFile, entries, parseError }: P
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeFile.readOnly && (
|
{activeFile.readOnly && (
|
||||||
<div className="rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-xs text-amber-700 dark:text-amber-400 flex items-start gap-2">
|
<div className="rounded-md border border-[var(--color-warning-border)] bg-[var(--color-warning-subtle)] px-3 py-2 text-xs text-[var(--color-warning-text-readable)] dark:text-[var(--color-warning-text-readable)] flex items-start gap-2">
|
||||||
<Lock className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
<Lock className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
Questo file è di sola lettura.{' '}
|
Questo file è di sola lettura.{' '}
|
||||||
@@ -180,7 +180,7 @@ export function ClientTranslations({ files, activeFile, entries, parseError }: P
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeFile.hasComments && !activeFile.readOnly && (
|
{activeFile.hasComments && !activeFile.readOnly && (
|
||||||
<div className="rounded-md border border-blue-500/30 bg-blue-500/10 px-3 py-2 text-xs text-blue-700 dark:text-blue-400 flex items-start gap-2">
|
<div className="rounded-md border border-[var(--color-info-border)] bg-[var(--color-info-subtle)] px-3 py-2 text-xs text-[var(--color-info-text-readable)] dark:text-[var(--color-info-text-readable)] flex items-start gap-2">
|
||||||
<Info className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
<Info className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
I commenti vengono preservati: il salvataggio modifica solo le righe dei valori
|
I commenti vengono preservati: il salvataggio modifica solo le righe dei valori
|
||||||
|
|||||||
@@ -63,11 +63,11 @@ export default async function AdminUserEdit({ params }: { params: Promise<{ id:
|
|||||||
} catch {
|
} catch {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
<p className="text-sm theme-text-muted dark:theme-text-muted">
|
||||||
<Link href="/admin/users">← {t("title")}</Link>
|
<Link href="/admin/users">← {t("title")}</Link>
|
||||||
</p>
|
</p>
|
||||||
<h1>{t("title")}</h1>
|
<h1>{t("title")}</h1>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{t("loadError")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted">{t("loadError")}</p>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ export default async function AdminUserEdit({ params }: { params: Promise<{ id:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
<p className="text-sm theme-text-muted dark:theme-text-muted mb-2">
|
||||||
<Link href={`/admin/users/${user.id}`}>← {user.username}</Link>
|
<Link href={`/admin/users/${user.id}`}>← {user.username}</Link>
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-3 mb-6">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
@@ -141,7 +141,7 @@ export default async function AdminUserEdit({ params }: { params: Promise<{ id:
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{t("hint")}</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted">{t("hint")}</p>
|
||||||
|
|
||||||
<div className="flex gap-2 mt-2">
|
<div className="flex gap-2 mt-2">
|
||||||
<button type="submit" className="btn btn-primary">
|
<button type="submit" className="btn btn-primary">
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export function UserQuickActions({ userId, username, isOnline }: UserQuickAction
|
|||||||
<Unplug className="mr-2 h-4 w-4" />
|
<Unplug className="mr-2 h-4 w-4" />
|
||||||
Disconnect
|
Disconnect
|
||||||
{isOnline && (
|
{isOnline && (
|
||||||
<Badge variant="default" className="ml-auto text-[10px] bg-green-600">
|
<Badge variant="default" className="ml-auto text-[10px] bg-[var(--color-success-subtle)]">
|
||||||
Online
|
Online
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -209,7 +209,7 @@ export function UserQuickActions({ userId, username, isOnline }: UserQuickAction
|
|||||||
{newPassword}
|
{newPassword}
|
||||||
</code>
|
</code>
|
||||||
<Button variant="ghost" size="icon" onClick={copyPassword}>
|
<Button variant="ghost" size="icon" onClick={copyPassword}>
|
||||||
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
{copied ? <Check className="h-4 w-4 text-[var(--color-success-text-readable)]" /> : <Copy className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -82,10 +82,10 @@ export function UserHero({
|
|||||||
className={`absolute -top-1 -right-1 flex h-4 w-4 ${isOnline ? '' : 'opacity-50'}`}
|
className={`absolute -top-1 -right-1 flex h-4 w-4 ${isOnline ? '' : 'opacity-50'}`}
|
||||||
>
|
>
|
||||||
{isOnline && (
|
{isOnline && (
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--color-success)] opacity-75" />
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
className={`relative inline-flex rounded-full h-4 w-4 border-2 border-card ${isOnline ? 'bg-green-500' : 'bg-zinc-400'}`}
|
className={`relative inline-flex rounded-full h-4 w-4 border-2 border-card ${isOnline ? 'bg-[var(--color-success-subtle)]' : 'bg-zinc-400'}`}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,7 +129,7 @@ export function UserHero({
|
|||||||
<Badge>{user.permission.rankName || `Rank ${user.rank}`}</Badge>
|
<Badge>{user.permission.rankName || `Rank ${user.rank}`}</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
variant={isOnline ? 'default' : 'outline'}
|
variant={isOnline ? 'default' : 'outline'}
|
||||||
className={isOnline ? 'bg-green-600' : ''}
|
className={isOnline ? 'bg-[var(--color-success-subtle)]' : ''}
|
||||||
>
|
>
|
||||||
{isOnline ? (
|
{isOnline ? (
|
||||||
<>
|
<>
|
||||||
@@ -143,7 +143,7 @@ export function UserHero({
|
|||||||
</Badge>
|
</Badge>
|
||||||
{activeBan && <Badge variant="destructive">Banned</Badge>}
|
{activeBan && <Badge variant="destructive">Banned</Badge>}
|
||||||
{user.twoFactorConfirmed ? (
|
{user.twoFactorConfirmed ? (
|
||||||
<Badge variant="outline" className="border-green-500/50 text-green-600">
|
<Badge variant="outline" className="border-[var(--color-success-border)] text-[var(--color-success-text-readable)]">
|
||||||
<Shield className="mr-1 h-3 w-3" /> 2FA
|
<Shield className="mr-1 h-3 w-3" /> 2FA
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -152,12 +152,12 @@ export function UserHero({
|
|||||||
{/* Currency row */}
|
{/* Currency row */}
|
||||||
<div className="flex flex-wrap gap-3 pt-1">
|
<div className="flex flex-wrap gap-3 pt-1">
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
<Coins className="h-4 w-4 text-yellow-500" />
|
<Coins className="h-4 w-4 text-[var(--color-warning-text-readable)]" />
|
||||||
<span className="font-semibold">{user.credits.toLocaleString()}</span>
|
<span className="font-semibold">{user.credits.toLocaleString()}</span>
|
||||||
<span className="text-muted-foreground text-xs">credits</span>
|
<span className="text-muted-foreground text-xs">credits</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
<Diamond className="h-4 w-4 text-blue-400" />
|
<Diamond className="h-4 w-4 text-[var(--color-info-text-readable)]" />
|
||||||
<span className="font-semibold">{diamonds.toLocaleString()}</span>
|
<span className="font-semibold">{diamonds.toLocaleString()}</span>
|
||||||
<span className="text-muted-foreground text-xs">diamonds</span>
|
<span className="text-muted-foreground text-xs">diamonds</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export default async function MultiAccountsPage({
|
|||||||
<span className="text-xs text-muted-foreground">#{acc.id}</span>
|
<span className="text-xs text-muted-foreground">#{acc.id}</span>
|
||||||
{acc.rank >= 5 && <Badge variant="outline">staff</Badge>}
|
{acc.rank >= 5 && <Badge variant="outline">staff</Badge>}
|
||||||
{acc.online === '1' && (
|
{acc.online === '1' && (
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-green-500" />
|
<span className="h-1.5 w-1.5 rounded-full bg-[var(--color-success-subtle)]" />
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ function getColumns(canEdit: boolean): DataTableColumn<UserRow>[] {
|
|||||||
],
|
],
|
||||||
render: (value) =>
|
render: (value) =>
|
||||||
value === '1' ? (
|
value === '1' ? (
|
||||||
<span className="flex items-center gap-1.5 text-green-600">
|
<span className="flex items-center gap-1.5 text-[var(--color-success-text-readable)]">
|
||||||
<Wifi className="h-3.5 w-3.5" /> Online
|
<Wifi className="h-3.5 w-3.5" /> Online
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -85,19 +85,19 @@ export default async function AdminVouchers() {
|
|||||||
<h3 style={{ marginTop: 0 }}>{t("newVoucher")}</h3>
|
<h3 style={{ marginTop: 0 }}>{t("newVoucher")}</h3>
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2" style={{ marginBottom: "0.75rem" }}>
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2" style={{ marginBottom: "0.75rem" }}>
|
||||||
<label style={{ display: "flex", flexDirection: "column", gap: "0.3rem" }}>
|
<label style={{ display: "flex", flexDirection: "column", gap: "0.3rem" }}>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">{t("form.code")}</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">{t("form.code")}</span>
|
||||||
<input name="code" placeholder={t("form.codePlaceholder")} maxLength={255} required />
|
<input name="code" placeholder={t("form.codePlaceholder")} maxLength={255} required />
|
||||||
</label>
|
</label>
|
||||||
<label style={{ display: "flex", flexDirection: "column", gap: "0.3rem" }}>
|
<label style={{ display: "flex", flexDirection: "column", gap: "0.3rem" }}>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">{t("form.amount")}</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">{t("form.amount")}</span>
|
||||||
<input name="amount" type="number" min={1} placeholder={t("form.amountPlaceholder")} required />
|
<input name="amount" type="number" min={1} placeholder={t("form.amountPlaceholder")} required />
|
||||||
</label>
|
</label>
|
||||||
<label style={{ display: "flex", flexDirection: "column", gap: "0.3rem" }}>
|
<label style={{ display: "flex", flexDirection: "column", gap: "0.3rem" }}>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">{t("form.maxUses")}</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">{t("form.maxUses")}</span>
|
||||||
<input name="maxUses" type="number" min={1} defaultValue={1} required />
|
<input name="maxUses" type="number" min={1} defaultValue={1} required />
|
||||||
</label>
|
</label>
|
||||||
<label style={{ display: "flex", flexDirection: "column", gap: "0.3rem" }}>
|
<label style={{ display: "flex", flexDirection: "column", gap: "0.3rem" }}>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">{t("form.expiresAt")}</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">{t("form.expiresAt")}</span>
|
||||||
<input name="expiresAt" type="datetime-local" />
|
<input name="expiresAt" type="datetime-local" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,7 +138,7 @@ export default async function AdminVouchers() {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{expired ? (
|
{expired ? (
|
||||||
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-red-500/22 text-[var(--color-text)]">
|
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-error-subtle)] text-[var(--color-text)]">
|
||||||
{t("statusExpired")}
|
{t("statusExpired")}
|
||||||
</span>
|
</span>
|
||||||
) : usedUp ? (
|
) : usedUp ? (
|
||||||
@@ -146,15 +146,15 @@ export default async function AdminVouchers() {
|
|||||||
{t("statusUsedUp")}
|
{t("statusUsedUp")}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]">
|
<span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-success-subtle)] text-[var(--color-text)]">
|
||||||
{t("statusActive")}
|
{t("statusActive")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{fmtDate(v.expiresAt)}
|
{fmtDate(v.expiresAt)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{fmtDate(v.createdAt)}
|
{fmtDate(v.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default async function AdminVpn({ searchParams }: { searchParams: Promise
|
|||||||
|
|
||||||
<section className="mt-6">
|
<section className="mt-6">
|
||||||
{sp.saved ? (
|
{sp.saved ? (
|
||||||
<p className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]">
|
<p className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-success-subtle)] text-[var(--color-text)]">
|
||||||
{t("settingsSaved")}
|
{t("settingsSaved")}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -83,7 +83,7 @@ export default async function AdminVpn({ searchParams }: { searchParams: Promise
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400" style={{ margin: "0.2rem 0 0" }}>
|
<p className="text-xs theme-text-muted dark:theme-text-muted" style={{ margin: "0.2rem 0 0" }}>
|
||||||
{t("providerHint")}
|
{t("providerHint")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,7 +100,7 @@ export default async function AdminVpn({ searchParams }: { searchParams: Promise
|
|||||||
placeholder={t("apiKeyPlaceholder")}
|
placeholder={t("apiKeyPlaceholder")}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400" style={{ margin: "0.2rem 0 0" }}>
|
<p className="text-xs theme-text-muted dark:theme-text-muted" style={{ margin: "0.2rem 0 0" }}>
|
||||||
{t("apiKeyHint")}
|
{t("apiKeyHint")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,7 +120,7 @@ export default async function AdminVpn({ searchParams }: { searchParams: Promise
|
|||||||
placeholder={t("blockMessagePlaceholder")}
|
placeholder={t("blockMessagePlaceholder")}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400" style={{ margin: "0.2rem 0 0" }}>
|
<p className="text-xs theme-text-muted dark:theme-text-muted" style={{ margin: "0.2rem 0 0" }}>
|
||||||
{t("blockMessageHint")}
|
{t("blockMessageHint")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,7 +135,7 @@ export default async function AdminVpn({ searchParams }: { searchParams: Promise
|
|||||||
|
|
||||||
<section className="mt-6">
|
<section className="mt-6">
|
||||||
<h2 className="admin-section-title">{t("rawIpTitle")}</h2>
|
<h2 className="admin-section-title">{t("rawIpTitle")}</h2>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs theme-text-muted dark:theme-text-muted">
|
||||||
{t.rich("rawIpDescription", { ipLink: (chunks) => <Link href="/admin/ip">{chunks}</Link> })}
|
{t.rich("rawIpDescription", { ipLink: (chunks) => <Link href="/admin/ip">{chunks}</Link> })}
|
||||||
</p>
|
</p>
|
||||||
<Link href="/admin/ip" className="btn btn-secondary">
|
<Link href="/admin/ip" className="btn btn-secondary">
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export default async function AdminWordFilter() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{words.map((w) => (
|
{words.map((w) => (
|
||||||
<tr key={String(w.id)}>
|
<tr key={String(w.id)}>
|
||||||
<td className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
|
||||||
{String(w.id)}
|
{String(w.id)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -114,11 +114,11 @@ export default async function AdminWriteableBoxes() {
|
|||||||
marginBottom: "0.75rem",
|
marginBottom: "0.75rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">#{String(box.id)}</span>
|
<span className="text-sm theme-text-muted dark:theme-text-muted">#{String(box.id)}</span>
|
||||||
{box.icon ? <span aria-hidden>{box.icon}</span> : null}
|
{box.icon ? <span aria-hidden>{box.icon}</span> : null}
|
||||||
<strong className="flex-1">{box.title}</strong>
|
<strong className="flex-1">{box.title}</strong>
|
||||||
<span
|
<span
|
||||||
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${box.isActive ? "bg-green-600/20 text-[var(--color-text)]" : "bg-red-500/22 text-[var(--color-text)]"}`}
|
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${box.isActive ? "bg-[var(--color-success-subtle)] text-[var(--color-text)]" : "bg-[var(--color-error-subtle)] text-[var(--color-text)]"}`}
|
||||||
>
|
>
|
||||||
{box.isActive ? t("active") : t("hidden")}
|
{box.isActive ? t("active") : t("hidden")}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -117,6 +117,23 @@ body {
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Semantic colors: safe across every complete light/dark preset. */
|
||||||
|
.theme-text-muted { color: var(--color-text-muted-readable); }
|
||||||
|
.theme-text-subtle { color: var(--color-text-subtle-readable); }
|
||||||
|
.theme-text-disabled { color: var(--color-text-disabled-readable); }
|
||||||
|
.theme-status-success,
|
||||||
|
.theme-status-warning,
|
||||||
|
.theme-status-error,
|
||||||
|
.theme-status-info {
|
||||||
|
border: 1px solid var(--status-border);
|
||||||
|
background: var(--status-background);
|
||||||
|
color: var(--status-text);
|
||||||
|
}
|
||||||
|
.theme-status-success { --status-border: var(--color-success-border); --status-background: var(--color-success-subtle); --status-text: var(--color-success-text-readable); }
|
||||||
|
.theme-status-warning { --status-border: var(--color-warning-border); --status-background: var(--color-warning-subtle); --status-text: var(--color-warning-text-readable); }
|
||||||
|
.theme-status-error { --status-border: var(--color-error-border); --status-background: var(--color-error-subtle); --status-text: var(--color-error-text-readable); }
|
||||||
|
.theme-status-info { --status-border: var(--color-info-border); --status-background: var(--color-info-subtle); --status-text: var(--color-info-text-readable); }
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
border: 4px solid transparent;
|
border: 4px solid transparent;
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ export function AdminNavLink({
|
|||||||
href={href}
|
href={href}
|
||||||
className={`flex items-center gap-2.5 px-2.5 py-2 rounded-xl text-sm font-medium no-underline transition-all duration-150 ${
|
className={`flex items-center gap-2.5 px-2.5 py-2 rounded-xl text-sm font-medium no-underline transition-all duration-150 ${
|
||||||
active
|
active
|
||||||
? "bg-gradient-to-r from-[var(--color-primary)]/20 to-transparent text-white shadow-sm border-l-2 border-[var(--color-primary)]"
|
? "bg-[var(--color-primary)]/15 text-[var(--admin-sidebar-text-readable)] shadow-sm border-l-2 border-[var(--color-primary)]"
|
||||||
: "text-[#8b8fa8] hover:text-white hover:bg-white/[0.04] hover:no-underline"
|
: "text-[var(--admin-sidebar-muted-readable)] hover:text-[var(--admin-sidebar-text-readable)] hover:bg-[var(--color-primary)]/10 hover:no-underline"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{icon ? (
|
{icon ? (
|
||||||
<span className={`flex-none ${active ? "text-[var(--color-primary)]" : "text-[#5c5f7a]"}`}>
|
<span className={`flex-none ${active ? "text-[var(--color-primary-readable)]" : "text-[var(--admin-sidebar-muted-readable)]"}`}>
|
||||||
{icon}
|
{icon}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
|||||||
{isDirty && (
|
{isDirty && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-4 px-1.5 text-[9px] text-orange-600 border-orange-300 bg-orange-50 dark:bg-orange-950/30 dark:border-orange-700"
|
className="h-4 px-1.5 text-[9px] text-[var(--color-warning-text-readable)] border-[var(--color-warning-border)] bg-[var(--color-warning-subtle)]"
|
||||||
>
|
>
|
||||||
<AlertCircle className="h-2.5 w-2.5 mr-0.5" />
|
<AlertCircle className="h-2.5 w-2.5 mr-0.5" />
|
||||||
unsaved
|
unsaved
|
||||||
@@ -310,7 +310,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
|||||||
|
|
||||||
{/* Dirty banner */}
|
{/* Dirty banner */}
|
||||||
{isDirty && (
|
{isDirty && (
|
||||||
<div className="flex items-center gap-2 px-4 py-1.5 bg-yellow-50 dark:bg-yellow-950/30 border-b border-yellow-200 dark:border-yellow-800 text-xs text-yellow-800 dark:text-yellow-200 shrink-0">
|
<div className="flex items-center gap-2 px-4 py-1.5 bg-[var(--color-warning-subtle)] border-b border-[var(--color-warning-border)] text-xs text-[var(--color-warning-text-readable)] shrink-0">
|
||||||
<AlertCircle className="h-3.5 w-3.5 shrink-0" />
|
<AlertCircle className="h-3.5 w-3.5 shrink-0" />
|
||||||
<span className="flex-1">
|
<span className="flex-1">
|
||||||
Unsaved changes — press{' '}
|
Unsaved changes — press{' '}
|
||||||
@@ -340,7 +340,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
|||||||
<TabsTrigger value="settings" className="gap-1.5 text-xs">
|
<TabsTrigger value="settings" className="gap-1.5 text-xs">
|
||||||
<Settings className="h-3.5 w-3.5" />
|
<Settings className="h-3.5 w-3.5" />
|
||||||
Settings
|
Settings
|
||||||
{isDirty && <span className="ml-0.5 h-1.5 w-1.5 rounded-full bg-orange-500" />}
|
{isDirty && <span className="ml-0.5 h-1.5 w-1.5 rounded-full bg-[var(--color-warning)]" />}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="items" className="gap-1.5 text-xs">
|
<TabsTrigger value="items" className="gap-1.5 text-xs">
|
||||||
<Package className="h-3.5 w-3.5" />
|
<Package className="h-3.5 w-3.5" />
|
||||||
@@ -359,7 +359,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
|||||||
const furniSet = new Set(itemsData.furniDataIdList)
|
const furniSet = new Set(itemsData.furniDataIdList)
|
||||||
const missing = itemsData.baseItems.filter((b) => !furniSet.has(b.id)).length
|
const missing = itemsData.baseItems.filter((b) => !furniSet.has(b.id)).length
|
||||||
return missing > 0 ? (
|
return missing > 0 ? (
|
||||||
<span className="ml-0.5 rounded-full bg-orange-500 text-white text-[9px] px-1 py-0.5 leading-none font-medium">
|
<span className="ml-0.5 rounded-full bg-[var(--color-warning)] text-[var(--color-warning-foreground-readable)] text-[9px] px-1 py-0.5 leading-none font-medium">
|
||||||
{missing}
|
{missing}
|
||||||
</span>
|
</span>
|
||||||
) : null
|
) : null
|
||||||
|
|||||||
@@ -109,17 +109,17 @@ export function CatalogIcon({ iconImage, size = 20 }: { iconImage: number; size?
|
|||||||
/* ─── Layout label helper ────────────────────────────────── */
|
/* ─── Layout label helper ────────────────────────────────── */
|
||||||
|
|
||||||
export const LAYOUT_COLORS: Record<string, string> = {
|
export const LAYOUT_COLORS: Record<string, string> = {
|
||||||
default_3x3: 'bg-blue-500/15 text-blue-600 dark:text-blue-400',
|
default_3x3: 'bg-[var(--color-info-subtle)] text-[var(--color-info-text-readable)] dark:text-[var(--color-info-text-readable)]',
|
||||||
frontpage: 'bg-amber-500/15 text-amber-600 dark:text-amber-400',
|
frontpage: 'bg-[var(--color-warning-subtle)] text-[var(--color-warning-text-readable)] dark:text-[var(--color-warning-text-readable)]',
|
||||||
spaces_new: 'bg-green-500/15 text-green-600 dark:text-green-400',
|
spaces_new: 'bg-[var(--color-success-subtle)] text-[var(--color-success-text-readable)] dark:text-[var(--color-success-text-readable)]',
|
||||||
pets: 'bg-pink-500/15 text-pink-600 dark:text-pink-400',
|
pets: 'bg-pink-500/15 text-pink-600 dark:text-pink-400',
|
||||||
pets2: 'bg-pink-500/15 text-pink-600 dark:text-pink-400',
|
pets2: 'bg-pink-500/15 text-pink-600 dark:text-pink-400',
|
||||||
pets3: 'bg-pink-500/15 text-pink-600 dark:text-pink-400',
|
pets3: 'bg-pink-500/15 text-pink-600 dark:text-pink-400',
|
||||||
marketplace: 'bg-purple-500/15 text-purple-600 dark:text-purple-400',
|
marketplace: 'bg-purple-500/15 text-purple-600 dark:text-purple-400',
|
||||||
recycler: 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400',
|
recycler: 'bg-[var(--color-success-subtle)] text-[var(--color-success-text-readable)] dark:text-[var(--color-success-text-readable)]',
|
||||||
club_buy: 'bg-yellow-500/15 text-yellow-600 dark:text-yellow-400',
|
club_buy: 'bg-[var(--color-warning-subtle)] text-[var(--color-warning-text-readable)] dark:text-[var(--color-warning-text-readable)]',
|
||||||
single_bundle: 'bg-indigo-500/15 text-indigo-600 dark:text-indigo-400',
|
single_bundle: 'bg-[var(--color-info-subtle)] text-[var(--color-info-text-readable)] dark:text-[var(--color-info-text-readable)]',
|
||||||
room_bundle: 'bg-indigo-500/15 text-indigo-600 dark:text-indigo-400',
|
room_bundle: 'bg-[var(--color-info-subtle)] text-[var(--color-info-text-readable)] dark:text-[var(--color-info-text-readable)]',
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Highlight matched text ─────────────────────────────── */
|
/* ─── Highlight matched text ─────────────────────────────── */
|
||||||
@@ -131,7 +131,7 @@ function HighlightText({ text, query }: { text: string; query: string }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{text.slice(0, idx)}
|
{text.slice(0, idx)}
|
||||||
<mark className="bg-yellow-200 dark:bg-yellow-800/60 text-inherit rounded-sm px-0.5">
|
<mark className="bg-[var(--color-warning-subtle)] text-[var(--color-warning-text-readable)] rounded-sm px-0.5">
|
||||||
{text.slice(idx, idx + query.length)}
|
{text.slice(idx, idx + query.length)}
|
||||||
</mark>
|
</mark>
|
||||||
{text.slice(idx + query.length)}
|
{text.slice(idx + query.length)}
|
||||||
@@ -325,7 +325,7 @@ export function CatalogTree({ rootPages, catalogType = 'normal' }: CatalogTreePr
|
|||||||
className={cn(
|
className={cn(
|
||||||
'px-2 py-1 rounded text-xs font-medium transition-colors flex items-center gap-1',
|
'px-2 py-1 rounded text-xs font-medium transition-colors flex items-center gap-1',
|
||||||
filterMode === 'enabled'
|
filterMode === 'enabled'
|
||||||
? 'bg-background shadow-sm text-green-600 dark:text-green-400'
|
? 'bg-background shadow-sm text-[var(--color-success-text-readable)] dark:text-[var(--color-success-text-readable)]'
|
||||||
: 'text-muted-foreground hover:text-foreground',
|
: 'text-muted-foreground hover:text-foreground',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -345,7 +345,7 @@ export function CatalogTree({ rootPages, catalogType = 'normal' }: CatalogTreePr
|
|||||||
className={cn(
|
className={cn(
|
||||||
'px-2 py-1 rounded text-xs font-medium transition-colors flex items-center gap-1',
|
'px-2 py-1 rounded text-xs font-medium transition-colors flex items-center gap-1',
|
||||||
filterMode === 'disabled'
|
filterMode === 'disabled'
|
||||||
? 'bg-background shadow-sm text-red-600 dark:text-red-400'
|
? 'bg-background shadow-sm text-[var(--color-error-text-readable)] dark:text-[var(--color-error-text-readable)]'
|
||||||
: 'text-muted-foreground hover:text-foreground',
|
: 'text-muted-foreground hover:text-foreground',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -643,8 +643,8 @@ function TreeItem({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-center h-5 w-5 shrink-0 rounded opacity-0 group-hover:opacity-100 transition-opacity',
|
'flex items-center justify-center h-5 w-5 shrink-0 rounded opacity-0 group-hover:opacity-100 transition-opacity',
|
||||||
isDisabled
|
isDisabled
|
||||||
? 'hover:bg-green-500/20 text-muted-foreground hover:text-green-600'
|
? 'hover:bg-[var(--color-success-subtle)] text-muted-foreground hover:text-[var(--color-success-text-readable)]'
|
||||||
: 'hover:bg-red-500/20 text-muted-foreground hover:text-red-600',
|
: 'hover:bg-[var(--color-error-subtle)] text-muted-foreground hover:text-[var(--color-error-text-readable)]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isDisabled ? (
|
{isDisabled ? (
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ export function BulkImportItems({ pageId, onImported }: BulkImportItemsProps) {
|
|||||||
<td className="px-2 py-0.5 font-mono">{row.pointsType ?? '—'}</td>
|
<td className="px-2 py-0.5 font-mono">{row.pointsType ?? '—'}</td>
|
||||||
<td className="px-2 py-0.5">
|
<td className="px-2 py-0.5">
|
||||||
{row.ok ? (
|
{row.ok ? (
|
||||||
<span className="text-green-600">OK</span>
|
<span className="text-[var(--color-success-text-readable)]">OK</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-destructive">{row.error}</span>
|
<span className="text-destructive">{row.error}</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function ItemsShopPreview({
|
|||||||
{/* Badges overlay */}
|
{/* Badges overlay */}
|
||||||
<div className="absolute right-1 top-1 flex flex-col gap-0.5">
|
<div className="absolute right-1 top-1 flex flex-col gap-0.5">
|
||||||
{isClub && (
|
{isClub && (
|
||||||
<Badge className="h-4 gap-0.5 border-0 bg-yellow-500/90 px-1 text-[8px] text-white">
|
<Badge className="h-4 gap-0.5 border-0 bg-[var(--color-warning-subtle)] px-1 text-[8px] text-white">
|
||||||
<Crown className="h-2.5 w-2.5" />
|
<Crown className="h-2.5 w-2.5" />
|
||||||
HC
|
HC
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -110,7 +110,7 @@ export function ItemsShopPreview({
|
|||||||
</p>
|
</p>
|
||||||
<div className="mt-0.5 flex items-center gap-1.5 text-[10px] font-mono">
|
<div className="mt-0.5 flex items-center gap-1.5 text-[10px] font-mono">
|
||||||
{item.costCredits > 0 && (
|
{item.costCredits > 0 && (
|
||||||
<span className="text-yellow-600 dark:text-yellow-500">
|
<span className="text-[var(--color-warning-text-readable)] dark:text-[var(--color-warning-text-readable)]">
|
||||||
{item.costCredits}c
|
{item.costCredits}c
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -61,15 +61,15 @@ export function DiagnosticRow({
|
|||||||
const text = state === "ok" ? "OK" : state === "warn" ? "WARN" : "ERROR";
|
const text = state === "ok" ? "OK" : state === "warn" ? "WARN" : "ERROR";
|
||||||
const dotColor =
|
const dotColor =
|
||||||
state === "ok"
|
state === "ok"
|
||||||
? "bg-green-600 shadow-[0_0_0_3px_rgba(22,163,74,0.22)]"
|
? "bg-[var(--color-success-subtle)] shadow-[0_0_0_3px_rgba(22,163,74,0.22)]"
|
||||||
: state === "warn"
|
: state === "warn"
|
||||||
? "bg-[var(--color-primary)] shadow-[0_0_0_3px_rgba(238,180,37,0.22)]"
|
? "bg-[var(--color-primary)] shadow-[0_0_0_3px_rgba(238,180,37,0.22)]"
|
||||||
: "bg-red-500 shadow-[0_0_0_3px_rgba(239,68,68,0.22)]";
|
: "bg-[var(--color-error-subtle)] shadow-[0_0_0_3px_rgba(239,68,68,0.22)]";
|
||||||
const badgeColor =
|
const badgeColor =
|
||||||
state === "ok"
|
state === "ok"
|
||||||
? "bg-green-600/20"
|
? "bg-[var(--color-success-subtle)]"
|
||||||
: state === "danger"
|
: state === "danger"
|
||||||
? "bg-red-500/22"
|
? "bg-[var(--color-error-subtle)]"
|
||||||
: "bg-[var(--color-primary)]/18";
|
: "bg-[var(--color-primary)]/18";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -144,7 +144,7 @@ export function OnlineUsersWidget({
|
|||||||
/>
|
/>
|
||||||
{user.roomId && user.roomName && (
|
{user.roomId && user.roomName && (
|
||||||
<div className="absolute -bottom-0.5 -right-0.5">
|
<div className="absolute -bottom-0.5 -right-0.5">
|
||||||
<span className="block w-[10px] h-[10px] bg-green-500 rounded-full border-2 border-[var(--color-surface)]" />
|
<span className="block w-[10px] h-[10px] bg-[var(--color-success-subtle)] rounded-full border-2 border-[var(--color-surface)]" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -160,9 +160,9 @@ export function OnlineUsersWidget({
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-[0.75rem] text-[var(--color-text-muted)]">
|
<div className="text-[0.75rem] text-[var(--color-text-muted)]">
|
||||||
{user.roomId && user.roomName ? (
|
{user.roomId && user.roomName ? (
|
||||||
<span className="text-green-500 font-medium">🏠 {user.roomName}</span>
|
<span className="text-[var(--color-success-text-readable)] font-medium">🏠 {user.roomName}</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-red-500">⏱️ {formatLastSeen(user.lastActivity)}</span>
|
<span className="text-[var(--color-error-text-readable)]">⏱️ {formatLastSeen(user.lastActivity)}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ export function EmulatorSettingsForm({
|
|||||||
<div
|
<div
|
||||||
key={setting.key}
|
key={setting.key}
|
||||||
className={`flex items-center gap-3 px-3 py-1.5 border-l-4 ${borderColor} ${
|
className={`flex items-center gap-3 px-3 py-1.5 border-l-4 ${borderColor} ${
|
||||||
isChanged ? 'bg-yellow-50/60 dark:bg-yellow-950/20' : ''
|
isChanged ? 'bg-[var(--color-warning-subtle)] dark:bg-[var(--color-warning-subtle)]' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<code
|
<code
|
||||||
@@ -305,7 +305,7 @@ export function EmulatorSettingsForm({
|
|||||||
value={display}
|
value={display}
|
||||||
onChange={(e) => updateValue(setting.key, e.target.value)}
|
onChange={(e) => updateValue(setting.key, e.target.value)}
|
||||||
className={`flex-1 h-8 text-xs font-mono ${
|
className={`flex-1 h-8 text-xs font-mono ${
|
||||||
isChanged ? 'border-yellow-500/60' : ''
|
isChanged ? 'border-[var(--color-warning-border)]' : ''
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{isChanged && (
|
{isChanged && (
|
||||||
|
|||||||
@@ -103,9 +103,9 @@ export function AdminMediaGrid() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Loading…</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted">Loading…</p>
|
||||||
) : files.length === 0 ? (
|
) : files.length === 0 ? (
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">No images yet. Upload one above.</p>
|
<p className="text-xs theme-text-muted dark:theme-text-muted">No images yet. Upload one above.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-3">
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-3">
|
||||||
{files.map((f) => (
|
{files.map((f) => (
|
||||||
|
|||||||
@@ -13,5 +13,11 @@ describe("themePaletteCss", () => {
|
|||||||
expect(css).toContain(`--color-background:${ocean.light.color_background}`);
|
expect(css).toContain(`--color-background:${ocean.light.color_background}`);
|
||||||
expect(css).toContain(`--color-background:${ocean.dark.color_background}`);
|
expect(css).toContain(`--color-background:${ocean.dark.color_background}`);
|
||||||
expect(ocean.dark.color_primary).toBe(ocean.light.color_primary);
|
expect(ocean.dark.color_primary).toBe(ocean.light.color_primary);
|
||||||
|
for (const variable of [
|
||||||
|
"--color-text-subtle-readable", "--color-text-disabled-readable",
|
||||||
|
"--admin-sidebar-text-readable", "--color-success-text-readable",
|
||||||
|
"--color-success-foreground-readable", "--color-warning-text-readable",
|
||||||
|
"--color-error-text-readable", "--color-info-text-readable",
|
||||||
|
]) expect(css).toContain(variable);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { readdirSync, readFileSync } from "node:fs";
|
||||||
|
import { join, relative } from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const ROOTS = ["src/app/admin", "src/components/admin"];
|
||||||
|
const GRAPHICAL_ALLOWLIST = [
|
||||||
|
"src/app/admin/favicon/favicon-generator.tsx",
|
||||||
|
"src/app/admin/import/clone/import-clone-client.tsx",
|
||||||
|
"src/app/admin/import/furni/import-furni-client.tsx",
|
||||||
|
"src/components/admin/catalog/items-shop-preview.tsx",
|
||||||
|
];
|
||||||
|
|
||||||
|
function sourceFiles(directory: string): string[] {
|
||||||
|
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||||
|
const path = join(directory, entry.name);
|
||||||
|
return entry.isDirectory() ? sourceFiles(path) : /\.(?:ts|tsx)$/.test(entry.name) ? [path] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("admin theme source audit", () => {
|
||||||
|
it("keeps ordinary admin text theme-aware", () => {
|
||||||
|
const violations: string[] = [];
|
||||||
|
for (const root of ROOTS) {
|
||||||
|
for (const file of sourceFiles(root)) {
|
||||||
|
const normalized = relative(process.cwd(), file).replaceAll("\\", "/");
|
||||||
|
if (GRAPHICAL_ALLOWLIST.includes(normalized)) continue;
|
||||||
|
const source = readFileSync(file, "utf8");
|
||||||
|
const risky = source.match(/(?:text-(?:gray|slate)-(?:400|500|600|700|800|900)|text-white(?:\/\d+)?|\)\]0)/g);
|
||||||
|
if (risky) violations.push(`${normalized}: ${[...new Set(risky)].join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(violations).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -86,6 +86,24 @@ describe("derivePublicForegrounds", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("covers admin hierarchy and every semantic status", () => {
|
||||||
|
const required = [
|
||||||
|
"subtle", "disabled", "sidebar", "input",
|
||||||
|
"success-text", "success-solid", "warning-text", "warning-solid",
|
||||||
|
"error-text", "error-solid", "info-text", "info-solid",
|
||||||
|
];
|
||||||
|
for (const preset of Object.values(PRESETS)) {
|
||||||
|
for (const palette of [preset.light, preset.dark]) {
|
||||||
|
const result = derivePublicForegrounds(palette);
|
||||||
|
for (const name of required) {
|
||||||
|
const pair = result.pairs.find((candidate) => candidate.name === name);
|
||||||
|
expect(pair, `missing ${name}`).toBeDefined();
|
||||||
|
expect(contrastRatio(pair!.foreground, pair!.background), name).toBeGreaterThanOrEqual(4.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("repairs hostile white-on-white custom colors", () => {
|
it("repairs hostile white-on-white custom colors", () => {
|
||||||
const result = derivePublicForegrounds({
|
const result = derivePublicForegrounds({
|
||||||
color_background: "#ffffff", color_surface: "#ffffff", color_dropdown: "#ffffff",
|
color_background: "#ffffff", color_surface: "#ffffff", color_dropdown: "#ffffff",
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ export function derivePublicForegrounds(c: PublicThemeColors) {
|
|||||||
make("body", value("color_text", "#0f172a"), background),
|
make("body", value("color_text", "#0f172a"), background),
|
||||||
make("surface", value("color_text", "#0f172a"), surface),
|
make("surface", value("color_text", "#0f172a"), surface),
|
||||||
make("muted", value("color_text_muted", "#64748b"), surface),
|
make("muted", value("color_text_muted", "#64748b"), surface),
|
||||||
|
make("subtle", value("color_text_muted", "#64748b"), surface),
|
||||||
|
make("disabled", value("color_text_muted", "#64748b"), surface),
|
||||||
|
make("sidebar", value("color_navbar_text", "#1e293b"), navbar),
|
||||||
|
make("input", value("color_text", "#0f172a"), dropdown),
|
||||||
make("link", value("link_color", primary), surface),
|
make("link", value("link_color", primary), surface),
|
||||||
make("link-hover", value("link_hover_color", primary), surface),
|
make("link-hover", value("link_hover_color", primary), surface),
|
||||||
make("navbar", value("color_navbar_text", "#1e293b"), navbar),
|
make("navbar", value("color_navbar_text", "#1e293b"), navbar),
|
||||||
@@ -31,6 +35,13 @@ export function derivePublicForegrounds(c: PublicThemeColors) {
|
|||||||
make("pill", value("color_text", "#0f172a"), surface),
|
make("pill", value("color_text", "#0f172a"), surface),
|
||||||
make("badge", value("button_text_color", "#1e293b"), primary),
|
make("badge", value("button_text_color", "#1e293b"), primary),
|
||||||
make("overlay", "#ffffff", "#111827"),
|
make("overlay", "#ffffff", "#111827"),
|
||||||
|
...(["success", "warning", "error", "info"] as const).flatMap((status) => {
|
||||||
|
const statusColor = value(`color_${status}`, "#0f172a");
|
||||||
|
return [
|
||||||
|
make(`${status}-text`, statusColor, surface),
|
||||||
|
make(`${status}-solid`, "#ffffff", statusColor),
|
||||||
|
];
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
return { pairs, byName: Object.fromEntries(pairs.map((pair) => [pair.name, pair.foreground])) };
|
return { pairs, byName: Object.fromEntries(pairs.map((pair) => [pair.name, pair.foreground])) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export function themePaletteCss(selector: string, palette: ThemePalette): string
|
|||||||
`--color-text-muted-readable:${readableColor(palette.color_text_muted, [palette.color_background, palette.color_surface])};`,
|
`--color-text-muted-readable:${readableColor(palette.color_text_muted, [palette.color_background, palette.color_surface])};`,
|
||||||
`--color-primary-readable:${readableColor(palette.color_primary, [palette.color_background, palette.color_surface])};`,
|
`--color-primary-readable:${readableColor(palette.color_primary, [palette.color_background, palette.color_surface])};`,
|
||||||
`--color-accent-readable:${readableColor(palette.color_accent, [palette.color_background, palette.color_surface])};`,
|
`--color-accent-readable:${readableColor(palette.color_accent, [palette.color_background, palette.color_surface])};`,
|
||||||
|
`--color-primary-foreground-readable:${semantic.byName.primary};`,
|
||||||
|
`--color-accent-foreground-readable:${semantic.byName.accent};`,
|
||||||
`--color-navbar-text-readable:${semantic.byName.navbar};`,
|
`--color-navbar-text-readable:${semantic.byName.navbar};`,
|
||||||
`--button-text-color-readable:${semantic.byName["button-primary"]};`,
|
`--button-text-color-readable:${semantic.byName["button-primary"]};`,
|
||||||
`--button-secondary-text-color-readable:${semantic.byName["button-secondary"]};`,
|
`--button-secondary-text-color-readable:${semantic.byName["button-secondary"]};`,
|
||||||
@@ -39,6 +41,18 @@ export function themePaletteCss(selector: string, palette: ThemePalette): string
|
|||||||
`--color-pill-text-readable:${semantic.byName.pill};`,
|
`--color-pill-text-readable:${semantic.byName.pill};`,
|
||||||
`--color-badge-text-readable:${semantic.byName.badge};`,
|
`--color-badge-text-readable:${semantic.byName.badge};`,
|
||||||
`--color-overlay-text-readable:${semantic.byName.overlay};`,
|
`--color-overlay-text-readable:${semantic.byName.overlay};`,
|
||||||
|
`--color-text-subtle-readable:${semantic.byName.subtle};`,
|
||||||
|
`--color-text-disabled-readable:${semantic.byName.disabled};`,
|
||||||
|
`--admin-sidebar-background:${palette.color_navbar};`,
|
||||||
|
`--admin-sidebar-text-readable:${semantic.byName.sidebar};`,
|
||||||
|
`--admin-sidebar-muted-readable:${semantic.byName.sidebar};`,
|
||||||
|
`--admin-input-text-readable:${semantic.byName.input};`,
|
||||||
|
...(["success", "warning", "error", "info"] as const).flatMap((status) => [
|
||||||
|
`--color-${status}-text-readable:${semantic.byName[`${status}-text`]};`,
|
||||||
|
`--color-${status}-foreground-readable:${semantic.byName[`${status}-solid`]};`,
|
||||||
|
`--color-${status}-subtle:color-mix(in srgb, ${palette[`color_${status}`]} 14%, ${palette.color_surface});`,
|
||||||
|
`--color-${status}-border:color-mix(in srgb, ${palette[`color_${status}`]} 38%, transparent);`,
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
return `${selector}{${declarations.join("")}}`;
|
return `${selector}{${declarations.join("")}}`;
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user