New Chnages to Switch Theme
This commit is contained in:
Generated
+11
@@ -24,6 +24,7 @@
|
|||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/circle-flags": "^1.2.10",
|
||||||
"@iconify-json/lets-icons": "^1.2.1",
|
"@iconify-json/lets-icons": "^1.2.1",
|
||||||
"@iconify-json/mdi": "^1.2.3",
|
"@iconify-json/mdi": "^1.2.3",
|
||||||
"@iconify-json/solar": "^1.2.5",
|
"@iconify-json/solar": "^1.2.5",
|
||||||
@@ -841,6 +842,16 @@
|
|||||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@iconify-json/circle-flags": {
|
||||||
|
"version": "1.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify-json/circle-flags/-/circle-flags-1.2.10.tgz",
|
||||||
|
"integrity": "sha512-sZRxs689a281RtZvuAiKtV7pQHv8Tk0HkinSM7QvLgdLEK8xgGRCcbDvL09Rq+/KtemmsMzGhS9/qt+r3ca+Ug==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/types": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@iconify-json/lets-icons": {
|
"node_modules/@iconify-json/lets-icons": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@iconify-json/lets-icons/-/lets-icons-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@iconify-json/lets-icons/-/lets-icons-1.2.1.tgz",
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/circle-flags": "^1.2.10",
|
||||||
"@iconify-json/lets-icons": "^1.2.1",
|
"@iconify-json/lets-icons": "^1.2.1",
|
||||||
"@iconify-json/mdi": "^1.2.3",
|
"@iconify-json/mdi": "^1.2.3",
|
||||||
"@iconify-json/solar": "^1.2.5",
|
"@iconify-json/solar": "^1.2.5",
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
---
|
||||||
|
import type { HTMLAttributes } from "astro/types";
|
||||||
|
import Dropdown from "./Dropdown.astro";
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
|
||||||
|
interface Language {
|
||||||
|
code: string;
|
||||||
|
label: string;
|
||||||
|
flag?: string;
|
||||||
|
dir?: "ltr" | "rtl";
|
||||||
|
}
|
||||||
|
interface Props extends HTMLAttributes<"div"> {
|
||||||
|
languages: Language[];
|
||||||
|
storageKey?: string;
|
||||||
|
rootSelector?: string;
|
||||||
|
compact?: boolean;
|
||||||
|
showLabel?: boolean;
|
||||||
|
rootExtraClass?: string;
|
||||||
|
class?: string;
|
||||||
|
onChange?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
languages = [{ code: "en", label: "English" }],
|
||||||
|
storageKey = "wr-lang",
|
||||||
|
rootSelector = "html",
|
||||||
|
compact = false,
|
||||||
|
showLabel = false,
|
||||||
|
rootExtraClass = "",
|
||||||
|
class: className,
|
||||||
|
onChange = "onLanguageChange",
|
||||||
|
...rest
|
||||||
|
}: Props = Astro.props;
|
||||||
|
|
||||||
|
const containerId = `ls-${Math.random().toString(36).slice(2)}`;
|
||||||
|
const icon = "wr:size-4";
|
||||||
|
const item = "wr:flex wr:items-center wr:gap-3 wr:w-full wr:px-3 wr:py-2.5 wr:rounded-lg wr:text-sm wr:hover:bg-muted/70 wr:transition wr:duration-150";
|
||||||
|
const checkBase = "wr:size-4 wr:opacity-0 wr:scale-90 wr:transition";
|
||||||
|
---
|
||||||
|
|
||||||
|
<div id={containerId} class={["wr:inline-flex", className].join(" ")} data-compact={compact ? "1" : ""} {...rest}>
|
||||||
|
<Dropdown openOn="click" placement="bottom-end" offset={5} shiftPadding={10} showArrow={true} closeOnSelect={true}>
|
||||||
|
<span slot="trigger" class="wr:inline-flex wr:items-center wr:gap-2">
|
||||||
|
<span data-current-icon class="wr:inline">
|
||||||
|
<Icon name="mdi:translate" class={icon} />
|
||||||
|
</span>
|
||||||
|
{showLabel && <span data-current-label class="wr:text-sm wr:font-medium">Language</span>}
|
||||||
|
<svg data-dd-chev class="wr:size-4 wr:opacity-70 wr:transition-transform wr:duration-150" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M7 10l5 5 5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{languages.map(lang => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitemradio"
|
||||||
|
aria-checked="false"
|
||||||
|
class={item}
|
||||||
|
data-dd-item
|
||||||
|
data-set-lang={lang.code}
|
||||||
|
data-owner={containerId}
|
||||||
|
>
|
||||||
|
<div class="wr:flex wr:items-center wr:gap-3">
|
||||||
|
<Icon name={lang.flag || "mdi:earth"} class={icon} />
|
||||||
|
</div>
|
||||||
|
<div class="wr:flex-1 wr:font-medium">{lang.label}</div>
|
||||||
|
<Icon name="mdi:check" class={checkBase} data-check />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style is:inline>
|
||||||
|
#{containerId}[data-compact="1"] [data-dd-trigger]{
|
||||||
|
height: 2.25rem;
|
||||||
|
padding-left: .5rem;
|
||||||
|
padding-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show check icon ONLY on active */
|
||||||
|
#{containerId} [role="menuitemradio"][aria-checked="true"] [data-check]{
|
||||||
|
opacity: 1 !important;
|
||||||
|
transform: scale(1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle active row styling */
|
||||||
|
#{containerId} [role="menuitemradio"][aria-checked="true"]{
|
||||||
|
background-color: color-mix(in hsl, hsl(var(--muted)) 70%, transparent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script is:inline define:vars={{ containerId, storageKey, rootSelector, rootExtraClass, onChange, languages }}>
|
||||||
|
(() => {
|
||||||
|
const host = document.getElementById(containerId);
|
||||||
|
if (!host) return;
|
||||||
|
|
||||||
|
const root = rootSelector === "html"
|
||||||
|
? document.documentElement
|
||||||
|
: document.querySelector(rootSelector) || document.documentElement;
|
||||||
|
|
||||||
|
const itemSelector = `[data-set-lang][data-owner="${containerId}"]`;
|
||||||
|
|
||||||
|
function reflectUI(code) {
|
||||||
|
// set label
|
||||||
|
const labelEl = host.querySelector("[data-current-label]");
|
||||||
|
if (labelEl) {
|
||||||
|
const lang = languages.find(l => l.code === code) || { label: code };
|
||||||
|
labelEl.textContent = lang.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark active items (remember they are portaled)
|
||||||
|
document.querySelectorAll(itemSelector).forEach(el => {
|
||||||
|
const active = el.getAttribute("data-set-lang") === code;
|
||||||
|
el.setAttribute("aria-checked", active ? "true" : "false");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHtmlDirIfNeeded(code) {
|
||||||
|
const lang = languages.find(l => l.code === code);
|
||||||
|
if (!lang) return;
|
||||||
|
if (lang.dir === "rtl") {
|
||||||
|
root.setAttribute("dir", "rtl");
|
||||||
|
} else {
|
||||||
|
root.setAttribute("dir", "ltr");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply(code) {
|
||||||
|
// Save to localStorage
|
||||||
|
localStorage.setItem(storageKey, code);
|
||||||
|
|
||||||
|
// Save cookie (client-side cookie, non-HttpOnly so JS + server-side dev proxy can read during dev)
|
||||||
|
// builds an expiry 365 days by default here:
|
||||||
|
// const days = 365;
|
||||||
|
// const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString();
|
||||||
|
// do NOT include HttpOnly in client-side cookie string
|
||||||
|
// const cookieStr = `${storageKey}=${encodeURIComponent(code)}; Path=/; Expires=${expires}; SameSite=Lax`;
|
||||||
|
// document.cookie = cookieStr;
|
||||||
|
|
||||||
|
// apply direction if language has dir
|
||||||
|
setHtmlDirIfNeeded(code);
|
||||||
|
|
||||||
|
root.setAttribute("lang", code);
|
||||||
|
|
||||||
|
reflectUI(code);
|
||||||
|
|
||||||
|
// invoke global callback if present
|
||||||
|
const fn = window[onChange];
|
||||||
|
if (typeof fn === "function") {
|
||||||
|
try { fn(code); } catch (e) { console.warn("onChange handler threw", e); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize from (1) cookie, (2) localStorage, (3) navigator.language, (4) first language prop
|
||||||
|
function readCookie(name) {
|
||||||
|
const match = document.cookie.split("; ").find(c => c.startsWith(name + "="));
|
||||||
|
return match ? decodeURIComponent(match.split("=")[1]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedCookie = readCookie(storageKey);
|
||||||
|
const savedLS = localStorage.getItem(storageKey);
|
||||||
|
const navLang = navigator.language ? navigator.language.split("-")[0] : null;
|
||||||
|
const initial = savedCookie || savedLS || (languages.some(l => l.code === navLang) ? navLang : languages[0].code);
|
||||||
|
|
||||||
|
// initial apply
|
||||||
|
apply(initial);
|
||||||
|
|
||||||
|
// Listen globally (portaled items)
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
const btn = e.target?.closest?.(itemSelector);
|
||||||
|
if (!btn) return;
|
||||||
|
const code = btn.getAttribute("data-set-lang");
|
||||||
|
if (!code) return;
|
||||||
|
apply(code);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -5,7 +5,7 @@ import { cn } from "../utils/cn";
|
|||||||
const attributes = Astro.props;
|
const attributes = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang="en">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ import Base from "./Base.astro";
|
|||||||
<li class="list-item">
|
<li class="list-item">
|
||||||
<a href="/table">Table</a>
|
<a href="/table">Table</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="list-item">
|
||||||
|
<a href="/language">Language</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
import LanguageSwitcher from "../components/LanguageSwitcher.astro";
|
||||||
|
import ComponentLayout from "../layouts/ComponentLayout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentLayout>
|
||||||
|
<div
|
||||||
|
class="wrnx wr:flex wr:items-center wr:flex-col wr:gap-4 wr:w-full wr:h-screen"
|
||||||
|
>
|
||||||
|
<div class="wr:flex wr:items-center wr:gap-3 wr:pb-3 wr:p-4">
|
||||||
|
<LanguageSwitcher
|
||||||
|
languages={[
|
||||||
|
{ code: "en", label: "English", flag: "circle-flags:in" },
|
||||||
|
{ code: "es", label: "Español" },
|
||||||
|
{ code: "fr", label: "Français" },
|
||||||
|
{ code: "de", label: "Deutsch" },
|
||||||
|
{ code: "zh", label: "中文" },
|
||||||
|
{ code: "ar", label: "العربية", dir: "rtl" },
|
||||||
|
]}
|
||||||
|
showLabel={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="wr:text-primary">Test Heading</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ComponentLayout>
|
||||||
Reference in New Issue
Block a user