Compare commits
2
Commits
5196cb0221
...
a54c34c6b3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a54c34c6b3 | ||
|
|
7375e2ed52 |
No files matched your search
@@ -58,7 +58,7 @@ export function DashboardView({
|
|||||||
error,
|
error,
|
||||||
mutate: mutateStats,
|
mutate: mutateStats,
|
||||||
} = useStats(initialStats);
|
} = useStats(initialStats);
|
||||||
const { data: activity } = useActivity(14, initialActivity as never);
|
const { data: activity } = useActivity(14, initialActivity);
|
||||||
const { data: reactions } = useTopReactions();
|
const { data: reactions } = useTopReactions();
|
||||||
const ambient = useAmbient();
|
const ambient = useAmbient();
|
||||||
|
|
||||||
|
|||||||
@@ -35,15 +35,18 @@ export function AmbientCanvas({
|
|||||||
let raf = 0;
|
let raf = 0;
|
||||||
let last = performance.now();
|
let last = performance.now();
|
||||||
|
|
||||||
// Lerp state
|
// Lerp state (start values match the inline style defaults on the element).
|
||||||
let r = 45,
|
let r = 45,
|
||||||
g = 212,
|
g = 212,
|
||||||
b = 191;
|
b = 191;
|
||||||
let targetR = 45,
|
|
||||||
targetG = 212,
|
|
||||||
targetB = 191;
|
|
||||||
let intensity = 0.35;
|
let intensity = 0.35;
|
||||||
let targetIntensity = 0.35;
|
// Last values actually written to the DOM — lets us skip `setProperty`
|
||||||
|
// entirely once a channel settles, so the continuous loop stops invalidating
|
||||||
|
// style/layout every frame when the ambient is static.
|
||||||
|
let lastR = -1,
|
||||||
|
lastG = -1,
|
||||||
|
lastB = -1,
|
||||||
|
lastAlpha = "-1";
|
||||||
|
|
||||||
const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
|
const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
|
||||||
|
|
||||||
@@ -53,21 +56,34 @@ export function AmbientCanvas({
|
|||||||
|
|
||||||
const tgt = targetRef.current;
|
const tgt = targetRef.current;
|
||||||
const [tr, tg, tb] = TONE_css[tgt.tone].split(",").map(Number);
|
const [tr, tg, tb] = TONE_css[tgt.tone].split(",").map(Number);
|
||||||
targetR = tr;
|
const targetIntensity = 0.15 + tgt.intensity * 0.85;
|
||||||
targetG = tg;
|
|
||||||
targetB = tb;
|
|
||||||
targetIntensity = 0.15 + tgt.intensity * 0.85;
|
|
||||||
|
|
||||||
r = lerp(r, targetR, 0.03);
|
r = lerp(r, tr, 0.03);
|
||||||
g = lerp(g, targetG, 0.03);
|
g = lerp(g, tg, 0.03);
|
||||||
b = lerp(b, targetB, 0.03);
|
b = lerp(b, tb, 0.03);
|
||||||
intensity = lerp(intensity, targetIntensity, 0.03);
|
intensity = lerp(intensity, targetIntensity, 0.03);
|
||||||
|
|
||||||
const root = mount;
|
const root = mount;
|
||||||
root.style.setProperty("--ab-r", String(Math.round(r)));
|
const R = Math.round(r);
|
||||||
root.style.setProperty("--ab-g", String(Math.round(g)));
|
const G = Math.round(g);
|
||||||
root.style.setProperty("--ab-b", String(Math.round(b)));
|
const B = Math.round(b);
|
||||||
root.style.setProperty("--ab-alpha", intensity.toFixed(3));
|
const A = intensity.toFixed(3);
|
||||||
|
if (R !== lastR) {
|
||||||
|
root.style.setProperty("--ab-r", String(R));
|
||||||
|
lastR = R;
|
||||||
|
}
|
||||||
|
if (G !== lastG) {
|
||||||
|
root.style.setProperty("--ab-g", String(G));
|
||||||
|
lastG = G;
|
||||||
|
}
|
||||||
|
if (B !== lastB) {
|
||||||
|
root.style.setProperty("--ab-b", String(B));
|
||||||
|
lastB = B;
|
||||||
|
}
|
||||||
|
if (A !== lastAlpha) {
|
||||||
|
root.style.setProperty("--ab-alpha", A);
|
||||||
|
lastAlpha = A;
|
||||||
|
}
|
||||||
|
|
||||||
raf = requestAnimationFrame(frame);
|
raf = requestAnimationFrame(frame);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ export function Select({
|
|||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpen((o) => !o)}
|
onClick={() => setOpen((o) => !o)}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-label={selected?.label ?? placeholder}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center justify-between gap-2 rounded-[8px] border border-hairline bg-surface-2 text-left text-ink transition-colors hover:border-signal/40",
|
"flex w-full items-center justify-between gap-2 rounded-[8px] border border-hairline bg-surface-2 text-left text-ink transition-colors hover:border-signal/40",
|
||||||
"focus:outline-none focus:border-signal/60",
|
"focus:outline-none focus:border-signal/60",
|
||||||
@@ -123,6 +126,7 @@ export function Select({
|
|||||||
createPortal(
|
createPortal(
|
||||||
<div
|
<div
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
|
role="listbox"
|
||||||
className="glass fixed z-[9999] max-h-72 overflow-auto p-1.5"
|
className="glass fixed z-[9999] max-h-72 overflow-auto p-1.5"
|
||||||
style={{
|
style={{
|
||||||
top: pos.top,
|
top: pos.top,
|
||||||
@@ -138,6 +142,8 @@ export function Select({
|
|||||||
<button
|
<button
|
||||||
key={o.value}
|
key={o.value}
|
||||||
type="button"
|
type="button"
|
||||||
|
role="option"
|
||||||
|
aria-selected={o.value === value}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange(o.value);
|
onChange(o.value);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import { isActivePath, navItems } from "@/lib/navigation";
|
import { isActivePath, navItems } from "@/lib/navigation";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -16,11 +17,35 @@ function NavItem({
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
Icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
Icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||||||
}) {
|
}) {
|
||||||
|
const itemRef = useRef<HTMLAnchorElement>(null);
|
||||||
|
const [tipPos, setTipPos] = useState<{ top: number; left: number } | null>(null);
|
||||||
|
const [hovering, setHovering] = useState(false);
|
||||||
|
|
||||||
|
// Measure the item and position the tooltip via a Portal on document.body
|
||||||
|
// so it always paints above the main content (which has its own stacking
|
||||||
|
// contexts from backdrop-blur panels).
|
||||||
|
const showTooltip = () => {
|
||||||
|
const el = itemRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
setTipPos({ top: rect.top + rect.height / 2, left: rect.right + 10 });
|
||||||
|
setHovering(true);
|
||||||
|
};
|
||||||
|
const hideTooltip = () => {
|
||||||
|
setHovering(false);
|
||||||
|
setTipPos(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
|
ref={itemRef}
|
||||||
href={href}
|
href={href}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
aria-current={active ? "page" : undefined}
|
aria-current={active ? "page" : undefined}
|
||||||
|
onMouseEnter={showTooltip}
|
||||||
|
onMouseLeave={hideTooltip}
|
||||||
|
onFocus={showTooltip}
|
||||||
|
onBlur={hideTooltip}
|
||||||
className={cn(
|
className={cn(
|
||||||
"nav-dock-item group relative flex size-9 items-center justify-center rounded-[8px] transition-all duration-150",
|
"nav-dock-item group relative flex size-9 items-center justify-center rounded-[8px] transition-all duration-150",
|
||||||
active
|
active
|
||||||
@@ -32,9 +57,25 @@ function NavItem({
|
|||||||
{active && (
|
{active && (
|
||||||
<span className="absolute -left-[5px] h-3.5 w-[2px] rounded-full bg-signal shadow-[0_0_8px_var(--color-signal-glow)]" />
|
<span className="absolute -left-[5px] h-3.5 w-[2px] rounded-full bg-signal shadow-[0_0_8px_var(--color-signal-glow)]" />
|
||||||
)}
|
)}
|
||||||
<span className="pointer-events-none absolute left-full z-50 ml-2.5 hidden whitespace-nowrap rounded-md border border-hairline bg-surface px-2 py-0.5 font-sans text-[11px] font-medium tracking-tight text-ink opacity-0 shadow-lg backdrop-blur-md transition-all group-hover:translate-x-0.5 group-hover:opacity-100 md:block">
|
|
||||||
{label}
|
{/* Tooltip — portalled to body so it never hides behind content */}
|
||||||
</span>
|
{hovering &&
|
||||||
|
tipPos &&
|
||||||
|
typeof document !== "undefined" &&
|
||||||
|
createPortal(
|
||||||
|
<span
|
||||||
|
role="tooltip"
|
||||||
|
className="pointer-events-none fixed z-[9999] translate-y-[-50%] whitespace-nowrap rounded-md border border-hairline bg-surface px-2.5 py-1 font-sans text-[11px] font-medium tracking-tight text-ink shadow-xl backdrop-blur-md"
|
||||||
|
style={{
|
||||||
|
top: tipPos.top,
|
||||||
|
left: tipPos.left,
|
||||||
|
animation: "fade-up 0.12s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user