rename toot-components to toots
This commit is contained in:
parent
737d63f88a
commit
6313827b1e
7 changed files with 4 additions and 4 deletions
13
src/timelines/toots/BoostIcon.css
Normal file
13
src/timelines/toots/BoostIcon.css
Normal 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;
|
||||
}
|
||||
}
|
22
src/timelines/toots/BoostIcon.tsx
Normal file
22
src/timelines/toots/BoostIcon.tsx
Normal 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;
|
71
src/timelines/toots/PreviewCard.css
Normal file
71
src/timelines/toots/PreviewCard.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
107
src/timelines/toots/PreviewCard.tsx
Normal file
107
src/timelines/toots/PreviewCard.tsx
Normal 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;
|
33
src/timelines/toots/TootContent.css
Normal file
33
src/timelines/toots/TootContent.css
Normal 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: "...";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
105
src/timelines/toots/TootContent.tsx
Normal file
105
src/timelines/toots/TootContent.tsx
Normal 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;
|
Loading…
Add table
Add a link
Reference in a new issue