Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a54c34c6b3 | ||
|
|
7375e2ed52 |
No files matched your search
@@ -58,7 +58,7 @@ export function DashboardView({
|
||||
error,
|
||||
mutate: mutateStats,
|
||||
} = useStats(initialStats);
|
||||
const { data: activity } = useActivity(14, initialActivity as never);
|
||||
const { data: activity } = useActivity(14, initialActivity);
|
||||
const { data: reactions } = useTopReactions();
|
||||
const ambient = useAmbient();
|
||||
|
||||
|
||||
@@ -35,15 +35,18 @@ export function AmbientCanvas({
|
||||
let raf = 0;
|
||||
let last = performance.now();
|
||||
|
||||
// Lerp state
|
||||
// Lerp state (start values match the inline style defaults on the element).
|
||||
let r = 45,
|
||||
g = 212,
|
||||
b = 191;
|
||||
let targetR = 45,
|
||||
targetG = 212,
|
||||
targetB = 191;
|
||||
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;
|
||||
|
||||
@@ -53,21 +56,34 @@ export function AmbientCanvas({
|
||||
|
||||
const tgt = targetRef.current;
|
||||
const [tr, tg, tb] = TONE_css[tgt.tone].split(",").map(Number);
|
||||
targetR = tr;
|
||||
targetG = tg;
|
||||
targetB = tb;
|
||||
targetIntensity = 0.15 + tgt.intensity * 0.85;
|
||||
const targetIntensity = 0.15 + tgt.intensity * 0.85;
|
||||
|
||||
r = lerp(r, targetR, 0.03);
|
||||
g = lerp(g, targetG, 0.03);
|
||||
b = lerp(b, targetB, 0.03);
|
||||
r = lerp(r, tr, 0.03);
|
||||
g = lerp(g, tg, 0.03);
|
||||
b = lerp(b, tb, 0.03);
|
||||
intensity = lerp(intensity, targetIntensity, 0.03);
|
||||
|
||||
const root = mount;
|
||||
root.style.setProperty("--ab-r", String(Math.round(r)));
|
||||
root.style.setProperty("--ab-g", String(Math.round(g)));
|
||||
root.style.setProperty("--ab-b", String(Math.round(b)));
|
||||
root.style.setProperty("--ab-alpha", intensity.toFixed(3));
|
||||
const R = Math.round(r);
|
||||
const G = Math.round(g);
|
||||
const B = Math.round(b);
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -101,6 +101,9 @@ export function Select({
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
aria-label={selected?.label ?? placeholder}
|
||||
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",
|
||||
"focus:outline-none focus:border-signal/60",
|
||||
@@ -123,6 +126,7 @@ export function Select({
|
||||
createPortal(
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
role="listbox"
|
||||
className="glass fixed z-[9999] max-h-72 overflow-auto p-1.5"
|
||||
style={{
|
||||
top: pos.top,
|
||||
@@ -138,6 +142,8 @@ export function Select({
|
||||
<button
|
||||
key={o.value}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={o.value === value}
|
||||
onClick={() => {
|
||||
onChange(o.value);
|
||||
setOpen(false);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
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 { cn } from "@/lib/utils";
|
||||
|
||||
@@ -16,11 +17,35 @@ function NavItem({
|
||||
active: boolean;
|
||||
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 (
|
||||
<a
|
||||
ref={itemRef}
|
||||
href={href}
|
||||
aria-label={label}
|
||||
aria-current={active ? "page" : undefined}
|
||||
onMouseEnter={showTooltip}
|
||||
onMouseLeave={hideTooltip}
|
||||
onFocus={showTooltip}
|
||||
onBlur={hideTooltip}
|
||||
className={cn(
|
||||
"nav-dock-item group relative flex size-9 items-center justify-center rounded-[8px] transition-all duration-150",
|
||||
active
|
||||
@@ -32,9 +57,25 @@ function NavItem({
|
||||
{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="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}
|
||||
</span>
|
||||
|
||||
{/* Tooltip — portalled to body so it never hides behind content */}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user