Compare commits

...

5 commits

Author SHA1 Message Date
thislight
66366e6486
MediaViewer: apply insets padding
All checks were successful
/ depoly (push) Successful in 57s
2024-08-05 16:44:12 +08:00
thislight
94088768ba
BottomSheet: position below the insets top 2024-08-05 16:28:50 +08:00
thislight
4b17c426ab
added recover page on unexpected error 2024-08-05 16:24:34 +08:00
thislight
93b4cd065a
format code using prettier 2024-08-05 15:33:00 +08:00
thislight
f06a7a6da1
timelines: workaround to a bug moves the panels 2024-08-05 15:11:22 +08:00
31 changed files with 380 additions and 264 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -40,7 +40,8 @@
"masto": "^6.8.0",
"nanostores": "^0.9.5",
"solid-js": "^1.8.18",
"solid-styled": "^0.11.1"
"solid-styled": "^0.11.1",
"stacktrace-js": "^2.0.2"
},
"packageManager": "bun@1.1.21"
}

View file

@ -53,11 +53,13 @@ const App: Component = () => {
);
});
const UnexpectedError = lazy(() => import("./UnexpectedError.js"))
return (
<ErrorBoundary
fallback={(err, reset) => {
console.error(err);
return <></>;
return <UnexpectedError error={err} />;
}}
>
<ThemeProvider theme={theme()}>

40
src/UnexpectedError.tsx Normal file
View file

@ -0,0 +1,40 @@
import { Button } from '@suid/material';
import {Component, createResource} from 'solid-js'
import { css } from 'solid-styled';
const UnexpectedError: Component<{error?: any}> = (props) => {
const [errorMsg] = createResource(() => props.error, async (err) => {
if (err instanceof Error) {
const mod = await import('stacktrace-js')
const stacktrace = await mod.fromError(err)
const strackMsg = stacktrace.map(entry => `${entry.functionName ?? "<unknown>"}@${entry.fileName}:(${entry.lineNumber}:${entry.columnNumber})`).join('\n')
return `${err.name}: ${err.message}\n${strackMsg}`
}
return err.toString()
})
css`
main {
padding: calc(var(--safe-area-inset-top) + 20px) calc(var(--safe-area-inset-right) + 20px) calc(var(--safe-area-inset-bottom) + 20px) calc(var(--safe-area-inset-left) + 20px);
}
`
return <main>
<h1>Oh, it is our fault.</h1>
<p>There is an unexpected error in our app, and it's not your fault.</p>
<p>You can reload to see if this guy is gone. If you meet this guy repeatly, please report to us.</p>
<div>
<Button onClick={() => window.location.reload()}>Reload</Button>
</div>
<details>
<summary>{errorMsg.loading ? 'Generating ' : " "}Technical Infomation (Bring to us if you report the problem)</summary>
<pre>
{errorMsg()}
</pre>
</details>
</main>
}
export default UnexpectedError;

View file

@ -44,12 +44,12 @@ const MastodonOAuth2Callback: Component = () => {
setDocumentTitle(`Back from ${ins.title}...`);
setSiteTitle(ins.title);
const srcset = []
const srcset = [];
if (ins.thumbnail.versions["@1x"]) {
srcset.push(`${ins.thumbnail.versions["@1x"]} 1x`)
srcset.push(`${ins.thumbnail.versions["@1x"]} 1x`);
}
if (ins.thumbnail.versions["@2x"]) {
srcset.push(`${ins.thumbnail.versions["@2x"]} 2x`)
srcset.push(`${ins.thumbnail.versions["@2x"]} 2x`);
}
setSiteImg({
@ -66,8 +66,8 @@ const MastodonOAuth2Callback: Component = () => {
onGoingOAuth2Process,
params.code,
);
$settings.setKey('onGoingOAuth2Process', undefined)
navigate('/', {replace: true})
$settings.setKey("onGoingOAuth2Process", undefined);
navigate("/", { replace: true });
return;
}
@ -95,18 +95,27 @@ const MastodonOAuth2Callback: Component = () => {
<div class={cards.layoutCentered}>
<div class={cards.card} aria-busy="true" aria-describedby={progressId}>
<LinearProgress
class={[cards.cardNoPad, cards.cardGutSkip].join(' ')}
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
id={progressId}
aria-labelledby={titleId}
/>
<Show when={siteImg()} fallback={<i aria-busy="true" aria-label="Preparing image..." style={{"height": "235px", display: "block"}}></i>}>
<Show
when={siteImg()}
fallback={
<i
aria-busy="true"
aria-label="Preparing image..."
style={{ height: "235px", display: "block" }}
></i>
}
>
<Img
src={siteImg()?.src}
srcset={siteImg()?.srcset}
blurhash={siteImg()?.blurhash}
class={[cards.cardNoPad, cards.cardGutSkip].join(' ')}
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
alt={`Banner image for ${siteTitle()}`}
style={{"height": "235px", "display": "block"}}
style={{ height: "235px", display: "block" }}
/>
</Show>
@ -114,7 +123,8 @@ const MastodonOAuth2Callback: Component = () => {
Contracting {siteTitle}...
</Title>
<p>
If this page stays too long, you can close this page and sign in again.
If this page stays too long, you can close this page and sign in
again.
</p>
</div>
</div>

View file

@ -69,8 +69,8 @@ const SignIn: Component = () => {
});
onMount(() => {
$settings.setKey('onGoingOAuth2Process', undefined)
})
$settings.setKey("onGoingOAuth2Process", undefined);
});
const onStartOAuth2 = async (e: Event) => {
e.preventDefault();
@ -107,7 +107,7 @@ const SignIn: Component = () => {
for (const [k, v] of Object.entries(args)) {
searches.set(k, v);
}
$settings.setKey("onGoingOAuth2Process", url)
$settings.setKey("onGoingOAuth2Process", url);
window.location.href = authStart.toString();
} catch (e) {
setServerUrlHelperText(

View file

@ -95,9 +95,13 @@ export const updateAcctInf = action(
},
);
export const signOut = action($accounts, "signOut", ($store, predicate: (acct: Account) => boolean) => {
$store.set($store.get().filter(a => !predicate(a)));
});
export const signOut = action(
$accounts,
"signOut",
($store, predicate: (acct: Account) => boolean) => {
$store.set($store.get().filter((a) => !predicate(a)));
},
);
export type RegisteredApp = {
site: string;

View file

@ -1,5 +1,5 @@
import {render} from 'solid-js/web'
import App from './App.js'
import "./material/theme.css"
import { render } from "solid-js/web";
import App from "./App.js";
import "./material/theme.css";
render(() => <App />, document.getElementById("root")!)
render(() => <App />, document.getElementById("root")!);

View file

@ -12,9 +12,7 @@ type Timeline = {
}): mastodon.Paginator<mastodon.v1.Status[], unknown>;
};
export function useTimeline(
timeline: Accessor<Timeline>,
) {
export function useTimeline(timeline: Accessor<Timeline>) {
let minId: string | undefined;
let maxId: string | undefined;
let otl: Timeline | undefined;

View file

@ -1,5 +1,5 @@
.bottomSheet {
composes: surface from 'material.module.css';
composes: surface from "material.module.css";
border: none;
position: absolute;
left: 50%;
@ -22,7 +22,7 @@
@media (max-width: 560px) {
& {
left: 0;
top: 0;
top: var(--safe-area-inset-top, 0);
transform: none;
bottom: 0;
height: 100vh;

View file

@ -1,5 +1,5 @@
import { createEffect, type ParentComponent } from "solid-js";
import styles from './BottomSheet.module.css'
import styles from "./BottomSheet.module.css";
export type BottomSheetProps = {
open?: boolean;
@ -20,7 +20,11 @@ const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
}
});
return <dialog class={styles.bottomSheet} ref={element!}>{props.children}</dialog>;
return (
<dialog class={styles.bottomSheet} ref={element!}>
{props.children}
</dialog>
);
};
export default BottomSheet;

View file

@ -9,12 +9,12 @@ import materialStyles from "./material.module.css";
const Button: Component<JSX.ButtonHTMLAttributes<HTMLButtonElement>> = (
props,
) => {
const [managed, passthough] = splitProps(props, ["class", 'type']);
const [managed, passthough] = splitProps(props, ["class", "type"]);
const classes = () =>
managed.class
? [materialStyles.button, managed.class].join(" ")
: materialStyles.button;
const type = () => managed.type ?? 'button'
const type = () => managed.type ?? "button";
return <button type={type()} class={classes()} {...passthough}></button>;
};

View file

@ -1,5 +1,5 @@
.card {
composes: surface from 'material.module.css';
composes: surface from "material.module.css";
border-radius: 2px;
box-shadow: var(--tutu-shadow-e2);
transition: var(--tutu-transition-shadow);
@ -28,7 +28,6 @@
margin-bottom: var(--card-gut, 20px);
}
}
}
.layoutCentered {

View file

@ -1,5 +1,5 @@
.textfield {
composes: touchTarget from 'material.module.css';
composes: touchTarget from "material.module.css";
--border-color: var(--tutu-color-inactive-on-surface);
--active-border-color: var(--tutu-color-primary);
@ -11,7 +11,8 @@
width: 100%;
}
&.error, &:has(>input[aria-invalid="true"]) {
&.error,
&:has(> input[aria-invalid="true"]) {
&:not(:focus-within) {
--border-color: var(--tutu-color-error-on-surface);
--label-color: var(--tutu-color-error-on-surface);
@ -23,7 +24,6 @@
}
}
position: relative;
& > label {
@ -31,7 +31,10 @@
left: 0;
bottom: calc(10px + var(--bottom-height, 0px));
color: var(--label-color);
transition: bottom .2s ease-in-out, font-size .2s ease-in-out, color .2s ease-in-out;
transition:
bottom 0.2s ease-in-out,
font-size 0.2s ease-in-out,
color 0.2s ease-in-out;
cursor: text;
font-size: 0.8125rem;
}
@ -40,13 +43,14 @@
bottom: calc(100% - 0.8125rem);
}
&:focus-within>label, &.float-label>label {
&:focus-within > label,
&.float-label > label {
bottom: calc(100% - 0.8125rem);
color: var(--active-label-color);
}
&>input[type='text'],
&>input[type='password'] {
& > input[type="text"],
& > input[type="password"] {
border: none;
outline: none;
border-bottom: 1px solid var(--border-color);
@ -54,7 +58,7 @@
padding-top: 16px;
padding-bottom: 8px;
margin-bottom: 1px;
transition: border-color .2s ease-in-out;
transition: border-color 0.2s ease-in-out;
&:focus {
border-bottom: 2px solid var(--active-border-color);

View file

@ -10,7 +10,7 @@
}
.button {
composes: buttonText from './typography.module.css';
composes: buttonText from "./typography.module.css";
composes: touchTarget;
border: none;
@ -18,7 +18,9 @@
color: var(--tutu-color-primary);
font-family: inherit;
&:focus,&:hover,&:focus-visible {
&:focus,
&:hover,
&:focus-visible {
background-color: var(--tutu-color-surface-dd);
}
@ -31,15 +33,19 @@
color: var(--tutu-color-on-primary);
}
&:disabled, &[aria-disabled]:not([aria-disabled="false"]) {
&:disabled,
&[aria-disabled]:not([aria-disabled="false"]) {
color: #9e9e9e;
&:focus,&:hover,&:focus-visible {
&:focus,
&:hover,
&:focus-visible {
background-color: transparent;
}
}
.toolbar &, .appbar & {
.toolbar &,
.appbar & {
height: 100%;
margin-block: 0;
padding-block: 0;
@ -49,7 +55,9 @@
.appbar & {
color: var(--tutu-color-on-primary);
&:focus,&:hover,&:focus-visible {
&:focus,
&:hover,
&:focus-visible {
background-color: var(--tutu-color-primary-ll);
}
@ -62,4 +70,3 @@
color: var(--tutu-color-on-surface);
}
}

View file

@ -3,14 +3,15 @@ import { deepPurple, amber } from "@suid/material/colors";
import { Accessor } from "solid-js";
export function useRootTheme(): Accessor<Theme> {
return () => createTheme({
return () =>
createTheme({
palette: {
primary: {
main: deepPurple[500]
main: deepPurple[500],
},
secondary: {
main: amber.A200
}
}
})
main: amber.A200,
},
},
});
}

View file

@ -1,5 +1,6 @@
:root,
[lang^="en"], [lang="en"] {
[lang^="en"],
[lang="en"] {
--md-typography-type: "regular";
--title-size: 1.25rem;
--title-weight: 500;
@ -19,9 +20,12 @@
}
}
[lang^="zh"], [lang="zh"],
[lang^="kr"], [lang="kr"],
[lang^="ja"], [lang="ja"] {
[lang^="zh"],
[lang="zh"],
[lang^="kr"],
[lang="kr"],
[lang^="ja"],
[lang="ja"] {
--md-typography-type: "dense";
--title-size: 1.4375rem;
--subheading-size: 1.1875rem;
@ -95,7 +99,6 @@
--tutu-anim-curve-sharp: cubic-bezier(0.4, 0, 0.6, 1);
@media (max-width: 300px) {
/* XS screen, like wearables */
& {
--tutu-transition-shadow: box-shadow 157.5ms var(--tutu-anim-curve-std);
@ -103,7 +106,6 @@
}
@media (max-width: 600px) {
/* Mobile */
& {
--tutu-transition-shadow: box-shadow 225ms var(--tutu-anim-curve-std);
@ -111,7 +113,6 @@
}
@media (max-width: 1200px) {
/* Tablet */
& {
--tutu-transition-shadow: box-shadow 292.5ms var(--tutu-anim-curve-std);
@ -125,7 +126,13 @@
}
* {
font-family: Roboto, "Noto Sans", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
font-family:
Roboto,
"Noto Sans",
system-ui,
-apple-system,
BlinkMacSystemFont,
sans-serif;
box-sizing: border-box;
margin: 0;
}

View file

@ -3,7 +3,7 @@ import { Dynamic } from "solid-js/web";
import typography from "./typography.module.css";
import { mergeClass } from "../utils";
type AnyElement = keyof JSX.IntrinsicElements | ParentComponent<any>
type AnyElement = keyof JSX.IntrinsicElements | ParentComponent<any>;
type PropsOf<E extends AnyElement> =
E extends ParentComponent<infer Props>
@ -12,9 +12,7 @@ type PropsOf<E extends AnyElement> =
? JSX.IntrinsicElements[E]
: JSX.HTMLAttributes<HTMLElement>;
export type TypographyProps<
E extends AnyElement,
> = {
export type TypographyProps<E extends AnyElement> = {
ref?: Ref<E>;
component?: E;
class?: string;
@ -33,7 +31,9 @@ type TypographyKind =
| "caption"
| "buttonText";
export function Typography<T extends AnyElement>(props: {typography: TypographyKind } & TypographyProps<T>) {
export function Typography<T extends AnyElement>(
props: { typography: TypographyKind } & TypographyProps<T>,
) {
const [managed, passthough] = splitProps(props, [
"ref",
"component",
@ -50,38 +50,38 @@ export function Typography<T extends AnyElement>(props: {typography: TypographyK
{...passthough}
></Dynamic>
);
};
}
export function Display4<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"display4"} {...props}></Typography>
return <Typography typography={"display4"} {...props}></Typography>;
}
export function Display3<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"display3"} {...props}></Typography>
return <Typography typography={"display3"} {...props}></Typography>;
}
export function Display2<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"display2"} {...props}></Typography>
return <Typography typography={"display2"} {...props}></Typography>;
}
export function Display1<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"display1"} {...props}></Typography>
return <Typography typography={"display1"} {...props}></Typography>;
}
export function Headline<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"headline"} {...props}></Typography>
return <Typography typography={"headline"} {...props}></Typography>;
}
export function Title<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"title"} {...props}></Typography>
return <Typography typography={"title"} {...props}></Typography>;
}
export function Subheading<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"subheading"} {...props}></Typography>
return <Typography typography={"subheading"} {...props}></Typography>;
}
export function Body1<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"body1"} {...props}></Typography>
return <Typography typography={"body1"} {...props}></Typography>;
}
export function Body2<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"body2"} {...props}></Typography>
return <Typography typography={"body2"} {...props}></Typography>;
}
export function Caption<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"caption"} {...props}></Typography>
return <Typography typography={"caption"} {...props}></Typography>;
}
export function ButtonText<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"buttonText"} {...props}></Typography>
return <Typography typography={"buttonText"} {...props}></Typography>;
}

View file

@ -1,11 +1,13 @@
import { createContext, useContext, type Accessor } from "solid-js";
export type HeroSource = {[key: string | symbol | number]: HTMLElement | undefined}
export type HeroSource = {
[key: string | symbol | number]: HTMLElement | undefined;
};
const HeroSourceContext = createContext<Accessor<HeroSource>>(() => ({}))
const HeroSourceContext = createContext<Accessor<HeroSource>>(() => ({}));
export const HeroSourceProvider = HeroSourceContext.Provider
export const HeroSourceProvider = HeroSourceContext.Provider;
export function useHeroSource() {
return useContext(HeroSourceContext)
return useContext(HeroSourceContext);
}

View file

@ -1,11 +1,15 @@
import { persistentMap } from "@nanostores/persistent";
type Settings = {
onGoingOAuth2Process?: string
prefetchTootsDisabled?: boolean
}
onGoingOAuth2Process?: string;
prefetchTootsDisabled?: boolean;
};
export const $settings = persistentMap<Settings>("settings::", {}, {
export const $settings = persistentMap<Settings>(
"settings::",
{},
{
encode: JSON.stringify,
decode: JSON.parse
})
decode: JSON.parse,
},
);

View file

@ -23,11 +23,9 @@ const CompactToot: Component<CompactTootProps> = (props) => {
>
<Img
src={toot().account.avatar}
class={[
tootStyle.tootAvatar,
].join(" ")}
class={[tootStyle.tootAvatar].join(" ")}
/>
<div class={[tootStyle.compactAuthorGroup].join(' ')}>
<div class={[tootStyle.compactAuthorGroup].join(" ")}>
<Body2
ref={(e: { innerHTML: string }) => {
appliedCustomEmoji(
@ -48,7 +46,7 @@ const CompactToot: Component<CompactTootProps> = (props) => {
ref={(e: { innerHTML: string }) => {
appliedCustomEmoji(e, toot().content, toot().emojis);
}}
class={[tootStyle.compactTootContent].join(' ')}
class={[tootStyle.compactTootContent].join(" ")}
></div>
</section>
);

View file

@ -155,20 +155,20 @@ const Home: ParentComponent = (props) => {
useDocumentTitle("Timelines");
const now = createTimeSource();
const settings$ = useStore($settings)
const settings$ = useStore($settings);
const sessions = useSessions();
const client = () => sessions()[0].client;
const [profile] = useAcctProfile(client);
const [panelOffset, setPanelOffset] = createSignal(0);
const prefetching = () => !settings$().prefetchTootsDisabled
const prefetching = () => !settings$().prefetchTootsDisabled;
const [currentFocusOn, setCurrentFocusOn] = createSignal<HTMLElement[]>([]);
const [focusRange, setFocusRange] = createSignal([0, 0] as readonly [
number,
number,
]);
const child = children(() => props.children)
const child = children(() => props.children);
let scrollEventLockReleased = true;
@ -229,7 +229,7 @@ const Home: ParentComponent = (props) => {
const onTabClick = (idx: number) => {
const items = panelList.querySelectorAll(".tab-panel");
if (items.length > idx) {
items.item(idx).scrollIntoView({ block: "nearest", behavior: "smooth" });
items.item(idx).scrollIntoView({ block: "start", behavior: "smooth" });
}
};
@ -269,7 +269,11 @@ const Home: ParentComponent = (props) => {
<Scaffold
topbar={
<AppBar position="static">
<Toolbar variant="dense" class="responsive" sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}>
<Toolbar
variant="dense"
class="responsive"
sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }}
>
<Tabs onFocusChanged={setCurrentFocusOn} offset={panelOffset()}>
<Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}>
Home
@ -282,7 +286,14 @@ const Home: ParentComponent = (props) => {
</Tab>
</Tabs>
<ProfileMenuButton profile={profile()}>
<MenuItem onClick={(e) => $settings.setKey("prefetchTootsDisabled", !$settings.get().prefetchTootsDisabled)}>
<MenuItem
onClick={(e) =>
$settings.setKey(
"prefetchTootsDisabled",
!$settings.get().prefetchTootsDisabled,
)
}
>
<ListItemText>Prefetch Toots</ListItemText>
<ListItemSecondaryAction>
<Switch checked={prefetching()}></Switch>

View file

@ -11,7 +11,7 @@ const MediaAttachmentGrid: Component<{
}> = (props) => {
let rootRef: HTMLElement;
const [viewerIndex, setViewerIndex] = createSignal<number>();
const viewerOpened = () => typeof viewerIndex() !== "undefined"
const viewerOpened = () => typeof viewerIndex() !== "undefined";
const gridTemplateColumns = () => {
const l = props.attachments.length;
if (l < 2) {

View file

@ -36,7 +36,7 @@ function within(n: number, target: number, range: number) {
}
function clamp(input: number, min: number, max: number) {
return Math.min(Math.max(input, min), max)
return Math.min(Math.max(input, min), max);
}
const MediaViewer: ParentComponent<MediaViewerProps> = (props) => {
@ -128,6 +128,13 @@ const MediaViewer: ParentComponent<MediaViewerProps> = (props) => {
left: 0;
z-index: 1;
cursor: ${dragging() ? "grabbing" : "grab"};
padding-left: var(--safe-area-inset-left, 0);
padding-right: var(--safe-area-inset-right, 0);
padding-bottom: var(--safe-area-inset-bottom, 0);
:global(> .MuiToolbar-root) {
padding-top: var(--safe-area-inset-top, 0);
}
}
.left-dock {
@ -207,7 +214,13 @@ const MediaViewer: ParentComponent<MediaViewerProps> = (props) => {
move: number,
idx: number,
) => {
const { ref, top: otop, left: oleft, scale: oscale, osize: [owidth, oheight] } = state[idx];
const {
ref,
top: otop,
left: oleft,
scale: oscale,
osize: [owidth, oheight],
} = state[idx];
const [cx, cy] = center;
const iy = clamp(cy - otop, 0, oheight),
ix = clamp(cx - oleft, 0, owidth); // in image coordinate system

View file

@ -8,7 +8,13 @@ import {
Menu,
MenuItem,
} from "@suid/material";
import { Show, createSignal, createUniqueId, type ParentComponent } from "solid-js";
import {
ErrorBoundary,
Show,
createSignal,
createUniqueId,
type ParentComponent,
} from "solid-js";
import {
Settings as SettingsIcon,
Bookmark as BookmarkIcon,
@ -64,7 +70,7 @@ const ProfileMenuButton: ParentComponent<{
"aria-labelledby": buttonId,
sx: {
minWidth: "220px",
}
},
}}
anchorOrigin={{
vertical: "top",

View file

@ -211,7 +211,7 @@ const RegularToot: Component<TootCardProps> = (props) => {
classList={{
[tootStyle.toot]: true,
[tootStyle.expanded]: managed.evaluated,
[managed.class || ""]: true
[managed.class || ""]: true,
}}
ref={rootRef!}
lang={toot().language || managed.lang}

View file

@ -1,8 +1,7 @@
import type { Component } from "solid-js";
const TootBottomSheet: Component = (props) => {
return <></>
}
return <></>;
};
export default TootBottomSheet
export default TootBottomSheet;

View file

@ -38,7 +38,9 @@ const TootThread: Component<TootThreadProps> = (props) => {
css`
article {
transition: margin 90ms var(--tutu-anim-curve-sharp), var(--tutu-transition-shadow);
transition:
margin 90ms var(--tutu-anim-curve-sharp),
var(--tutu-transition-shadow);
user-select: none;
cursor: pointer;
}
@ -64,7 +66,10 @@ const TootThread: Component<TootThreadProps> = (props) => {
`;
return (
<article classList={{ "thread-line": !!inReplyTo(), "expanded": expanded() }} onClick={() => setExpanded((x) => !x)}>
<article
classList={{ "thread-line": !!inReplyTo(), expanded: expanded() }}
onClick={() => setExpanded((x) => !x)}
>
<Show when={inReplyTo()}>
<CompactToot
status={inReplyTo()!}

View file

@ -6,7 +6,8 @@
&.toot {
/* fix composition ordering: I think the css module processor should aware the overriding and behaves, but no */
transition: margin-block 125ms var(--tutu-anim-curve-std),
transition:
margin-block 125ms var(--tutu-anim-curve-std),
height 225ms var(--tutu-anim-curve-std),
var(--tutu-transition-shadow);
border-radius: 0;
@ -80,7 +81,7 @@
}
.tootContent {
composes: cardNoPad from '../material/cards.module.css';
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;
@ -150,7 +151,7 @@
}
.tootAttachmentGrp {
composes: cardNoPad from '../material/cards.module.css';
composes: cardNoPad from "../material/cards.module.css";
margin-top: 1em;
margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px);
margin-right: var(--card-pad, 0);
@ -168,7 +169,7 @@
}
.tootBottomActionGrp {
composes: cardGutSkip from '../material/cards.module.css';
composes: cardGutSkip from "../material/cards.module.css";
padding-block: calc((var(--card-gut) - 10px) / 2);
animation: 225ms var(--tutu-anim-curve-std) tootBottomExpanding;

View file

@ -1,26 +1,26 @@
import { createRenderEffect, createSignal, onCleanup } from "solid-js";
export function useDocumentTitle(newTitle?: string) {
const capturedTitle = document.title
const [title, setTitle] = createSignal(newTitle ?? capturedTitle)
const capturedTitle = document.title;
const [title, setTitle] = createSignal(newTitle ?? capturedTitle);
createRenderEffect(() => {
document.title = title()
})
document.title = title();
});
onCleanup(() => {
document.title = capturedTitle
})
document.title = capturedTitle;
});
return setTitle
return setTitle;
}
export function mergeClass(c1: string | undefined, c2: string | undefined) {
if (!c1) {
return c2
return c2;
}
if (!c2) {
return c1
return c1;
}
return [c1, c2].join(' ')
return [c1, c2].join(" ");
}