feat(frontend): Ambient/WebGL console revamp + lint/type cleanup
Ground-up rebuild of the GMW frontend as an Ambient Field console: - WebGL ambient background (Three.js shader, drifting motes, reduced-motion aware) - Glassmorphism dark cyber theme across all 8 routes - SSR page + client view split with SWR fallback; realtime via WebSocket - Command palette (Cmd+K), chatbot FAB, guild/channel pickers - Chart primitives: donut, radial-gauge, area-activity, sparkline, equalizer Cleanup (review pass): - Remove stray Puppeteer nav-test/nav-debug scripts - Replace non-null assertions with guards (dashboard/moderation) - Drop unused useGuilds fetches in messages/voice views - Type implicit-any `let` declarations across pages - Add a11y roles/labels to SVG charts and audio, tidy imports
This commit is contained in:
@@ -2,7 +2,10 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
function initials(name?: string | null): string {
|
||||
if (!name) return "?";
|
||||
const parts = name.replace(/[^\p{L}\p{N} _]/gu, "").trim().split(/\s+/);
|
||||
const parts = name
|
||||
.replace(/[^\p{L}\p{N} _]/gu, "")
|
||||
.trim()
|
||||
.split(/\s+/);
|
||||
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
||||
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Slot } from "./slot";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Slot } from "./slot";
|
||||
|
||||
type Variant = "primary" | "ghost" | "outline" | "danger" | "subtle";
|
||||
type Size = "sm" | "md" | "lg" | "icon";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export { Button } from "./button";
|
||||
export { Badge } from "./badge";
|
||||
export { GlassPanel, GlassCard } from "./card";
|
||||
export { Input, Textarea } from "./input";
|
||||
export { Skeleton } from "./skeleton";
|
||||
export { Avatar } from "./avatar";
|
||||
export { Select } from "./select";
|
||||
export type { SelectOption } from "./select";
|
||||
export { Toaster, toast, useToast } from "./toast";
|
||||
export { Badge } from "./badge";
|
||||
export { Button } from "./button";
|
||||
export { GlassCard, GlassPanel } from "./card";
|
||||
export { Input, Textarea } from "./input";
|
||||
export { Progress, Spinner } from "./progress";
|
||||
export type { SelectOption } from "./select";
|
||||
export { Select } from "./select";
|
||||
export { Skeleton } from "./skeleton";
|
||||
export { Toaster, toast, useToast } from "./toast";
|
||||
export { Tooltip } from "./tooltip";
|
||||
|
||||
@@ -17,10 +17,19 @@ export function Progress({
|
||||
? "var(--color-amber)"
|
||||
: "var(--color-signal)";
|
||||
return (
|
||||
<div className={cn("h-1.5 w-full overflow-hidden rounded-full bg-white/8", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-1.5 w-full overflow-hidden rounded-full bg-white/8",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="h-full rounded-full transition-[width] duration-500"
|
||||
style={{ width: `${pct}%`, background: color, boxShadow: `0 0 12px -2px ${color}` }}
|
||||
style={{
|
||||
width: `${pct}%`,
|
||||
background: color,
|
||||
boxShadow: `0 0 12px -2px ${color}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface SelectOption {
|
||||
@@ -31,7 +31,8 @@ export function Select({
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||||
if (ref.current && !ref.current.contains(e.target as Node))
|
||||
setOpen(false);
|
||||
};
|
||||
document.addEventListener("mousedown", onDoc);
|
||||
return () => document.removeEventListener("mousedown", onDoc);
|
||||
@@ -54,7 +55,10 @@ export function Select({
|
||||
{selected?.label ?? placeholder}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={cn("size-4 shrink-0 text-ink-faint transition-transform", open && "rotate-180")}
|
||||
className={cn(
|
||||
"size-4 shrink-0 text-ink-faint transition-transform",
|
||||
open && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -76,11 +80,17 @@ export function Select({
|
||||
}}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-2 rounded-[9px] px-3 py-2 text-left text-sm transition-colors",
|
||||
o.value === value ? "bg-signal/15 text-signal" : "text-ink hover:bg-white/6",
|
||||
o.value === value
|
||||
? "bg-signal/15 text-signal"
|
||||
: "text-ink hover:bg-white/6",
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{o.label}</span>
|
||||
{o.hint && <span className="mono text-[0.65rem] text-ink-faint">{o.hint}</span>}
|
||||
{o.hint && (
|
||||
<span className="mono text-[0.65rem] text-ink-faint">
|
||||
{o.hint}
|
||||
</span>
|
||||
)}
|
||||
{o.value === value && <Check className="size-3.5 shrink-0" />}
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -6,22 +6,26 @@ import * as React from "react";
|
||||
* Minimal Slot — merges its props onto its single child element (Radix-style
|
||||
* `asChild`). Enough for wrapping <Link>/<a> in a Button.
|
||||
*/
|
||||
export const Slot = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>(
|
||||
({ children, ...slotProps }, ref) => {
|
||||
if (!React.isValidElement(children)) return null;
|
||||
const childProps = children.props as Record<string, unknown>;
|
||||
const merged: Record<string, unknown> = { ...childProps, ...slotProps, ref };
|
||||
// Merge className
|
||||
if (slotProps.className || childProps.className) {
|
||||
merged.className = [childProps.className, slotProps.className]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
// Merge style
|
||||
if (slotProps.style || childProps.style) {
|
||||
merged.style = { ...(childProps.style as object), ...(slotProps.style as object) };
|
||||
}
|
||||
return React.cloneElement(children, merged);
|
||||
},
|
||||
);
|
||||
export const Slot = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }
|
||||
>(({ children, ...slotProps }, ref) => {
|
||||
if (!React.isValidElement(children)) return null;
|
||||
const childProps = children.props as Record<string, unknown>;
|
||||
const merged: Record<string, unknown> = { ...childProps, ...slotProps, ref };
|
||||
// Merge className
|
||||
if (slotProps.className || childProps.className) {
|
||||
merged.className = [childProps.className, slotProps.className]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
// Merge style
|
||||
if (slotProps.style || childProps.style) {
|
||||
merged.style = {
|
||||
...(childProps.style as object),
|
||||
...(slotProps.style as object),
|
||||
};
|
||||
}
|
||||
return React.cloneElement(children, merged);
|
||||
});
|
||||
Slot.displayName = "Slot";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { AlertTriangle, CheckCircle2, Info, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CheckCircle2, AlertTriangle, Info, X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ToastTone = "signal" | "vermilion" | "neutral";
|
||||
@@ -63,7 +63,12 @@ export function Toaster({ position = "bottom-right" }: { position?: string }) {
|
||||
: "bottom-4 left-1/2 -translate-x-1/2";
|
||||
|
||||
return (
|
||||
<div className={cn("pointer-events-none fixed z-[100] flex w-[min(92vw,360px)] flex-col gap-2", pos)}>
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none fixed z-[100] flex w-[min(92vw,360px)] flex-col gap-2",
|
||||
pos,
|
||||
)}
|
||||
>
|
||||
{items.map((t) => {
|
||||
const Icon = icons[t.tone];
|
||||
return (
|
||||
@@ -83,7 +88,9 @@ export function Toaster({ position = "bottom-right" }: { position?: string }) {
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-semibold text-ink">{t.title}</div>
|
||||
{t.description && (
|
||||
<div className="mt-0.5 text-xs text-ink-soft">{t.description}</div>
|
||||
<div className="mt-0.5 text-xs text-ink-soft">
|
||||
{t.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -21,6 +21,7 @@ export function Tooltip({
|
||||
onMouseLeave={() => setShow(false)}
|
||||
onFocus={() => setShow(true)}
|
||||
onBlur={() => setShow(false)}
|
||||
role="presentation"
|
||||
>
|
||||
{children}
|
||||
{show && (
|
||||
|
||||
Reference in New Issue
Block a user