animation tweaks and mobile desktop fixes

This commit is contained in:
dvorakmortonxd 2026-09-23 11:50:25 +02:00
commit ee11b4f71b
4 changed files with 107 additions and 16 deletions

View file

@ -1,8 +1,21 @@
const USERNAME = "dvorakmortonxd";
export async function GET() { export async function GET() {
try { try {
const res = await fetch("https://git.dvorakmortonxd.dev/api/v1/users/dvorakmortonxd/activities/feeds?limit=20"); // The public user feed also contains activity from repositories the user
// watches (e.g. commits pushed by other people to an org repo), so pull a
// larger page and keep only actions the user actually performed.
const res = await fetch(
`https://git.dvorakmortonxd.dev/api/v1/users/${USERNAME}/activities/feeds?limit=50`
);
const data = await res.json(); const data = await res.json();
return Response.json(data); if (!Array.isArray(data)) {
return Response.json(data);
}
const mine = data.filter(
(act) => act?.act_user?.login === USERNAME
);
return Response.json(mine);
} catch { } catch {
return Response.json({ error: "failed to load" }, { status: 500 }); return Response.json({ error: "failed to load" }, { status: 500 });
} }

View file

@ -27,12 +27,11 @@ export default function ServiceButton({ url, checkUrl, className, icon, name, de
const statusColor = effectiveStatus === 'up' ? '#4ade80' : effectiveStatus === 'down' ? '#ef4444' : '#9ca3af'; const statusColor = effectiveStatus === 'up' ? '#4ade80' : effectiveStatus === 'down' ? '#ef4444' : '#9ca3af';
const statusGlow = effectiveStatus === 'up' ? 'rgba(74, 222, 128, 0.9)' : effectiveStatus === 'down' ? 'rgba(239, 68, 68, 0.9)' : 'transparent'; const statusGlow = effectiveStatus === 'up' ? 'rgba(74, 222, 128, 0.9)' : effectiveStatus === 'down' ? 'rgba(239, 68, 68, 0.9)' : 'transparent';
const statusLabel = effectiveStatus === 'up' ? 'Active' : effectiveStatus === 'down' ? 'Dead' : 'Checking...'; const statusLabel = effectiveStatus === 'up' ? 'Active' : effectiveStatus === 'down' ? 'Dead' : 'Checking...';
const isFriend = className === 'friend';
const content = ( const content = (
<> <>
<span className="service-icon-wrap"> <span className="service-icon-wrap">
{isFriend && <span className="service-status-dot" />} <span className="service-status-dot" />
<span className="service-icon">{icon}</span> <span className="service-icon">{icon}</span>
</span> </span>
<span className="service-text"> <span className="service-text">

View file

@ -53,6 +53,7 @@ export default function WindowsDesktop() {
]); ]);
const [activeId, setActiveId] = useState<number | null>(1); const [activeId, setActiveId] = useState<number | null>(1);
const [startOpen, setStartOpen] = useState(false); const [startOpen, setStartOpen] = useState(false);
const [startClosing, setStartClosing] = useState(false);
const [selectedIcon, setSelectedIcon] = useState<string | null>(null); const [selectedIcon, setSelectedIcon] = useState<string | null>(null);
const [closingIds, setClosingIds] = useState<number[]>([]); const [closingIds, setClosingIds] = useState<number[]>([]);
@ -78,7 +79,7 @@ export default function WindowsDesktop() {
}; };
const openApp = (appId: string) => { const openApp = (appId: string) => {
setStartOpen(false); if (startOpen) setStartClosing(true);
const existing = wins.find((w) => w.appId === appId); const existing = wins.find((w) => w.appId === appId);
if (existing) { if (existing) {
setWins((prev) => setWins((prev) =>
@ -117,6 +118,21 @@ export default function WindowsDesktop() {
setActiveId(id); setActiveId(id);
}; };
const closeStart = () => {
if (startOpen && !startClosing) setStartClosing(true);
};
const toggleStart = () => {
if (!startOpen) {
setStartClosing(false);
setStartOpen(true);
} else if (startClosing) {
setStartClosing(false);
} else {
setStartClosing(true);
}
};
// Animate the window out first, then drop it from state. // Animate the window out first, then drop it from state.
const closeWin = (id: number) => const closeWin = (id: number) =>
setClosingIds((prev) => (prev.includes(id) ? prev : [...prev, id])); setClosingIds((prev) => (prev.includes(id) ? prev : [...prev, id]));
@ -168,7 +184,7 @@ export default function WindowsDesktop() {
style={{ backgroundImage: wallpaper }} style={{ backgroundImage: wallpaper }}
onPointerDown={(e) => { onPointerDown={(e) => {
if (e.target === e.currentTarget) { if (e.target === e.currentTarget) {
setStartOpen(false); closeStart();
setSelectedIcon(null); setSelectedIcon(null);
} }
}} }}
@ -264,7 +280,17 @@ export default function WindowsDesktop() {
{/* Application launcher */} {/* Application launcher */}
{startOpen && ( {startOpen && (
<div className="kickoff" onPointerDown={(e) => e.stopPropagation()}> <div
className={"kickoff" + (startClosing ? " closing" : "")}
onPointerDown={(e) => e.stopPropagation()}
onAnimationEnd={(e) => {
if (e.target !== e.currentTarget) return;
if (startClosing) {
setStartOpen(false);
setStartClosing(false);
}
}}
>
<div className="kickoff-content"> <div className="kickoff-content">
<div className="kickoff-section-title">Applications</div> <div className="kickoff-section-title">Applications</div>
<div className="kickoff-list"> <div className="kickoff-list">
@ -333,7 +359,7 @@ export default function WindowsDesktop() {
<button <button
type="button" type="button"
className={"win-start" + (startOpen ? " open" : "")} className={"win-start" + (startOpen ? " open" : "")}
onClick={() => setStartOpen((v) => !v)} onClick={() => toggleStart()}
aria-label="Application Launcher" aria-label="Application Launcher"
title="Application Launcher" title="Application Launcher"
> >
@ -352,7 +378,8 @@ export default function WindowsDesktop() {
className={ className={
"win-tb-btn" + "win-tb-btn" +
(activeId === w.id && !w.minimized ? " active" : "") + (activeId === w.id && !w.minimized ? " active" : "") +
(w.minimized ? " minimized" : "") (w.minimized ? " minimized" : "") +
(closingIds.includes(w.id) ? " closing" : "")
} }
onClick={() => taskbarClick(w)} onClick={() => taskbarClick(w)}
title={app.title} title={app.title}

View file

@ -92,17 +92,13 @@ html[data-device="phone"] .skeuo-mobile-root { display: block; }
color: #ffffff; color: #ffffff;
cursor: default; cursor: default;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.95); text-shadow: 0 1px 3px rgba(0, 0, 0, 0.95);
transition: transform 0.12s ease, background-color 0.12s ease, transition: background-color 0.12s ease, border-color 0.12s ease;
border-color 0.12s ease;
} }
.win-icon:hover, .win-icon:hover,
.win-icon.selected { .win-icon.selected {
background: rgba(61, 174, 233, 0.16); background: rgba(61, 174, 233, 0.16);
border-color: rgba(61, 174, 233, 0.4); border-color: rgba(61, 174, 233, 0.4);
} }
.win-icon:active {
transform: scale(0.9);
}
.win-icon-glyph { .win-icon-glyph {
width: 48px; width: 48px;
height: 48px; height: 48px;
@ -128,8 +124,11 @@ a.win-icon {
right: 10px; right: 10px;
left: auto; left: auto;
align-items: flex-end; align-items: flex-end;
flex-wrap: nowrap; /* Wrap into extra columns on short screens so the service shortcuts never
max-height: none; collide with the friends stack anchored to the bottom-right. */
flex-wrap: wrap;
align-content: flex-start;
max-height: calc(100% - 350px);
} }
.win-icons-br { .win-icons-br {
top: auto; top: auto;
@ -396,6 +395,17 @@ a.win-icon {
.app-surface .service-button::before { .app-surface .service-button::before {
content: none; content: none;
} }
/* Emoji glyphs get a square drop-shadow, so the status glow is drawn with the
status dot behind the icon instead of a filter on the glyph. */
.app-surface .service-button:not(.friend) .service-icon {
filter: none;
}
.app-surface .service-button:not(.friend) .service-status-dot {
width: 34px;
height: 34px;
background: radial-gradient(circle, var(--status-glow) 0%, rgba(0, 0, 0, 0) 70%);
filter: blur(1px);
}
.app-surface .service-button:hover { .app-surface .service-button:hover {
background: linear-gradient(to bottom, #f2f9ff, #cfe4f8); background: linear-gradient(to bottom, #f2f9ff, #cfe4f8);
border-color: #7db9e8; border-color: #7db9e8;
@ -912,6 +922,31 @@ a.win-icon {
background: transparent; background: transparent;
color: #eff0f1; color: #eff0f1;
cursor: pointer; cursor: pointer;
animation: tbBtnIn 0.16s ease-out;
}
.win-tb-btn.closing {
animation: tbBtnOut 0.12s ease-in forwards;
pointer-events: none;
}
@keyframes tbBtnIn {
from {
opacity: 0;
transform: translateY(6px) scale(0.92);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes tbBtnOut {
from {
opacity: 1;
transform: translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateY(6px) scale(0.92);
}
} }
.win-tb-btn::after { .win-tb-btn::after {
content: ""; content: "";
@ -1031,6 +1066,20 @@ a.win-icon {
overflow: auto; overflow: auto;
padding: 10px 12px; padding: 10px 12px;
} }
.kickoff.closing {
animation: kickoffOut 0.16s ease-in forwards;
pointer-events: none;
}
@keyframes kickoffOut {
from {
opacity: 1;
transform: translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateY(10px) scale(0.98);
}
}
.kickoff-section-title { .kickoff-section-title {
margin: 4px 2px 8px; margin: 4px 2px 8px;
font-size: 0.7rem; font-size: 0.7rem;
@ -1098,6 +1147,9 @@ a.win-icon {
color: #ffffff; color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", Roboto, font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", Roboto,
"Helvetica Neue", Arial, sans-serif; "Helvetica Neue", Arial, sans-serif;
-webkit-user-select: none;
user-select: none;
-webkit-touch-callout: none;
} }
.mobile-home::before { .mobile-home::before {
content: ""; content: "";