Add theme toggle and update MineGeo branding
This commit is contained in:
457
app/appearance.css
Normal file
457
app/appearance.css
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
/* Site chrome only: map artwork, parchment, tiles and panoramas keep their colors. */
|
||||||
|
.brand {
|
||||||
|
gap: 9px;
|
||||||
|
}
|
||||||
|
.brand .brand-mark {
|
||||||
|
display: block;
|
||||||
|
width: 42px;
|
||||||
|
height: 46px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.brand > span.brand-wordmark {
|
||||||
|
color: var(--text);
|
||||||
|
font:
|
||||||
|
850 25px/1 Arial,
|
||||||
|
Helvetica,
|
||||||
|
sans-serif;
|
||||||
|
letter-spacing: -1.3px;
|
||||||
|
}
|
||||||
|
.brand-wordmark > span {
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
.brand sup {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.site-footer .brand .brand-mark {
|
||||||
|
width: 36px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.game-header .brand .brand-wordmark {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
.game-header .brand .brand-mark {
|
||||||
|
width: 32px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
.theme-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-control);
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background: var(--panel2);
|
||||||
|
border-color: var(--green);
|
||||||
|
}
|
||||||
|
.theme-moon,
|
||||||
|
html[data-theme="light"] .theme-sun {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .theme-moon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] {
|
||||||
|
--bg: #f4f6f1;
|
||||||
|
--panel: #ffffff;
|
||||||
|
--panel2: #e9eee5;
|
||||||
|
--line: #cad4c6;
|
||||||
|
--green: #397325;
|
||||||
|
--muted: #586654;
|
||||||
|
--text: #202c20;
|
||||||
|
--board-stone: #ffffff;
|
||||||
|
--board-edge: #cad4c6;
|
||||||
|
--board-gold: #866318;
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] body {
|
||||||
|
background-image: radial-gradient(
|
||||||
|
ellipse at 80% 12%,
|
||||||
|
#dce8d180,
|
||||||
|
transparent 55%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
html[data-theme="light"] :is(h1, h2, h3) {
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .hero h1 span {
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] :is(.site-header, .site-footer) {
|
||||||
|
background: var(--bg);
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
html[data-theme="light"] :is(nav, .brand sup) {
|
||||||
|
color: var(--muted);
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .header-account {
|
||||||
|
background: #e0ecd7;
|
||||||
|
color: #2b4b21;
|
||||||
|
border-color: #a8bf9c;
|
||||||
|
border-bottom-color: #739565;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .header-account:hover {
|
||||||
|
background: #d1e3c5;
|
||||||
|
}
|
||||||
|
html[data-theme="light"]
|
||||||
|
:is(.button, .button.secondary, .icon-button):not(.map-actions .button) {
|
||||||
|
background: var(--panel2);
|
||||||
|
color: var(--text);
|
||||||
|
border-color: #aab9a3;
|
||||||
|
text-shadow: none;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px #fff,
|
||||||
|
0 3px 8px #243a1812;
|
||||||
|
}
|
||||||
|
html[data-theme="light"]
|
||||||
|
:is(.button, .icon-button):not(.map-actions .button):hover {
|
||||||
|
background: #dce7d4;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .button.primary:not(.map-actions .button) {
|
||||||
|
background: linear-gradient(#a6df80, #83c05d);
|
||||||
|
color: #18310d;
|
||||||
|
border-color: #74a554;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .button.primary:not(.map-actions .button):hover {
|
||||||
|
background: linear-gradient(#b7e994, #98ce75);
|
||||||
|
}
|
||||||
|
html[data-theme="light"]
|
||||||
|
:is(
|
||||||
|
.world-stats span:not(.live-dot),
|
||||||
|
.hero p,
|
||||||
|
.hero-mode-note,
|
||||||
|
.hero-notes,
|
||||||
|
.contribute-banner p,
|
||||||
|
.play-steps li > span,
|
||||||
|
.round-result p,
|
||||||
|
.round-result .score small,
|
||||||
|
.results-total small,
|
||||||
|
.card-body p,
|
||||||
|
.card-meta,
|
||||||
|
.first-map-panel p,
|
||||||
|
.play-steps p,
|
||||||
|
.mode-row p,
|
||||||
|
.mode-row h3 span,
|
||||||
|
.mode-footnote,
|
||||||
|
.site-footer p,
|
||||||
|
.site-footer > div:nth-child(2),
|
||||||
|
.leaderboard-preview-copy p,
|
||||||
|
.board-heading > span:last-child,
|
||||||
|
.board-footnote,
|
||||||
|
.leaderboard-table thead th,
|
||||||
|
.rank-number,
|
||||||
|
.score-unit,
|
||||||
|
.leaderboard-table .games-column,
|
||||||
|
.leaderboard-table .average-column,
|
||||||
|
.leaderboard-period,
|
||||||
|
.podium-label,
|
||||||
|
.podium-player p span,
|
||||||
|
.rankings-heading p,
|
||||||
|
.leaderboard-pagination,
|
||||||
|
.leaderboard-pagination nav,
|
||||||
|
.leaderboard-rules p,
|
||||||
|
.leaderboard-empty p,
|
||||||
|
.field
|
||||||
|
) {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
html[data-theme="light"]
|
||||||
|
:is(
|
||||||
|
.eyebrow,
|
||||||
|
.status-label,
|
||||||
|
.text-link,
|
||||||
|
.leaderboard-preview-copy .text-link,
|
||||||
|
.leaderboard-rules .text-link,
|
||||||
|
.history-table th
|
||||||
|
) {
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
html[data-theme="light"]
|
||||||
|
:is(
|
||||||
|
.world-stats,
|
||||||
|
.map-card,
|
||||||
|
.home-page .map-card,
|
||||||
|
.card-body,
|
||||||
|
.home-page .card-body,
|
||||||
|
.home-panel,
|
||||||
|
.home-panel + .home-panel,
|
||||||
|
.first-map-panel,
|
||||||
|
.empty-state,
|
||||||
|
.auth-layout .auth-card,
|
||||||
|
.settings-card,
|
||||||
|
.leaderboard-preview-board,
|
||||||
|
.rankings-board,
|
||||||
|
.tool-card,
|
||||||
|
.start-panel,
|
||||||
|
.auth-card,
|
||||||
|
.editor-panel,
|
||||||
|
.report-dialog,
|
||||||
|
.result-summary,
|
||||||
|
.round-result,
|
||||||
|
.draft-row,
|
||||||
|
.token-box
|
||||||
|
) {
|
||||||
|
background: var(--panel);
|
||||||
|
border-color: var(--line);
|
||||||
|
box-shadow: 0 5px 18px #243a1810;
|
||||||
|
}
|
||||||
|
html[data-theme="light"]
|
||||||
|
:is(
|
||||||
|
.world-stats > div,
|
||||||
|
.card-meta,
|
||||||
|
.card-body,
|
||||||
|
.home-leaderboard,
|
||||||
|
.leaderboard-toolbar,
|
||||||
|
.leaderboard-rules,
|
||||||
|
.board-footnote,
|
||||||
|
.leaderboard-table td,
|
||||||
|
.leaderboard-table th,
|
||||||
|
.leaderboard-pagination,
|
||||||
|
.search-summary,
|
||||||
|
.history-table td,
|
||||||
|
.history-table th
|
||||||
|
) {
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
html[data-theme="light"]
|
||||||
|
:is(.world-stats strong, .score-column strong, .podium-player p) {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
html[data-theme="light"]
|
||||||
|
:is(.mode-row, .account-email, .auth-tabs, .leaderboard-table thead) {
|
||||||
|
background: var(--panel2);
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .mode-row:hover {
|
||||||
|
background: #dce7d4;
|
||||||
|
border-color: #99b389;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] :is(.contribute-banner, .badge) {
|
||||||
|
background: #e9f2e1;
|
||||||
|
color: #36552a;
|
||||||
|
border-color: #aac49b;
|
||||||
|
}
|
||||||
|
html[data-theme="light"]
|
||||||
|
:is(
|
||||||
|
.field input,
|
||||||
|
.field textarea,
|
||||||
|
.field select,
|
||||||
|
.search-field,
|
||||||
|
.leaderboard-search,
|
||||||
|
.preview-toolbar select,
|
||||||
|
.language-switcher select
|
||||||
|
) {
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text);
|
||||||
|
border-color: #b4c3ac;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] :is(input, textarea)::placeholder {
|
||||||
|
color: #64735e;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
html[data-theme="light"]
|
||||||
|
:is(
|
||||||
|
.tabs button.active,
|
||||||
|
.auth-tabs button[aria-pressed="true"],
|
||||||
|
.difficulty-options button.selected,
|
||||||
|
.leaderboard-filters a[aria-current="page"]
|
||||||
|
) {
|
||||||
|
background: #dcebd1;
|
||||||
|
color: #2b5420;
|
||||||
|
border-color: #88aa75;
|
||||||
|
}
|
||||||
|
html[data-theme="light"]
|
||||||
|
:is(.difficulty-options button, .leaderboard-search button) {
|
||||||
|
background: var(--panel2);
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
html[data-theme="light"]
|
||||||
|
:is(.leaderboard-filters a:hover, .leaderboard-table tbody tr:hover) {
|
||||||
|
background: #eef3e9;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .your-row {
|
||||||
|
background: #e9f2e1;
|
||||||
|
}
|
||||||
|
html[data-theme="light"]
|
||||||
|
:is(.you-label, .leaderboard-skeleton, .leaderboard-skeleton > div) {
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .leaderboard-skeleton span {
|
||||||
|
background: var(--panel2);
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .page-number {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .rank-number.place-2 {
|
||||||
|
color: #52646f;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .rank-number.place-3 {
|
||||||
|
color: #866145;
|
||||||
|
}
|
||||||
|
html[data-theme="light"]
|
||||||
|
:is(.podium-block, .podium-1 .podium-block, .podium-3 .podium-block) {
|
||||||
|
background-color: var(--panel2);
|
||||||
|
border-color: #a8b99f;
|
||||||
|
box-shadow:
|
||||||
|
inset 5px 0 #ffffff80,
|
||||||
|
inset -5px 0 #243a1808,
|
||||||
|
0 4px #cad4c6;
|
||||||
|
}
|
||||||
|
html[data-theme="light"]
|
||||||
|
:is(.podium-block > span, .podium-1 .podium-block > span) {
|
||||||
|
color: #506446;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] :is(pre, .token-box code) {
|
||||||
|
background: #e9eee5;
|
||||||
|
color: #33502a;
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .error-message {
|
||||||
|
background: #fbeee8;
|
||||||
|
color: #953c25;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .success-message {
|
||||||
|
background: #e9f2e1;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .username-moderator {
|
||||||
|
color: #246494;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .username-bug-finder {
|
||||||
|
color: #866318;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .username-custom-prefix {
|
||||||
|
color: #7043a4;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .prefix-player-list button[aria-pressed="true"] {
|
||||||
|
border-color: #246494;
|
||||||
|
}
|
||||||
|
/* Keep freely chosen glow colors visible against a dark chip in light mode. */
|
||||||
|
html[data-theme="light"] .username-glow {
|
||||||
|
background: #202c20;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding-inline: 5px;
|
||||||
|
}
|
||||||
|
html[data-theme="light"]
|
||||||
|
:is(
|
||||||
|
.game-info,
|
||||||
|
.multiplayer-scoreboard,
|
||||||
|
.round-result,
|
||||||
|
.panorama-controls button,
|
||||||
|
.preview-viewport .panorama-controls button,
|
||||||
|
.preview-status,
|
||||||
|
.game-loading,
|
||||||
|
.look-help
|
||||||
|
) {
|
||||||
|
background: #f4f6f1f2;
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .game-info small {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .game-info strong {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .game-info .urgent {
|
||||||
|
color: #a23924;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .game-header .brand {
|
||||||
|
background: #f4f6f1ed;
|
||||||
|
padding: 4px 9px;
|
||||||
|
border-radius: 9px;
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .room-chat-mine strong {
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .site-cursor-orbit {
|
||||||
|
border-color: #34532aa6;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .site-cursor-dot span {
|
||||||
|
background: #273b21;
|
||||||
|
box-shadow: 0 0 1px #fff;
|
||||||
|
}
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.site-header {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.header-inner {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
padding-block: 14px;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.header-inner nav {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-row: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
padding: 12px 0 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
.header-inner {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.header-actions {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 2;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
.header-inner > .brand {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 1;
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
.header-inner .brand .brand-wordmark {
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
.header-inner .brand .brand-mark {
|
||||||
|
width: 30px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
.header-inner .brand {
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.header-actions {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.game-header .brand .brand-wordmark {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 380px) {
|
||||||
|
.header-actions {
|
||||||
|
grid-row: 2;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
justify-self: start;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.header-inner nav {
|
||||||
|
grid-row: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
html[lang="ru"] .header-actions {
|
||||||
|
grid-row: 2;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
justify-self: start;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
html[lang="ru"] .header-inner nav {
|
||||||
|
grid-row: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
app/icon.png
Normal file
BIN
app/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
@@ -16,6 +16,8 @@ import "./polish.css";
|
|||||||
import "./mobile-game.css";
|
import "./mobile-game.css";
|
||||||
import "./multiplayer.css";
|
import "./multiplayer.css";
|
||||||
import "./support.css";
|
import "./support.css";
|
||||||
|
import "./appearance.css";
|
||||||
|
import { themeInitScript } from "@/shared/theme";
|
||||||
import { SiteSettingsProvider } from "@/components/SiteSettingsProvider";
|
import { SiteSettingsProvider } from "@/components/SiteSettingsProvider";
|
||||||
import { initialSiteSettings } from "@/lib/site-settings";
|
import { initialSiteSettings } from "@/lib/site-settings";
|
||||||
import { user } from "@/lib/supabase";
|
import { user } from "@/lib/supabase";
|
||||||
@@ -40,7 +42,10 @@ export default async function RootLayout({
|
|||||||
const [{ locale, preference, detectedLocale }, settings, account] =
|
const [{ locale, preference, detectedLocale }, settings, account] =
|
||||||
await Promise.all([getI18n(), initialSiteSettings(), user()]);
|
await Promise.all([getI18n(), initialSiteSettings(), user()]);
|
||||||
return (
|
return (
|
||||||
<html lang={locale}>
|
<html lang={locale} data-theme="dark" suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
|
||||||
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<I18nProvider
|
<I18nProvider
|
||||||
locale={locale}
|
locale={locale}
|
||||||
|
|||||||
BIN
assets/branding/minegeo-mark-source.png
Normal file
BIN
assets/branding/minegeo-mark-source.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 819 KiB |
@@ -8,10 +8,16 @@ export function Brand() {
|
|||||||
const { settings } = useSiteSettings();
|
const { settings } = useSiteSettings();
|
||||||
return (
|
return (
|
||||||
<Link href="/" className="brand" aria-label={t("MineGeo home")}>
|
<Link href="/" className="brand" aria-label={t("MineGeo home")}>
|
||||||
<span className="brand-cube">
|
<img
|
||||||
<span />
|
className="brand-mark"
|
||||||
</span>
|
src="/images/minegeo-mark.webp"
|
||||||
|
width={42}
|
||||||
|
height={46}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<span className="brand-wordmark">
|
||||||
Mine<span>Geo</span>
|
Mine<span>Geo</span>
|
||||||
|
</span>
|
||||||
{settings.betaBadgeEnabled && <sup>BETA</sup>}
|
{settings.betaBadgeEnabled && <sup>BETA</sup>}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { user } from "@/lib/supabase";
|
|||||||
import { Brand } from "./Brand";
|
import { Brand } from "./Brand";
|
||||||
import { SiteNav } from "./SiteNav";
|
import { SiteNav } from "./SiteNav";
|
||||||
import { ReportBugButton } from "./ReportBugButton";
|
import { ReportBugButton } from "./ReportBugButton";
|
||||||
|
import { ThemeToggle } from "./ThemeToggle";
|
||||||
export async function Header() {
|
export async function Header() {
|
||||||
const { t } = await getI18n();
|
const { t } = await getI18n();
|
||||||
const account = await user();
|
const account = await user();
|
||||||
@@ -16,6 +17,7 @@ export async function Header() {
|
|||||||
<Brand />
|
<Brand />
|
||||||
<SiteNav />
|
<SiteNav />
|
||||||
<div className="header-actions">
|
<div className="header-actions">
|
||||||
|
<ThemeToggle />
|
||||||
<ReportBugButton />
|
<ReportBugButton />
|
||||||
<Link
|
<Link
|
||||||
href={account ? "/dashboard" : "/log-in"}
|
href={account ? "/dashboard" : "/log-in"}
|
||||||
@@ -37,6 +39,7 @@ export async function Footer() {
|
|||||||
<p>{t("A familiar world. A whole new perspective.")}</p>
|
<p>{t("A familiar world. A whole new perspective.")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
<ThemeToggle />
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
<Link href="/downloads">{t("Tools & downloads")}</Link>
|
<Link href="/downloads">{t("Tools & downloads")}</Link>
|
||||||
<Link href="/leaderboard">{t("Leaderboard")}</Link>
|
<Link href="/leaderboard">{t("Leaderboard")}</Link>
|
||||||
|
|||||||
59
components/ThemeToggle.tsx
Normal file
59
components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useSyncExternalStore } from "react";
|
||||||
|
import { Moon, Sun } from "lucide-react";
|
||||||
|
import { useI18n } from "./I18nProvider";
|
||||||
|
import { themeStorageKey, type Theme } from "@/shared/theme";
|
||||||
|
|
||||||
|
function currentTheme(): Theme {
|
||||||
|
return document.documentElement.dataset.theme === "light" ? "light" : "dark";
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribe(onChange: () => void) {
|
||||||
|
const observer = new MutationObserver(onChange);
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["data-theme"],
|
||||||
|
});
|
||||||
|
function syncStorage(event: StorageEvent) {
|
||||||
|
if (event.key === themeStorageKey || event.key === null) {
|
||||||
|
document.documentElement.dataset.theme =
|
||||||
|
event.newValue === "light" ? "light" : "dark";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener("storage", syncStorage);
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
window.removeEventListener("storage", syncStorage);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const theme = useSyncExternalStore(subscribe, currentTheme, () => "dark");
|
||||||
|
const label = t(
|
||||||
|
theme === "light" ? "Turn on dark mode" : "Turn on light mode",
|
||||||
|
);
|
||||||
|
function toggle() {
|
||||||
|
const next = currentTheme() === "light" ? "dark" : "light";
|
||||||
|
document.documentElement.dataset.theme = next;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(themeStorageKey, next);
|
||||||
|
} catch {
|
||||||
|
// Switching still works for this visit if browser storage is unavailable.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="theme-toggle"
|
||||||
|
onClick={toggle}
|
||||||
|
aria-label={label}
|
||||||
|
title={label}
|
||||||
|
aria-pressed={theme === "light"}
|
||||||
|
>
|
||||||
|
<Sun className="theme-sun" size={19} aria-hidden="true" />
|
||||||
|
<Moon className="theme-moon" size={19} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
docs/appearance.md
Normal file
32
docs/appearance.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Theme and logo
|
||||||
|
|
||||||
|
The sun/moon buttons in the header and footer switch between dark and light mode.
|
||||||
|
Dark is the default. The choice is saved locally as `minegeo-theme-v1`, applied
|
||||||
|
before paint, and synchronized across tabs. Switching works for the current visit
|
||||||
|
even when browser storage is disabled. No account is required.
|
||||||
|
|
||||||
|
`app/appearance.css` supplies the light palette and shared logo styles. Map
|
||||||
|
artwork, parchment, terrain tiles, and panoramas retain their original colors.
|
||||||
|
|
||||||
|
The transparent source logo is `assets/branding/minegeo-mark-source.png`.
|
||||||
|
The optimized site asset is `public/images/minegeo-mark.webp`; `app/icon.png`
|
||||||
|
is the browser icon. The `Brand` component renders the mark and a live text
|
||||||
|
wordmark, keeping the site name accessible and sharp at small sizes.
|
||||||
|
|
||||||
|
Generated with the built-in imagegen tool using this prompt:
|
||||||
|
|
||||||
|
> Use case: logo-brand. Asset type: new MineGeo website logo symbol, transparent
|
||||||
|
> PNG. Primary request: create a distinctive polished compact symbol for a
|
||||||
|
> Minecraft geography guessing game, replacing a plain grass block. Subject:
|
||||||
|
> a bold angular location pin whose upper body is an isometric emerald
|
||||||
|
> grass-topped voxel cube, with a small ivory compass diamond cutout in the
|
||||||
|
> center. A unified single emblem, unmistakable silhouette, crisp deliberate
|
||||||
|
> geometric edges, contemporary gaming identity. Palette: rich forest green
|
||||||
|
> outline, vivid emerald and lime upper faces, warm ochre earth side, small
|
||||||
|
> ivory compass accent. Vector-like flat shapes with subtle face shading only,
|
||||||
|
> no photorealism, no extraneous floating elements, no words or letters, no
|
||||||
|
> landscape or map drawing. Composition: one centered emblem, fills most of
|
||||||
|
> square canvas with small uniform transparent margin, readable when shrunk
|
||||||
|
> to 40px, strong outline readable on both white and charcoal website backgrounds.
|
||||||
|
> Background genuinely transparent. Avoid mockups, glow, shadows beyond the
|
||||||
|
> emblem, fine textures, tiny detail, text, watermark.
|
||||||
BIN
public/images/minegeo-mark.webp
Normal file
BIN
public/images/minegeo-mark.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
@@ -786,5 +786,7 @@
|
|||||||
"Submit this report before sending messages": "Отправьте обращение перед отправкой сообщений",
|
"Submit this report before sending messages": "Отправьте обращение перед отправкой сообщений",
|
||||||
"Invalid support message": "Недопустимое сообщение поддержке",
|
"Invalid support message": "Недопустимое сообщение поддержке",
|
||||||
"Sign in to link this report": "Войдите, чтобы привязать обращение",
|
"Sign in to link this report": "Войдите, чтобы привязать обращение",
|
||||||
"Remove hidden control characters": "Удалите скрытые управляющие символы"
|
"Remove hidden control characters": "Удалите скрытые управляющие символы",
|
||||||
|
"Turn on light mode": "Включить светлую тему",
|
||||||
|
"Turn on dark mode": "Включить тёмную тему"
|
||||||
}
|
}
|
||||||
|
|||||||
5
shared/theme.ts
Normal file
5
shared/theme.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type Theme = "dark" | "light";
|
||||||
|
export const themeStorageKey = "minegeo-theme-v1";
|
||||||
|
|
||||||
|
// Apply the saved choice before paint; storage can be disabled in private browsers.
|
||||||
|
export const themeInitScript = `(function(){try{var t=localStorage.getItem("${themeStorageKey}");if(t==="light"||t==="dark")document.documentElement.dataset.theme=t}catch(e){}})()`;
|
||||||
Reference in New Issue
Block a user