feat: polish locale and theme controls

This commit is contained in:
2026-06-21 22:50:34 +07:00
parent 90a4c1eccf
commit 9043f3a711
10 changed files with 200 additions and 130 deletions
+3 -2
View File
@@ -1,6 +1,6 @@
# Dating
Warm-witty dating JD and CV site for Tien Nguyen Minh.
Warm-witty dating JD and CV site for Tiến Nguyn Minh.
## Pages
@@ -12,7 +12,8 @@ Warm-witty dating JD and CV site for Tien Nguyen Minh.
- Light theme: white and blue
- Dark theme: black and pink
- English and Vietnamese copy switch
- Compact emoji theme switch and flag language switch
- English and full Vietnamese copy switch
- Static i18n and theme preferences persisted in the browser
## Tech
+11 -5
View File
@@ -1,8 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<title>Dating JD &amp; CV</title>
<rect width="128" height="128" rx="64" fill="#1d2930" />
<circle cx="64" cy="64" r="48" fill="#f7f4ed" />
<path d="M33 66c19-25 43-25 62 0-19 24-43 24-62 0Z" fill="#9d3f45" opacity=".92" />
<title>Tiến Nguyễn Minh Dating JD &amp; CV</title>
<defs>
<linearGradient id="accent" x1="28" x2="100" y1="42" y2="88" gradientUnits="userSpaceOnUse">
<stop stop-color="#1f6feb" />
<stop offset="1" stop-color="#ff4fa3" />
</linearGradient>
</defs>
<rect width="128" height="128" rx="64" fill="#050507" />
<circle cx="64" cy="64" r="48" fill="#f7fbff" />
<path d="M33 66c19-25 43-25 62 0-19 24-43 24-62 0Z" fill="url(#accent)" />
<text
x="64"
y="74"
@@ -10,7 +16,7 @@
font-family="Georgia, serif"
font-size="34"
font-weight="700"
fill="#f7f4ed"
fill="#ffffff"
>
TN
</text>

Before

Width:  |  Height:  |  Size: 448 B

After

Width:  |  Height:  |  Size: 669 B

+7 -15
View File
@@ -4,7 +4,7 @@ export const en: SiteCopy = {
layout: {
skipLink: 'Skip to content',
brand: 'Dating JD & CV',
homeAria: 'Dating JD and CV home',
homeAria: 'Dating JD and CV home for Tiến Nguyễn Minh',
primaryNavAria: 'Primary navigation'
},
nav: {
@@ -15,28 +15,20 @@ export const en: SiteCopy = {
controls: {
themeGroup: 'Theme',
languageGroup: 'Language',
themes: {
light: 'Light',
dark: 'Dark'
},
themeActions: {
light: 'Use light theme',
dark: 'Use dark theme'
},
languages: {
en: 'EN',
vi: 'VI'
},
languageActions: {
en: 'Use English',
vi: 'Use Vietnamese'
}
},
home: {
metaTitle: 'Tien Nguyen Minh | Dating JD & CV',
metaDescription: 'A warm-witty dating job description and CV for Tien Nguyen Minh.',
metaTitle: 'Tiến Nguyn Minh | Dating JD & CV',
metaDescription: 'A warm-witty dating job description and CV for Tiến Nguyn Minh.',
heroEyebrow: 'Open role / long-term partner',
title: 'Tien Nguyen Minh',
title: 'Tiến Nguyn Minh',
lead: 'A small application packet for one future lover: equal parts job description, personal CV, and honest invitation to build something kind.',
primaryCta: 'Read the JD',
secondaryCta: 'Review the CV',
@@ -90,10 +82,10 @@ export const en: SiteCopy = {
'No pressure test. No hidden puzzle round. Strong candidates are kind when tired, direct when confused, and willing to choose each other in small boring moments.'
},
cv: {
metaTitle: 'CV | Tien Nguyen Minh',
metaDescription: 'A warm personal CV for Tien Nguyen Minh, rewritten for a future lover.',
metaTitle: 'CV | Tiến Nguyn Minh',
metaDescription: 'A warm personal CV for Tiến Nguyn Minh, rewritten for a future lover.',
heroEyebrow: 'Candidate CV',
title: 'Tien Nguyen Minh',
title: 'Tiến Nguyn Minh',
lead: 'Senior Software Engineer, backend systems builder, game-server caretaker, and candidate for one emotionally responsible long-term partnership.',
summaryAria: 'Candidate summary',
strengthsEyebrow: 'Core strengths',
-2
View File
@@ -22,9 +22,7 @@ export type SiteCopy = {
controls: {
themeGroup: string;
languageGroup: string;
themes: Record<Theme, string>;
themeActions: Record<Theme, string>;
languages: Record<Locale, string>;
languageActions: Record<Locale, string>;
};
home: {
+39 -47
View File
@@ -3,70 +3,62 @@ import type { SiteCopy } from './types';
export const vi: SiteCopy = {
layout: {
skipLink: 'Bỏ qua đến nội dung',
brand: 'JD & CV hẹn hò',
homeAria: 'Trang chủ JD và CV hẹn hò',
brand: 'Mô tả & hồ sơ hẹn hò',
homeAria: 'Trang chủ mô tả và hồ sơ hẹn hò của Tiến Nguyễn Minh',
primaryNavAria: 'Điều hướng chính'
},
nav: {
home: 'Trang chủ',
jd: 'JD',
cv: 'CV'
jd: 'Mô tả công việc',
cv: 'Hồ sơ cá nhân'
},
controls: {
themeGroup: 'Giao diện',
languageGroup: 'Ngôn ngữ',
themes: {
light: 'Sáng',
dark: 'Tối'
},
themeActions: {
light: 'ng giao diện sáng',
dark: 'ng giao diện tối'
},
languages: {
en: 'EN',
vi: 'VI'
light: 'Chuyển sang giao diện sáng',
dark: 'Chuyển sang giao diện tối'
},
languageActions: {
en: 'ng tiếng Anh',
vi: 'ng tiếng Việt'
en: 'Chuyển sang tiếng Anh',
vi: 'Chuyển sang tiếng Việt'
}
},
home: {
metaTitle: 'Tien Nguyen Minh | JD & CV hẹn hò',
metaDescription: 'JD hẹn hò và CV ấm áp, dí dỏm của Tien Nguyen Minh.',
metaTitle: 'Tiến Nguyn Minh | Mô tả & hồ sơ hẹn hò',
metaDescription: 'Mô tả hẹn hò và hồ sơ cá nhân ấm áp, dí dỏm của Tiến Nguyn Minh.',
heroEyebrow: 'Vị trí mở / bạn đồng hành dài hạn',
title: 'Tien Nguyen Minh',
lead: 'Một bộ hồ sơ nhỏ gửi đến một người yêu tương lai: vừa là mô tả công việc, vừa là CV cá nhân, vừa là lời mời nghiêm túc để cùng xây điều tử tế.',
primaryCta: 'Đọc JD',
secondaryCta: 'Xem CV',
title: 'Tiến Nguyn Minh',
lead: 'Một bộ hồ sơ nhỏ gửi đến một người yêu tương lai: vừa là mô tả công việc, vừa là hồ sơ cá nhân, vừa là lời mời nghiêm túc để cùng xây điều tử tế.',
primaryCta: 'Đọc mô tả công việc',
secondaryCta: 'Xem hồ sơ cá nhân',
packetEyebrow: 'Bộ hồ sơ ứng tuyển',
packetTitle: 'Một trò đùa nghiêm túc, được quản lý nhẹ nhàng.',
futureLoverTitle: 'Dành cho người yêu tương lai',
futureLoverBody:
'JD nói rõ quyền lợi của vị trí này: sự ổn định, chú ý, hài hước, bảo trì cảm xúc, và một người xem việc quan tâm là công việc hằng ngày chứ không phải màn ra mắt.',
'Mô tả công việc nói rõ quyền lợi của vị trí này: sự ổn định, chú ý, hài hước, chăm sóc cảm xúc, và một người xem việc quan tâm là công việc hằng ngày chứ không phải màn ra mắt.',
committeeTitle: 'Dành cho hội đồng tuyển chọn',
committeeBody:
'CV tóm tắt ứng viên: kỹ sư phần mềm, người nghĩ bằng hệ thống, kiên nhẫn debug, và một con người vẫn đều đặn refactor thói quen chưa tốt.'
'Hồ sơ cá nhân tóm tắt ứng viên: kỹ sư phần mềm, người nghĩ bằng hệ thống, kiên nhẫn gỡ lỗi, và một con người vẫn đều đặn chỉnh sửa thói quen chưa tốt.'
},
jd: {
metaTitle: 'JD | Vị trí người yêu tương lai',
metaTitle: 'Mô tả công việc | Vị trí người yêu tương lai',
metaDescription: 'Quyền lợi, trách nhiệm và ghi chú tuyển chọn cho vị trí người yêu tương lai.',
heroEyebrow: 'Mô tả công việc',
title: 'Người yêu tương lai',
lead: 'Đây là một vị trí dài hạn, làm việc trực tiếp với con người. Gói đãi ngộ chủ yếu gồm sự chú ý, lòng chung thủy, những bữa ăn chung, chuyện cười riêng, và một người biết thử lại sau sai lầm.',
summaryAria: 'Tóm tắt vị trí',
summary: [
['Loại hình', 'Full-time bằng trái tim, lịch linh hoạt'],
['Loại hình', 'Toàn thời gian bằng trái tim, lịch linh hoạt'],
['Địa điểm', 'Chủ yếu ở Trái Đất, đôi khi tại Sài Gòn'],
['Ngày bắt đầu', 'Khi niềm tin qua vòng review']
['Ngày bắt đầu', 'Khi niềm tin qua vòng xem xét']
],
benefitsEyebrow: 'Quyền lợi',
benefitsTitle: 'Bạn sẽ nhận được.',
benefits: [
'Ưu tiên hỗ trợ trong những ngày khó, ý tưởng lạ, và các buổi tối yên tĩnh.',
'Sự thành thật đều đặn, giao tiếp rõ ràng, và bug report không đổ lỗi.',
'Một người có thể debug hệ thống production và cũng overthink chuyện ăn gì tối nay.',
'Sự thành thật đều đặn, giao tiếp rõ ràng, và báo lỗi không đổ lỗi.',
'Một người có thể gỡ lỗi hệ thống đang vận hành và cũng nghĩ hơi nhiều chuyện ăn gì tối nay.',
'Lộ trình phát triển dài hạn: khỏe hơn, đi nhiều hơn, và có nhiều câu chuyện hơn.'
],
responsibilitiesEyebrow: 'Trách nhiệm',
@@ -80,54 +72,54 @@ export const vi: SiteCopy = {
niceEyebrow: 'Điểm cộng',
processTitle: 'Quy trình phỏng vấn.',
niceToHave: [
'Thích đồ ăn, đi bộ, game, phim, hoặc học những thứ lạ không vì deadline nào cả.',
'Có thể cười với một bản parody công sở mà không biến tình yêu thành OKR quý.',
'Thích đồ ăn, đi bộ, trò chơi, phim, hoặc học những thứ lạ không vì hạn chót nào cả.',
'Có thể cười với một bản nhại công sở mà không biến tình yêu thành mục tiêu quý.',
'Tin rằng tình cảm chạy tốt hơn nhờ kiên nhẫn, không phải trò đoán ý.'
],
selectionTitle: 'Ghi chú tuyển chọn',
selectionBody:
'Không có bài test áp lực. Không có vòng giải đố ẩn. Ứng viên mạnh là người vẫn tử tế khi mệt, nói thẳng khi rối, và sẵn sàng chọn nhau trong những khoảnh khắc nhỏ rất đời thường.'
'Không có bài kiểm tra áp lực. Không có vòng giải đố ẩn. Ứng viên mạnh là người vẫn tử tế khi mệt, nói thẳng khi rối, và sẵn sàng chọn nhau trong những khoảnh khắc nhỏ rất đời thường.'
},
cv: {
metaTitle: 'CV | Tien Nguyen Minh',
metaDescription: 'CV cá nhân ấm áp của Tien Nguyen Minh, viết lại cho người yêu tương lai.',
heroEyebrow: 'CV ứng viên',
title: 'Tien Nguyen Minh',
lead: 'Senior Software Engineer, người xây backend, người trông game server, và ứng viên cho một mối quan hệ dài hạn có trách nhiệm cảm xúc.',
metaTitle: 'Hồ sơ cá nhân | Tiến Nguyn Minh',
metaDescription: 'Hồ sơ cá nhân ấm áp của Tiến Nguyn Minh, viết lại cho người yêu tương lai.',
heroEyebrow: 'Hồ sơ ứng viên',
title: 'Tiến Nguyn Minh',
lead: 'Kỹ sư phần mềm cấp cao, người xây hệ thống máy chủ, người chăm sóc máy chủ trò chơi, và ứng viên cho một mối quan hệ dài hạn có trách nhiệm cảm xúc.',
summaryAria: 'Tóm tắt ứng viên',
strengthsEyebrow: 'Điểm mạnh cốt lõi',
strengthsTitle: 'Vì sao nên đưa vào shortlist.',
strengthsTitle: 'Vì sao nên đưa vào danh sách ưu tiên.',
strengths: [
{
title: 'Đáng tin khi tải cao',
body: 'Senior Software Engineer tại ZingPlay Game Studios, VNG Corp., với hơn 5 năm xây dựng và vận hành backend game multiplayer realtime.'
body: 'Kỹ sư phần mềm cấp cao tại ZingPlay Game Studios, VNG Corp., với hơn 5 năm xây dựng và vận hành hệ thống máy chủ cho trò chơi nhiều người chơi thời gian thực.'
},
{
title: 'Tư duy builder',
body: 'Quen biến ý tưởng mơ hồ thành hệ thống chạy được bằng Java, Go, TypeScript, Docker, CI, và rất nhiều debug thực tế.'
title: 'Tư duy xây dựng',
body: 'Quen biến ý tưởng mơ hồ thành hệ thống chạy được bằng Java, Go, TypeScript, Docker, tích hợp liên tục, và rất nhiều gỡ lỗi thực tế.'
},
{
title: 'Nghĩ đường dài',
body: 'Duy trì hệ thống production, test các luồng quan trọng khi release, và thích sửa bền hơn là nói cho kịch tính.'
body: 'Duy trì hệ thống đang vận hành, kiểm thử các luồng quan trọng khi phát hành, và thích sửa bền hơn là nói cho kịch tính.'
},
{
title: 'Vẫn đang nâng cấp',
body: 'Giáo trình hiện tại gồm giao tiếp rõ hơn, routine tốt hơn, đồng cảm sâu hơn, và nhớ rằng nghỉ ngơi cũng là một feature.'
body: 'Giáo trình hiện tại gồm giao tiếp rõ hơn, thói quen tốt hơn, đồng cảm sâu hơn, và nhớ rằng nghỉ ngơi cũng là một tính năng.'
}
],
factsEyebrow: 'Thông tin tham khảo',
factsTitle: 'Một vài dữ kiện hữu ích.',
facts: [
['Vai trò hiện tại', 'Senior Software Engineer'],
['Nghề chính', 'Backend cho game multiplayer'],
['Vai trò hiện tại', 'Kỹ sư phần mềm cấp cao'],
['Nghề chính', 'Hệ thống máy chủ cho trò chơi nhiều người chơi'],
['Ngôn ngữ', 'Java, Go, TypeScript, Shell'],
['Địa điểm', 'TP. Hồ Chí Minh, Việt Nam'],
['Học vấn', 'Khoa học và Kỹ thuật Máy tính, HCMUT'],
['Chế độ quan hệ', 'Static site trước, trò chuyện thật sau']
['Chế độ quan hệ', 'Trang tĩnh trước, trò chuyện thật sau']
],
closingEyebrow: 'Lời kết',
closingTitle: 'Phiên bản thành thật.',
closingBody:
'Tôi không phải một sản phẩm hoàn hảo. Tôi là một hệ thống được bảo trì: có thể quan sát, đôi khi cứng đầu, nhìn chung đáng tin, và tốt hơn nhiều khi có đúng người cùng review roadmap.'
'Tôi không phải một sản phẩm hoàn hảo. Tôi là một hệ thống được bảo trì: có thể quan sát, đôi khi cứng đầu, nhìn chung đáng tin, và tốt hơn nhiều khi có đúng người cùng xem lại lộ trình.'
}
};
+29
View File
@@ -20,6 +20,8 @@
--line: rgba(31, 111, 235, 0.2);
--glow-primary: rgba(31, 111, 235, 0.18);
--shadow: 0 24px 70px rgba(16, 32, 51, 0.14);
--motion-fast: 160ms ease;
--motion-theme: 260ms ease;
--hero-ink: #ffffff;
--hero-muted: rgba(255, 255, 255, 0.86);
--hero-accent: #9dccff;
@@ -76,12 +78,29 @@ body {
radial-gradient(circle at 12% 0%, var(--glow-primary), transparent 30rem),
linear-gradient(120deg, var(--background) 0%, var(--surface) 54%, var(--surface-muted) 100%);
color: var(--ink);
transition:
background var(--motion-theme),
color var(--motion-theme);
}
a {
color: inherit;
text-decoration-thickness: 0.08em;
text-underline-offset: 0.24em;
transition:
background-color var(--motion-fast),
border-color var(--motion-fast),
color var(--motion-fast);
}
button {
transition:
background-color var(--motion-fast),
border-color var(--motion-fast),
box-shadow var(--motion-fast),
color var(--motion-fast),
opacity var(--motion-fast),
transform var(--motion-fast);
}
a:focus-visible,
@@ -127,6 +146,16 @@ li + li {
margin-top: 0.65rem;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 1ms !important;
scroll-behavior: auto !important;
transition-duration: 1ms !important;
}
}
@media (max-width: 720px) {
h1 {
font-size: clamp(3rem, 18vw, 5rem);
+11
View File
@@ -26,6 +26,7 @@
justify-content: center;
padding: 0.85rem 1.1rem;
border: 1px solid var(--primary);
border-radius: 8px;
background: var(--primary);
color: var(--on-primary);
font-weight: 700;
@@ -65,9 +66,15 @@
.panel {
border-left: 4px solid var(--sage);
border-radius: 8px;
background: var(--panel-bg);
padding: clamp(1.1rem, 3vw, 1.6rem);
box-shadow: 0 1px 0 var(--line);
transition:
background-color var(--motion-theme),
border-color var(--motion-theme),
box-shadow var(--motion-theme),
color var(--motion-theme);
}
.panel.strong {
@@ -80,12 +87,16 @@
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1px;
margin-top: 2rem;
overflow: hidden;
border-radius: 8px;
background: var(--line);
transition: background-color var(--motion-theme);
}
.meta-grid > div {
background: var(--panel-bg);
padding: 1rem;
transition: background-color var(--motion-theme);
}
.meta-grid span {
+41 -25
View File
@@ -7,6 +7,7 @@
background: var(--primary);
color: var(--on-primary);
padding: 0.75rem 1rem;
border-radius: 8px;
}
.skip-link:focus {
@@ -30,6 +31,10 @@
border-bottom: 1px solid var(--line);
background: var(--header-bg);
backdrop-filter: blur(18px);
transition:
background-color var(--motion-theme),
border-color var(--motion-theme),
box-shadow var(--motion-theme);
}
.brand-mark,
@@ -41,6 +46,7 @@ nav {
.brand-mark {
font-weight: 700;
line-height: 1.1;
text-decoration: none;
}
@@ -54,6 +60,10 @@ nav {
background: var(--paper);
color: var(--primary);
font-family: 'Fraunces', Georgia, serif;
transition:
background-color var(--motion-theme),
border-color var(--motion-theme),
color var(--motion-theme);
}
.header-actions {
@@ -68,67 +78,73 @@ nav a {
min-height: 44px;
display: inline-flex;
align-items: center;
border-bottom: 1px solid transparent;
padding: 0.55rem 0.72rem;
border: 1px solid transparent;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
text-decoration: none;
}
nav a:hover {
border-color: currentColor;
background: var(--surface-muted);
border-color: var(--line);
}
nav a.active {
background: var(--primary);
color: var(--on-primary);
}
.preference-controls {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.65rem;
gap: 0.5rem;
flex-wrap: wrap;
}
.preference-group {
display: flex;
align-items: center;
gap: 0.4rem;
}
.preference-label {
color: var(--muted);
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
}
.segmented-control {
display: flex;
gap: 0.2rem;
padding: 0.2rem;
border: 1px solid var(--line);
border-radius: 8px;
border-radius: 999px;
background: var(--surface-translucent);
box-shadow: 0 1px 0 var(--line);
transition:
background-color var(--motion-theme),
border-color var(--motion-theme),
box-shadow var(--motion-theme);
}
.segmented-control button {
min-width: 44px;
min-height: 44px;
width: 44px;
height: 44px;
border: 0;
border-radius: 6px;
border-radius: 999px;
background: transparent;
color: var(--ink);
cursor: pointer;
font: inherit;
font-size: 0.78rem;
font-size: 1.08rem;
font-weight: 700;
line-height: 1;
touch-action: manipulation;
}
.segmented-control button:hover {
background: var(--surface-muted);
}
.segmented-control button:active {
transform: scale(0.96);
}
.segmented-control button.active {
background: var(--primary);
color: var(--on-primary);
box-shadow: 0 10px 24px var(--glow-primary);
}
@media (max-width: 720px) {
@@ -140,11 +156,11 @@ nav a:hover {
.header-actions,
.preference-controls {
width: 100%;
justify-content: flex-start;
justify-content: space-between;
}
.preference-group {
flex-direction: column;
align-items: flex-start;
nav {
max-width: 100%;
flex-wrap: wrap;
}
}
+55 -34
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { page } from '$app/state';
import favicon from '$lib/assets/favicon.svg';
import {
copy,
@@ -17,11 +18,21 @@
let { children } = $props();
const navItems = [
{ href: resolve('/'), labelKey: 'home' },
{ href: resolve('/jd/'), labelKey: 'jd' },
{ href: resolve('/cv/'), labelKey: 'cv' }
{ href: resolve('/'), labelKey: 'home', routeId: '/' },
{ href: resolve('/jd/'), labelKey: 'jd', routeId: '/jd' },
{ href: resolve('/cv/'), labelKey: 'cv', routeId: '/cv' }
] as const;
const themeIcons = {
light: '☀️',
dark: '🌙'
} as const;
const localeIcons = {
en: '🇺🇸',
vi: '🇻🇳'
} as const;
onMount(initializePreferences);
</script>
@@ -40,43 +51,53 @@
<div class="header-actions">
<nav aria-label={$copy.layout.primaryNavAria}>
{#each navItems as item (item.href)}
<a href={item.href}>{$copy.nav[item.labelKey]}</a>
<a
href={item.href}
class:active={page.route.id === item.routeId}
aria-current={page.route.id === item.routeId ? 'page' : undefined}
>
{$copy.nav[item.labelKey]}
</a>
{/each}
</nav>
<div class="preference-controls">
<div class="preference-group" role="group" aria-label={$copy.controls.themeGroup}>
<span class="preference-label">{$copy.controls.themeGroup}</span>
<div class="segmented-control">
{#each themeOptions as option (option)}
<button
type="button"
class:active={$theme === option}
aria-label={$copy.controls.themeActions[option]}
aria-pressed={$theme === option}
onclick={() => setTheme(option)}
>
{$copy.controls.themes[option]}
</button>
{/each}
</div>
<div
class="segmented-control icon-segmented"
role="group"
aria-label={$copy.controls.themeGroup}
>
{#each themeOptions as option (option)}
<button
type="button"
class:active={$theme === option}
aria-label={$copy.controls.themeActions[option]}
aria-pressed={$theme === option}
title={$copy.controls.themeActions[option]}
onclick={() => setTheme(option)}
>
<span aria-hidden="true">{themeIcons[option]}</span>
</button>
{/each}
</div>
<div class="preference-group" role="group" aria-label={$copy.controls.languageGroup}>
<span class="preference-label">{$copy.controls.languageGroup}</span>
<div class="segmented-control">
{#each localeOptions as option (option)}
<button
type="button"
class:active={$locale === option}
aria-label={$copy.controls.languageActions[option]}
aria-pressed={$locale === option}
onclick={() => setLocale(option)}
>
{$copy.controls.languages[option]}
</button>
{/each}
</div>
<div
class="segmented-control icon-segmented"
role="group"
aria-label={$copy.controls.languageGroup}
>
{#each localeOptions as option (option)}
<button
type="button"
class:active={$locale === option}
aria-label={$copy.controls.languageActions[option]}
aria-pressed={$locale === option}
title={$copy.controls.languageActions[option]}
onclick={() => setLocale(option)}
>
<span aria-hidden="true">{localeIcons[option]}</span>
</button>
{/each}
</div>
</div>
</div>
+4
View File
@@ -66,7 +66,10 @@
.facts-list {
display: grid;
gap: 1px;
overflow: hidden;
border-radius: 8px;
background: var(--line);
transition: background-color var(--motion-theme);
}
.facts-list div {
@@ -75,6 +78,7 @@
gap: 1rem;
background: var(--panel-bg);
padding: 1rem;
transition: background-color var(--motion-theme);
}
.facts-list span {