feat: enhance Lanyard component with tooltip support and improve styling
Some checks failed
Code quality checks / biome (push) Has been cancelled

This commit is contained in:
Seth 2025-05-19 01:13:14 -04:00
parent cc451a8fef
commit 5d7220f5a8
6 changed files with 229 additions and 94 deletions

View file

@ -0,0 +1,110 @@
.card {
width: 100%;
}
.activityCard {
background-color: transparent;
border-radius: 8px;
padding: 16px;
width: 100% !important;
/* Set a max width */
}
.status {
font-size: 14px;
margin-bottom: 8px;
}
.content {
display: flex;
flex-wrap: wrap;
/* Allow wrapping for smaller screens */
}
.bigImage {
position: relative;
width: 120px;
/* Set fixed width for the big image */
height: 120px;
/* Set fixed height for the big image */
flex-shrink: 0;
/* Prevent shrinking */
}
.bigImage img {
width: 100%;
height: 100%;
border-radius: 8px;
object-fit: cover;
/* Ensures the image covers the area without distortion */
}
.smallImage {
position: absolute;
bottom: -8px;
right: -8px;
width: 40px;
/* Set fixed width for the small image */
height: 40px;
/* Set fixed height for the small image */
overflow: hidden;
border-radius: 50%;
}
.smallImage img {
width: 100%;
height: 100%;
object-fit: cover;
/* Ensures the image covers the area without distortion */
}
.textInfo {
margin-left: 16px;
display: flex;
flex-direction: column;
justify-content: center;
flex-grow: 1;
/* Allow text info to grow */
}
.appName {
font-size: 16px;
font-weight: bold;
}
.state,
.details {
font-size: 14px;
color: #b9bbbe;
}
/* Media Queries for Responsiveness */
@media (max-width: 480px) {
.bigImage {
width: 80px;
/* Adjust size for smaller screens */
height: 80px;
}
.smallImage {
width: 30px;
/* Adjust size for smaller screens */
height: 30px;
}
.textInfo {
margin-left: 8px;
/* Reduce margin for smaller screens */
}
.appName {
font-size: 14px;
/* Adjust font size */
}
.state,
.details {
font-size: 12px;
/* Adjust font size */
}
}

View file

@ -1,8 +1,11 @@
import { highlightElement } from "@speed-highlight/core";
import { createRef } from "tsx-dom";
import colors from "../../utilities/colors.module.css";
import socket from "../../utilities/socket";
import "mdui/components/tooltip.js";
import style from "./index.module.css"; // entirely gpt generated :sob:
const activityTypes: Record<number, string> = {
0: "Playing",
1: "Streaming",
@ -12,6 +15,22 @@ const activityTypes: Record<number, string> = {
5: "Competing in",
};
const getImageUrl = (activity: LanyardActivity, size: "large" | "small") => {
if (!activity.assets) return null;
const image = activity.assets[`${size}_image`];
if (!image) return null;
if (image.startsWith("mp:external")) {
return `https://wsrv.nl/?w=${size === "large" ? 120 : 40}&url=https://${image.split("/").slice(3).join("/")}`;
}
if (image.startsWith("mp:")) {
return `https://cdn.discordapp.com/app-assets/${activity.application_id}/${image.slice(3)}.webp`;
}
};
export default () => {
const container = createRef<HTMLDivElement>();
@ -32,33 +51,52 @@ export default () => {
}
if (container.current) {
container.current.textContent = JSON.stringify(
{
activities: [
...new Set(
lanyard.activities.map((act) => {
const type = activityTypes[act.type];
const parts = [
`${type}${act.name === "Custom Status" ? "" : ` ${act.name}`}`,
];
if (act.details && act.details !== act.name)
parts.push(act.details);
if (act.state && act.state !== act.name) parts.push(act.state);
return parts;
}),
),
],
},
null,
2,
);
highlightElement(container.current);
container.current.innerHTML = "";
for (const activity of lanyard.activities) {
if (activity.type === 4) {
continue;
}
const largeImage = getImageUrl(activity, "large");
const smallImage = getImageUrl(activity, "small");
container.current.innerHTML += (
<div>
{/* @ts-expect-error; variant is not in the types for some reason? */}
<mdui-card variant="filled" class={style.card}>
{" "}
<div class={style.activityCard}>
<div class={style.status}>{activityTypes[activity.type]}</div>
<div class={style.content}>
{largeImage && (
<div class={style.bigImage}>
{/* @ts-expect-error; placement is not in the types for some reason? */}
<mdui-tooltip content={activity.assets?.large_text} placement="top-right">
<img src={largeImage} alt="Large Activity" />
</mdui-tooltip>
{smallImage && (
<div class={style.smallImage}>
{/* @ts-expect-error; placement is not in the types for some reason? */}
<mdui-tooltip content={activity.assets?.small_text} placement="top-right">
<img src={smallImage} alt="Small Activity" />
</mdui-tooltip>
</div>
)}
</div>
)}
<div class={style.textInfo}>
<div class={style.appName}>{activity.name}</div>
<div class={style.state}>{activity.state}</div>
<div class={style.details}>{activity.details}</div>
</div>
</div>
</div>
</mdui-card>
<br />
</div>
).outerHTML;
}
}
});
return (
<code class="shj-lang-json" ref={container}>
{"{}"}
</code>
);
return <div ref={container} />;
};

View file

@ -1,26 +1,41 @@
import "mdui/components/card";
import "mdui/components/avatar";
import "mdui/components/segmented-button-group";
import "mdui/components/segmented-button";
import "mdui/components/tooltip.js";
import colors from "../../../utilities/colors.module.css";
import type { Tooltip } from "mdui/components/tooltip.js";
import { createRef } from "tsx-dom";
import socket from "../../../utilities/socket";
import Hyperate from "../../Hyperate";
import Lanyard from "../../Lanyard";
import styles from "./index.module.css";
export default () => {
const tooltip = createRef<Tooltip>();
socket.addEventListener("lanyard", (event: Event) => {
const lanyard = (event as CustomEvent<LanyardData>).detail;
const customStatus = lanyard.activities.find((act) => act.type === 4);
if (tooltip.current) {
if (customStatus?.state) {
tooltip.current.setAttribute("content", customStatus.state);
} else {
tooltip.current.removeAttribute("content");
}
}
});
return (
<div class={styles.container}>
<mdui-card
// @ts-expect-error; variant is not in the types for some reason?
variant="filled"
class={`${styles.card} ${styles.center}`}
>
<mdui-avatar
src="/public/Abyssinian/default.png"
class={`${styles.avatar} ${colors.pfp}`}
/>
<mdui-card class={`${styles.card} ${styles.center}`}>
<mdui-tooltip ref={tooltip}>
<mdui-avatar
src="/public/Abyssinian/default.png"
class={styles.avatar}
/>
</mdui-tooltip>
<p>
Seth, the <strong>dedicated</strong> backend developer, with many{" "}
<strong>passions</strong>.
@ -34,16 +49,6 @@ export default () => {
</p>
</mdui-card>
<br class={styles.lanyard} />
<mdui-card
// @ts-expect-error; variant is not in the types for some reason?
variant="filled"
class={`${styles.card} ${styles.lanyard}`}
>
<Lanyard />
</mdui-card>
<br class={styles.hyperate} />
<mdui-card
@ -53,6 +58,12 @@ export default () => {
>
<Hyperate />
</mdui-card>
<br class={styles.lanyard} />
<div class={`${styles.card} ${styles.lanyard}`}>
<Lanyard />
</div>
</div>
);
};

View file

@ -2,7 +2,7 @@ const effectTick = new Audio("https://no.ipv4.army/u/Effect_Tick.ogg");
effectTick.volume = 0.1;
const whitelistedTags = ["button", "icon", "item"];
const whitelistedTags = ["button", "icon", "item", "tooltip", "avatar"];
document.onclick = (event: MouseEvent) => {
const target = event.target as HTMLElement;
@ -12,8 +12,6 @@ document.onclick = (event: MouseEvent) => {
const tagName = target.tagName.toLowerCase();
const isWhitelisted = whitelistedTags.some((tag) => tagName.includes(tag));
console.log(tagName, isWhitelisted);
if (!isWhitelisted) return;
"vibrate" in navigator && navigator.vibrate(1);

View file

@ -197,43 +197,3 @@
--mdui-color-surface-container-highest: 56, 51, 54;
--mdui-color-surface-tint-color: 255, 171, 243;
}
.pfp {
--mdui-color-primary: 255, 183, 123;
--mdui-color-on-primary: 77, 39, 0;
--mdui-color-primary-container: 109, 58, 0;
--mdui-color-on-primary-container: 255, 220, 194;
--mdui-color-secondary: 227, 192, 166;
--mdui-color-on-secondary: 65, 44, 25;
--mdui-color-secondary-container: 90, 66, 46;
--mdui-color-on-secondary-container: 255, 220, 194;
--mdui-color-tertiary: 196, 203, 151;
--mdui-color-on-tertiary: 46, 51, 13;
--mdui-color-tertiary-container: 68, 74, 34;
--mdui-color-on-tertiary-container: 224, 231, 177;
--mdui-color-error: 255, 180, 171;
--mdui-color-on-error: 105, 0, 5;
--mdui-color-error-container: 147, 0, 10;
--mdui-color-on-error-container: 255, 180, 171;
--mdui-color-background: 32, 27, 23;
--mdui-color-on-background: 236, 224, 218;
--mdui-color-surface: 32, 27, 23;
--mdui-color-on-surface: 236, 224, 218;
--mdui-color-surface-variant: 81, 68, 59;
--mdui-color-on-surface-variant: 214, 195, 182;
--mdui-color-outline: 158, 142, 130;
--mdui-color-outline-variant: 81, 68, 59;
--mdui-color-shadow: 0, 0, 0;
--mdui-color-scrim: 0, 0, 0;
--mdui-color-inverse-surface: 236, 224, 218;
--mdui-color-inverse-on-surface: 53, 47, 43;
--mdui-color-inverse-primary: 143, 78, 0;
--mdui-color-surface-dim: 23, 18, 15;
--mdui-color-surface-bright: 62, 56, 51;
--mdui-color-surface-container-lowest: 18, 13, 10;
--mdui-color-surface-container-low: 32, 27, 23;
--mdui-color-surface-container: 36, 31, 27;
--mdui-color-surface-container-high: 47, 41, 37;
--mdui-color-surface-container-highest: 58, 52, 47;
--mdui-color-surface-tint-color: 255, 183, 123;
}

24
types/lanyard.d.ts vendored
View file

@ -1,9 +1,27 @@
type LanyardActivity = {
type: number;
id: string;
name: string;
details?: string;
type: number;
state?: string;
[key: string]: unknown;
session_id: string;
details?: string;
application_id: string;
timestamps?: {
start?: number;
end?: number;
};
assets?: {
large_image?: string;
large_text?: string;
small_image?: string;
small_text?: string;
};
created_at?: number;
buttons?: string[];
};
type LanyardData = {