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
477 changes: 403 additions & 74 deletions web/src/App.tsx

Large diffs are not rendered by default.

76 changes: 47 additions & 29 deletions web/src/components/LanguageSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useState, useRef, useEffect } from "react";
import { createPortal } from "react-dom";
import { Check } from "lucide-react";
import { Button } from "@nous-research/ui/ui/components/button";
import { BottomPickSheet } from "@/components/BottomPickSheet";
import { Typography } from "@/components/NouiTypography";
Expand All @@ -25,10 +27,11 @@ import { cn } from "@/lib/utils";
* viewport / overflow ancestors. Below the `sm` breakpoint, `dropUp` uses a
* bottom sheet portaled to `document.body` instead of an anchored dropdown.
*/
export function LanguageSwitcher({ dropUp = false }: LanguageSwitcherProps) {
export function LanguageSwitcher({ collapsed = false, dropUp = false }: LanguageSwitcherProps) {
const { locale, setLocale, t } = useI18n();
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const narrowViewport = useBelowBreakpoint(640);
const useMobileSheet = Boolean(dropUp && narrowViewport);

Expand All @@ -41,15 +44,14 @@ export function LanguageSwitcher({ dropUp = false }: LanguageSwitcherProps) {
return () => document.removeEventListener("keydown", onKey);
}, [open]);

// Outside-click closing only for anchored dropdown β€” sheet uses backdrop + portal.
useEffect(() => {
if (!open || useMobileSheet) return;

function onPointerDown(e: PointerEvent) {
if (!containerRef.current) return;
if (!containerRef.current.contains(e.target as Node)) {
setOpen(false);
}
const target = e.target as Node;
if (containerRef.current?.contains(target)) return;
if (dropdownRef.current?.contains(target)) return;
setOpen(false);
}

document.addEventListener("pointerdown", onPointerDown);
Expand All @@ -69,7 +71,10 @@ export function LanguageSwitcher({ dropUp = false }: LanguageSwitcherProps) {
aria-label={t.language.switchTo}
aria-haspopup="listbox"
aria-expanded={open}
className="px-2 py-1 normal-case tracking-normal font-normal text-xs text-text-secondary hover:text-foreground"
className={cn(
"px-2 py-1 normal-case tracking-normal font-normal text-xs text-text-secondary hover:text-foreground",
collapsed && "hover:bg-transparent",
)}
>
<span className="inline-flex items-center gap-1.5">
<Typography
Expand Down Expand Up @@ -99,23 +104,33 @@ export function LanguageSwitcher({ dropUp = false }: LanguageSwitcherProps) {
</BottomPickSheet>
)}

{open && !useMobileSheet && (
<div
aria-label={sheetTitle}
className={cn(
"absolute right-0 z-50 min-w-[10rem] rounded-md border border-border bg-popover shadow-md py-1 max-h-80 overflow-y-auto",
dropUp ? "bottom-full mb-1" : "top-full mt-1",
)}
role="listbox"
>
<LanguageSwitcherOptions
allLocales={allLocales}
locale={locale}
setLocale={setLocale}
setOpen={setOpen}
/>
</div>
)}
{open && !useMobileSheet && (() => {
const rect = containerRef.current?.getBoundingClientRect();
const dropdown = (
<div
ref={dropdownRef}
aria-label={sheetTitle}
className={cn(
"min-w-[10rem] border border-border bg-popover shadow-md py-1 max-h-80 overflow-y-auto",
dropUp ? "fixed z-[100]" : "absolute z-50 right-0 top-full mt-1",
)}
role="listbox"
style={
dropUp && rect
? { bottom: window.innerHeight - rect.top + 4, left: rect.left }
: undefined
}
>
<LanguageSwitcherOptions
allLocales={allLocales}
locale={locale}
setLocale={setLocale}
setOpen={setOpen}
/>
</div>
);
return dropUp ? createPortal(dropdown, document.body) : dropdown;
Comment thread
austinpickett marked this conversation as resolved.
})()}
</div>
);
}
Expand All @@ -134,10 +149,12 @@ function LanguageSwitcherOptions({
return (
<button
aria-selected={selected}
className={
"w-full text-left px-3 py-1.5 text-xs flex items-center gap-2 hover:bg-accent hover:text-accent-foreground transition-colors " +
(selected ? "font-semibold text-foreground" : "text-muted-foreground")
}
className={cn(
"w-full text-left px-3 py-1.5 flex items-center gap-2 cursor-pointer",
"font-mondwest text-display text-xs tracking-[0.08em]",
"hover:bg-accent hover:text-accent-foreground transition-colors",
selected ? "font-semibold text-foreground" : "text-muted-foreground",
)}
key={code}
onClick={() => {
setLocale(code);
Expand All @@ -148,7 +165,7 @@ function LanguageSwitcherOptions({
>
<span className="truncate">{meta.name}</span>

{selected && <span className="ml-auto text-xs">βœ“</span>}
{selected && <Check className="ml-auto h-3 w-3 shrink-0 text-midground" />}
</button>
);
})}
Expand All @@ -164,5 +181,6 @@ interface LanguageSwitcherOptionsProps {
}

interface LanguageSwitcherProps {
collapsed?: boolean;
dropUp?: boolean;
}
9 changes: 6 additions & 3 deletions web/src/components/SidebarFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Typography } from "@/components/NouiTypography";
import { useSidebarStatus } from "@/hooks/useSidebarStatus";
import type { StatusResponse } from "@/lib/api";
import { cn } from "@/lib/utils";
import { useI18n } from "@/i18n";

export function SidebarFooter() {
const status = useSidebarStatus();
export function SidebarFooter({ status }: SidebarFooterProps) {
const { t } = useI18n();

return (
Expand Down Expand Up @@ -37,3 +36,7 @@ export function SidebarFooter() {
</div>
);
}

interface SidebarFooterProps {
status: StatusResponse | null;
}
10 changes: 6 additions & 4 deletions web/src/components/SidebarStatusStrip.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { Link } from "react-router-dom";
import type { StatusResponse } from "@/lib/api";
import { useSidebarStatus } from "@/hooks/useSidebarStatus";
import { cn } from "@/lib/utils";
import { useI18n } from "@/i18n";

/** Gateway + session summary for the System sidebar block (no separate strip chrome). */
export function SidebarStatusStrip() {
const status = useSidebarStatus();
export function SidebarStatusStrip({ status }: SidebarStatusStripProps) {
const { t } = useI18n();

if (status === null) {
Expand Down Expand Up @@ -50,7 +48,7 @@ export function SidebarStatusStrip() {
);
}

function gatewayLine(
export function gatewayLine(
status: StatusResponse,
t: ReturnType<typeof useI18n>["t"],
): { label: string; tone: string } {
Expand All @@ -68,3 +66,7 @@ function gatewayLine(
? { label: g.running, tone: "text-success" }
: { label: g.off, tone: "text-muted-foreground" };
}

interface SidebarStatusStripProps {
status: StatusResponse | null;
}
102 changes: 60 additions & 42 deletions web/src/components/ThemeSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { Palette, Check } from "lucide-react";
import { Button } from "@nous-research/ui/ui/components/button";
import { ListItem } from "@nous-research/ui/ui/components/list-item";
Expand All @@ -23,11 +24,12 @@ import { cn } from "@/lib/utils";
* bottom sheet portaled to `document.body` so the picker is not clipped by
* the sidebar (same idea as a responsive Drawer).
*/
export function ThemeSwitcher({ dropUp = false }: ThemeSwitcherProps) {
export function ThemeSwitcher({ collapsed = false, dropUp = false }: ThemeSwitcherProps) {
const { themeName, availableThemes, setTheme } = useTheme();
const { t } = useI18n();
const [open, setOpen] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const narrowViewport = useBelowBreakpoint(640);
const useMobileSheet = Boolean(dropUp && narrowViewport);

Expand All @@ -45,12 +47,10 @@ export function ThemeSwitcher({ dropUp = false }: ThemeSwitcherProps) {
useEffect(() => {
if (!open || useMobileSheet) return;
const onMouseDown = (e: MouseEvent) => {
if (
wrapperRef.current &&
!wrapperRef.current.contains(e.target as Node)
) {
close();
}
const target = e.target as Node;
if (wrapperRef.current?.contains(target)) return;
if (dropdownRef.current?.contains(target)) return;
close();
};
document.addEventListener("mousedown", onMouseDown);
return () => document.removeEventListener("mousedown", onMouseDown);
Expand All @@ -64,22 +64,29 @@ export function ThemeSwitcher({ dropUp = false }: ThemeSwitcherProps) {
<div ref={wrapperRef} className="relative">
<Button
ghost
size={collapsed ? "icon" : undefined}
onClick={() => setOpen((o) => !o)}
className="px-2 py-1 normal-case tracking-normal font-normal text-xs text-text-secondary hover:text-foreground"
title={t.theme?.switchTheme ?? "Switch theme"}
className={cn(
collapsed
? "text-text-secondary hover:text-foreground hover:bg-transparent"
: "px-2 py-1 normal-case tracking-normal font-normal text-xs text-text-secondary hover:text-foreground",
)}
title={`${t.theme?.switchTheme ?? "Switch theme"}: ${label}`}
aria-label={t.theme?.switchTheme ?? "Switch theme"}
aria-expanded={open}
aria-haspopup="listbox"
>
<span className="inline-flex items-center gap-1.5">
<Palette className="h-3.5 w-3.5" />

<Typography
mondwest
className="hidden sm:inline text-display tracking-wide text-xs"
>
{label}
</Typography>
{!collapsed && (
<Typography
mondwest
className="hidden sm:inline text-display tracking-wide text-xs"
>
{label}
</Typography>
)}
</span>
</Button>

Expand All @@ -101,34 +108,44 @@ export function ThemeSwitcher({ dropUp = false }: ThemeSwitcherProps) {
</BottomPickSheet>
)}

{open && !useMobileSheet && (
<div
aria-label={sheetTitle}
className={cn(
"absolute z-50 min-w-[240px] max-h-[70dvh] overflow-y-auto",
dropUp ? "left-0 bottom-full mb-1" : "right-0 top-full mt-1",
"border border-current/20 bg-background-base/95 backdrop-blur-sm",
"shadow-[0_12px_32px_-8px_rgba(0,0,0,0.6)]",
)}
role="listbox"
>
<div className="border-b border-current/20 px-3 py-2">
<Typography
mondwest
className="text-display text-xs tracking-[0.12em] text-text-tertiary"
>
{sheetTitle}
</Typography>
</div>
{open && !useMobileSheet && (() => {
const rect = wrapperRef.current?.getBoundingClientRect();
const dropdown = (
<div
ref={dropdownRef}
aria-label={sheetTitle}
className={cn(
"min-w-[240px] max-h-[70dvh] overflow-y-auto",
"border border-current/20 bg-background-base/95 backdrop-blur-sm",
"shadow-[0_12px_32px_-8px_rgba(0,0,0,0.6)]",
dropUp ? "fixed z-[100]" : "absolute z-50 right-0 top-full mt-1",
)}
role="listbox"
style={
dropUp && rect
? { bottom: window.innerHeight - rect.top + 4, left: rect.left }
: undefined
}
>
<div className="border-b border-current/20 px-3 py-2">
<Typography
mondwest
className="text-display text-xs tracking-[0.12em] text-text-tertiary"
>
{sheetTitle}
</Typography>
</div>

<ThemeSwitcherOptions
availableThemes={availableThemes}
close={close}
setTheme={setTheme}
themeName={themeName}
/>
</div>
)}
<ThemeSwitcherOptions
availableThemes={availableThemes}
close={close}
setTheme={setTheme}
themeName={themeName}
/>
</div>
);
return dropUp ? createPortal(dropdown, document.body) : dropdown;
Comment thread
austinpickett marked this conversation as resolved.
})()}
</div>
);
}
Expand Down Expand Up @@ -221,5 +238,6 @@ interface ThemeSwitcherOptionsProps {
}

interface ThemeSwitcherProps {
collapsed?: boolean;
dropUp?: boolean;
}
3 changes: 2 additions & 1 deletion web/src/i18n/af.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export const af: Translations = {

sessions: {
title: "Sessies",
history: "Geskiedenis",
overview: "Oorsig",
searchPlaceholder: "Soek boodskap-inhoud...",
noSessions: "Nog geen sessies nie",
Expand Down Expand Up @@ -422,7 +423,7 @@ export const af: Translations = {
},

language: {
switchTo: "Skakel oor na Engels",
switchTo: "Verander taal",
},

theme: {
Expand Down
3 changes: 2 additions & 1 deletion web/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export const de: Translations = {

sessions: {
title: "Sitzungen",
history: "Verlauf",
overview: "Übersicht",
searchPlaceholder: "Nachrichteninhalt suchen...",
noSessions: "Noch keine Sitzungen",
Expand Down Expand Up @@ -422,7 +423,7 @@ export const de: Translations = {
},

language: {
switchTo: "Zu Englisch wechseln",
switchTo: "Sprache wechseln",
},

theme: {
Expand Down
3 changes: 2 additions & 1 deletion web/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export const en: Translations = {

sessions: {
title: "Sessions",
history: "History",
overview: "Overview",
searchPlaceholder: "Search message content...",
noSessions: "No sessions yet",
Expand Down Expand Up @@ -422,7 +423,7 @@ export const en: Translations = {
},

language: {
switchTo: "Switch to Chinese",
switchTo: "Switch language",
},

theme: {
Expand Down
Loading
Loading