rename toot-components to toots

This commit is contained in:
thislight 2024-11-20 16:26:05 +08:00
parent 737d63f88a
commit 6313827b1e
No known key found for this signature in database
GPG key ID: FCFE5192241CCD4E
7 changed files with 4 additions and 4 deletions

View file

@ -0,0 +1,13 @@
.BoostIcon {
display: inline-flex;
border-radius: 2px;
background-color: green;
padding: 0.125em;
align-items: center;
> svg {
color: white;
font-size: 1em;
vertical-align: middle;
}
}

View file

@ -0,0 +1,22 @@
import {
splitProps,
type Component,
type JSX,
} from "solid-js";
import {
Repeat,
} from "@suid/icons-material";
import "./BoostIcon.css";
const BoostIcon: Component<JSX.HTMLElementTags["i"]> = (props) => {
const [managed, rest] = splitProps(props, ["class"]);
return (
<i class={["BoostIcon", managed.class].join(" ")} {...rest}>
<Repeat />
</i>
);
};
export default BoostIcon;

View file

@ -0,0 +1,71 @@
.PreviewCard {
display: block;
border: 1px solid #eeeeee;
background-color: var(--tutu-color-surface-d);
text-decoration: none;
border-radius: 4px;
overflow: hidden;
margin-top: 1em;
margin-bottom: 1.5em;
color: var(--tutu-color-secondary-text-on-surface);
transition: color 220ms var(--tutu-anim-curve-std), background-color 220ms var(--tutu-anim-curve-std);
padding-bottom: 8px;
overflow: hidden;
z-index: 1;
position: relative;
contain: layout style;
>img {
background-color: var(--tutu-color-surface);
max-width: 100%;
height: auto;
&.loaded {
background-color: #eeeeee;
}
}
&:hover,
&:focus-visible {
color: var(--tutu-color-on-surface);
>h1 {
text-decoration: underline;
}
}
>h1 {
color: var(--tutu-color-on-surface);
max-height: calc(4 * var(--title-line-height) * var(--title-size));
}
>p {
max-height: calc(8 * var(--body-line-height) * var(--body-size));
}
>h1,
>p {
margin-left: 16px;
margin-right: 16px;
overflow: hidden;
text-overflow: ellipsis;
}
&.compact {
display: grid;
grid-template-columns: minmax(10%, 30%) 1fr;
padding-left: 16px;
padding-right: 16px;
padding-top: 8px;
>img:first-child {
grid-row: 1 / 3;
object-fit: contain;
}
>h1,
>p {
margin-right: 0;
}
}
}

View file

@ -0,0 +1,107 @@
import Color from "colorjs.io";
import type { mastodon } from "masto";
import { createEffect, createMemo, Show } from "solid-js";
import { Title, Body1 } from "../../material/typography";
import { averageColorHex } from "../../platform/blurhash";
import "./PreviewCard.css";
function onResetImg(event: Event & { currentTarget: HTMLImageElement }) {
event.currentTarget.classList.remove("loaded");
}
function onImgLoaded(event: Event & { currentTarget: HTMLImageElement }) {
event.currentTarget.classList.add("loaded");
}
export function PreviewCard(props: {
src: mastodon.v1.PreviewCard;
alwaysCompact?: boolean;
}) {
let root: HTMLAnchorElement;
createEffect(() => {
if (props.alwaysCompact) {
root.classList.add("compact");
return;
}
if (!props.src.width) return;
const width = root.getBoundingClientRect().width;
if (width > props.src.width) {
root.classList.add("compact");
} else {
root.classList.remove("compact");
}
});
const imgAverageColor = createMemo(() => {
if (!props.src.image) return;
return new Color(averageColorHex(props.src.blurhash));
});
const prefersWhiteText = createMemo(() => {
const oc = imgAverageColor();
if (!oc) return;
const colorWhite = new Color("white");
return colorWhite.luminance / oc.luminance > 3.5;
});
const focusSurfaceColor = createMemo(() => {
const oc = imgAverageColor();
if (!oc) return;
if (prefersWhiteText()) {
return new Color(oc).darken(0.2);
} else {
return new Color(oc).lighten(0.2);
}
});
const textColorName = createMemo(() => {
const useWhiteText = prefersWhiteText();
if (typeof useWhiteText === "undefined") {
return;
}
return useWhiteText ? "white" : "black";
});
const secondaryTextColor = createMemo(() => {
const tcn = textColorName();
if (!tcn) return;
const tc = new Color(tcn);
tc.alpha = 0.75;
return tc;
});
return (
<a
ref={root!}
class={"PreviewCard"}
href={props.src.url}
target="_blank"
referrerPolicy="unsafe-url"
style={{
"--tutu-color-surface": imgAverageColor()?.toString(),
"--tutu-color-surface-d": focusSurfaceColor()?.toString(),
"--tutu-color-on-surface": textColorName(),
"--tutu-color-secondary-text-on-surface":
secondaryTextColor()?.toString(),
}}
>
<Show when={props.src.image}>
<img
onLoadStart={onResetImg}
onLoad={onImgLoaded}
crossOrigin="anonymous"
src={props.src.image!}
width={props.src.width || undefined}
height={props.src.height || undefined}
loading="lazy"
/>
</Show>
<Title component="h1">{props.src.title}</Title>
<Body1 component="p">{props.src.description}</Body1>
</a>
);
}
export default PreviewCard;

View file

@ -0,0 +1,33 @@
.TootContent {
composes: cardNoPad from "../material/cards.module.css";
margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px);
margin-right: var(--card-pad, 0);
line-height: 1.5;
> .content {
display: contents;
}
& * {
user-select: text;
}
& a {
color: var(--tutu-color-primary-d);
}
& a[target="_blank"] {
word-break: break-all;
>.invisible {
display: none;
}
>.ellipsis {
&::after {
display: inline;
content: "...";
}
}
}
}

View file

@ -0,0 +1,105 @@
import type { mastodon } from "masto";
import {
splitProps,
type Component,
type JSX,
createRenderEffect,
createMemo,
Show,
} from "solid-js";
import { resolveCustomEmoji } from "../../masto/toot.js";
import { makeAcctText, useDefaultSession } from "../../masto/clients.js";
import "./TootContent.css";
import { Button } from "@suid/material";
function preventDefault(event: Event) {
event.preventDefault();
}
export type TootContentProps = JSX.HTMLAttributes<HTMLDivElement> & {
source?: string;
emojis?: mastodon.v1.CustomEmoji[];
mentions: mastodon.v1.StatusMention[];
sensitive?: boolean;
spoilerText?: string;
reveal?: boolean;
onToggleReveal?: JSX.EventHandlerUnion<HTMLElement, Event>;
};
const TootContent: Component<TootContentProps> = (oprops) => {
const session = useDefaultSession();
const [props, rest] = splitProps(oprops, [
"source",
"emojis",
"mentions",
"class",
"sensitive",
"spoilerText",
"reveal",
"onToggleReveal",
]);
const clientFinder = createMemo(() =>
session() ? makeAcctText(session()!) : undefined,
);
const shouldRevealContent = () => {
return !props.sensitive || (props.sensitive && props.reveal);
};
return (
<div
ref={(ref) => {
createRenderEffect(() => {
const finder = clientFinder();
for (const mention of props.mentions) {
const elements = ref.querySelectorAll<HTMLAnchorElement>(
`a[href='${mention.url}']`,
);
for (const e of elements) {
e.onclick = preventDefault;
e.dataset.action = "acct";
e.dataset.client = finder;
e.dataset.acctId = mention.id.toString();
}
}
});
}}
class={`TootContent ${props.class || ""}`}
{...rest}
>
<Show when={props.sensitive}>
<div>
<span
ref={(ref) => {
createRenderEffect(() => {
ref.innerHTML = props.spoilerText
? props.emojis
? resolveCustomEmoji(props.spoilerText, props.emojis)
: props.spoilerText
: "";
});
}}
></span>
<Button onClick={props.onToggleReveal}>"Content Warning"</Button>
</div>
</Show>
<Show when={shouldRevealContent()}>
<div
class="content"
ref={(ref) =>
createRenderEffect(() => {
ref.innerHTML = props.source
? props.emojis
? resolveCustomEmoji(props.source, props.emojis)
: props.source
: "";
})
}
></div>
</Show>
</div>
);
};
export default TootContent;