Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/app/[locale]/admin/agents/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default function AdminAgentsPage() {
const [editingId, setEditingId] = useState<string | null>(null);
const [draft, setDraft] = useState<string[]>([]);
const t = useTranslations("Admin");
const tScenario = useTranslations("Scenarios");

const load = useCallback(async () => {
setLoading(true);
Expand Down Expand Up @@ -152,7 +153,7 @@ export default function AdminAgentsPage() {
className="inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full bg-purple-50 text-purple-600"
>
<span>{sc.emoji}</span>
{sc.nameZh}
{tScenario(slug)}
</span>
) : null;
})}
Expand Down
45 changes: 29 additions & 16 deletions src/components/LocaleSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ import { routing } from "@/i18n/routing";
import { LOCALE_META } from "@/lib/locales";

/**
* Language selector. Switches the active locale while preserving the current
* path and query string. Uses next-intl's locale-aware router so the locale
* prefix is added/removed automatically (default locale stays unprefixed).
* Compact language selector. The visible chip shows only the uppercase locale
* code (e.g. `EN`, `ZH`) so its width stays small and fixed regardless of how
* long the active language's native name is (e.g. "Bahasa Indonesia"). A
* transparent native <select> is overlaid on top: it keeps full keyboard and
* screen-reader accessibility and renders the OS-native option list with the
* full native language names, so discoverability isn't lost.
*
* The current query is read from `window.location` at click time rather than
* via `useSearchParams()` to avoid forcing a Suspense boundary on the layout.
* Switching preserves the current path and query string via next-intl's
* locale-aware router (the default locale stays unprefixed). The current query
* is read from `window.location` at change time rather than via
* `useSearchParams()` to avoid forcing a Suspense boundary on the layout.
*/
export default function LocaleSwitcher({ className = "" }: { className?: string }) {
const locale = useLocale();
Expand All @@ -31,29 +36,37 @@ export default function LocaleSwitcher({ className = "" }: { className?: string
}

return (
<div className={`relative inline-flex items-center ${className}`}>
<Languages className="pointer-events-none absolute start-2.5 h-4 w-4 text-gray-400" aria-hidden />
<div
className={`relative inline-flex items-center rounded-full border border-gray-300 bg-gray-50 text-sm text-gray-700 transition-colors hover:border-purple-300 focus-within:border-purple-400 focus-within:ring-2 focus-within:ring-purple-100 ${
isPending ? "opacity-60" : ""
} ${className}`}
>
<Languages className="pointer-events-none absolute start-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" aria-hidden />
{/* Visible label — just the short code, keeps the chip narrow. */}
<span className="pointer-events-none py-1.5 ps-8 pe-7 font-medium uppercase">{locale}</span>
<svg
className="pointer-events-none absolute end-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-gray-400"
viewBox="0 0 12 12"
fill="none"
aria-hidden
>
<path d="M3 4.5 6 7.5 9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
{/* Transparent native <select> on top: drives selection + a11y, while the
OS option list still shows full native language names. */}
<select
value={locale}
onChange={(e) => onSelect(e.target.value)}
disabled={isPending}
aria-label={t("label")}
className="appearance-none rounded-full border border-gray-300 bg-gray-50 py-1.5 ps-8 pe-7 text-sm text-gray-700 outline-none transition-colors hover:border-purple-300 focus:border-purple-400 focus:ring-2 focus:ring-purple-100 disabled:opacity-60"
className="absolute inset-0 h-full w-full cursor-pointer appearance-none opacity-0 outline-none disabled:cursor-default"
>
{routing.locales.map((l) => (
<option key={l} value={l}>
{LOCALE_META[l].nativeLabel}
</option>
))}
</select>
<svg
className="pointer-events-none absolute end-2.5 h-3.5 w-3.5 text-gray-400"
viewBox="0 0 12 12"
fill="none"
aria-hidden
>
<path d="M3 4.5 6 7.5 9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
);
}
8 changes: 5 additions & 3 deletions src/components/ui/AgentCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getTranslations } from "next-intl/server";
import { Link } from "@/i18n/navigation";
import { Zap, Layers, Star, GitFork } from "lucide-react";
import { findScenario, scenarioLabel } from "@/lib/scenarios";
import { findScenario } from "@/lib/scenarios";

// Minimal translator shape so priceLabel can be called with or without i18n.
type Translator = (key: string, values?: Record<string, string | number>) => string;
Expand Down Expand Up @@ -57,6 +57,7 @@ function formatStars(n: number): string {

export default async function AgentCard({ agent }: { agent: AgentCardData }) {
const t = await getTranslations("Components");
const tScenario = await getTranslations("Scenarios");
const isProject = agent.kind === "PROJECT";
return (
<Link
Expand Down Expand Up @@ -87,14 +88,15 @@ export default async function AgentCard({ agent }: { agent: AgentCardData }) {
{agent.scenarios.slice(0, 2).map((slug) => {
const sc = findScenario(slug);
if (!sc) return null;
const label = tScenario(slug);
return (
<span
key={slug}
title={scenarioLabel(sc)}
title={label}
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium bg-purple-50 text-purple-600 max-w-full truncate"
>
<span>{sc.emoji}</span>
{sc.nameZh}
{label}
</span>
);
})}
Expand Down
Loading