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", "masto": "^6.8.0",
"nanostores": "^0.9.5", "nanostores": "^0.9.5",
"solid-js": "^1.8.18", "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" "packageManager": "bun@1.1.21"
} }

View file

@ -9,4 +9,4 @@
.custom-emoji { .custom-emoji {
width: 1.25em; width: 1.25em;
} }

View file

@ -53,11 +53,13 @@ const App: Component = () => {
); );
}); });
const UnexpectedError = lazy(() => import("./UnexpectedError.js"))
return ( return (
<ErrorBoundary <ErrorBoundary
fallback={(err, reset) => { fallback={(err, reset) => {
console.error(err); console.error(err);
return <></>; return <UnexpectedError error={err} />;
}} }}
> >
<ThemeProvider theme={theme()}> <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}...`); setDocumentTitle(`Back from ${ins.title}...`);
setSiteTitle(ins.title); setSiteTitle(ins.title);
const srcset = [] const srcset = [];
if (ins.thumbnail.versions["@1x"]) { if (ins.thumbnail.versions["@1x"]) {
srcset.push(`${ins.thumbnail.versions["@1x"]} 1x`) srcset.push(`${ins.thumbnail.versions["@1x"]} 1x`);
} }
if (ins.thumbnail.versions["@2x"]) { if (ins.thumbnail.versions["@2x"]) {
srcset.push(`${ins.thumbnail.versions["@2x"]} 2x`) srcset.push(`${ins.thumbnail.versions["@2x"]} 2x`);
} }
setSiteImg({ setSiteImg({
@ -66,8 +66,8 @@ const MastodonOAuth2Callback: Component = () => {
onGoingOAuth2Process, onGoingOAuth2Process,
params.code, params.code,
); );
$settings.setKey('onGoingOAuth2Process', undefined) $settings.setKey("onGoingOAuth2Process", undefined);
navigate('/', {replace: true}) navigate("/", { replace: true });
return; return;
} }
@ -95,18 +95,27 @@ const MastodonOAuth2Callback: Component = () => {
<div class={cards.layoutCentered}> <div class={cards.layoutCentered}>
<div class={cards.card} aria-busy="true" aria-describedby={progressId}> <div class={cards.card} aria-busy="true" aria-describedby={progressId}>
<LinearProgress <LinearProgress
class={[cards.cardNoPad, cards.cardGutSkip].join(' ')} class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
id={progressId} id={progressId}
aria-labelledby={titleId} 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 <Img
src={siteImg()?.src} src={siteImg()?.src}
srcset={siteImg()?.srcset} srcset={siteImg()?.srcset}
blurhash={siteImg()?.blurhash} blurhash={siteImg()?.blurhash}
class={[cards.cardNoPad, cards.cardGutSkip].join(' ')} class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
alt={`Banner image for ${siteTitle()}`} alt={`Banner image for ${siteTitle()}`}
style={{"height": "235px", "display": "block"}} style={{ height: "235px", display: "block" }}
/> />
</Show> </Show>
@ -114,7 +123,8 @@ const MastodonOAuth2Callback: Component = () => {
Contracting {siteTitle}... Contracting {siteTitle}...
</Title> </Title>
<p> <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> </p>
</div> </div>
</div> </div>

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import {render} from 'solid-js/web' import { render } from "solid-js/web";
import App from './App.js' import App from "./App.js";
import "./material/theme.css" 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>; }): mastodon.Paginator<mastodon.v1.Status[], unknown>;
}; };
export function useTimeline( export function useTimeline(timeline: Accessor<Timeline>) {
timeline: Accessor<Timeline>,
) {
let minId: string | undefined; let minId: string | undefined;
let maxId: string | undefined; let maxId: string | undefined;
let otl: Timeline | undefined; let otl: Timeline | undefined;

View file

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

View file

@ -1,5 +1,5 @@
import { createEffect, type ParentComponent } from "solid-js"; import { createEffect, type ParentComponent } from "solid-js";
import styles from './BottomSheet.module.css' import styles from "./BottomSheet.module.css";
export type BottomSheetProps = { export type BottomSheetProps = {
open?: boolean; 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; export default BottomSheet;

View file

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

View file

@ -1,5 +1,5 @@
.card { .card {
composes: surface from 'material.module.css'; composes: surface from "material.module.css";
border-radius: 2px; border-radius: 2px;
box-shadow: var(--tutu-shadow-e2); box-shadow: var(--tutu-shadow-e2);
transition: var(--tutu-transition-shadow); transition: var(--tutu-transition-shadow);
@ -12,7 +12,7 @@
} }
&:not(.manualMargin) { &:not(.manualMargin) {
&>:not(.cardNoPad) { & > :not(.cardNoPad) {
margin-inline: var(--card-pad, 20px); margin-inline: var(--card-pad, 20px);
} }
@ -20,7 +20,7 @@
margin-top: var(--card-gut, 20px); margin-top: var(--card-gut, 20px);
} }
>.cardGutSkip+*:not(.cardGutSkip) { > .cardGutSkip + *:not(.cardGutSkip) {
margin-top: var(--card-gut, 20px); margin-top: var(--card-gut, 20px);
} }
@ -28,7 +28,6 @@
margin-bottom: var(--card-gut, 20px); margin-bottom: var(--card-gut, 20px);
} }
} }
} }
.layoutCentered { .layoutCentered {
@ -52,4 +51,4 @@
overflow: auto; overflow: auto;
} }
} }
} }

View file

@ -1,5 +1,5 @@
.textfield { .textfield {
composes: touchTarget from 'material.module.css'; composes: touchTarget from "material.module.css";
--border-color: var(--tutu-color-inactive-on-surface); --border-color: var(--tutu-color-inactive-on-surface);
--active-border-color: var(--tutu-color-primary); --active-border-color: var(--tutu-color-primary);
@ -7,74 +7,78 @@
--active-label-color: var(--tutu-color-primary); --active-label-color: var(--tutu-color-primary);
--helper-text-color: var(--tutu-color-inactive-on-surface); --helper-text-color: var(--tutu-color-inactive-on-surface);
&>* { & > * {
width: 100%; width: 100%;
} }
&.error, &:has(>input[aria-invalid="true"]) { &.error,
&:not(:focus-within) { &:has(> input[aria-invalid="true"]) {
--border-color: var(--tutu-color-error-on-surface); &:not(:focus-within) {
--label-color: var(--tutu-color-error-on-surface); --border-color: var(--tutu-color-error-on-surface);
--helper-text-color: var(--tutu-color-error-on-surface); --label-color: var(--tutu-color-error-on-surface);
} --helper-text-color: var(--tutu-color-error-on-surface);
}
&:focus-within { &:focus-within {
--helper-text-color: var(--tutu-color-error-on-surface); --helper-text-color: var(--tutu-color-error-on-surface);
} }
} }
position: relative; position: relative;
&>label { & > label {
position: absolute; position: absolute;
left: 0; left: 0;
bottom: calc(10px + var(--bottom-height, 0px)); bottom: calc(10px + var(--bottom-height, 0px));
color: var(--label-color); color: var(--label-color);
transition: bottom .2s ease-in-out, font-size .2s ease-in-out, color .2s ease-in-out; transition:
cursor: text; bottom 0.2s ease-in-out,
font-size: 0.8125rem; font-size 0.2s ease-in-out,
color 0.2s ease-in-out;
cursor: text;
font-size: 0.8125rem;
} }
&>label:has(+ input:not(:placeholder-shown)) { & > label:has(+ input:not(:placeholder-shown)) {
bottom: calc(100% - 0.8125rem); bottom: calc(100% - 0.8125rem);
} }
&:focus-within>label, &.float-label>label { &:focus-within > label,
bottom: calc(100% - 0.8125rem); &.float-label > label {
color: var(--active-label-color); bottom: calc(100% - 0.8125rem);
color: var(--active-label-color);
} }
&>input[type='text'], & > input[type="text"],
&>input[type='password'] { & > input[type="password"] {
border: none; border: none;
outline: none; outline: none;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
background-color: transparent; background-color: transparent;
padding-top: 16px; padding-top: 16px;
padding-bottom: 8px; padding-bottom: 8px;
margin-bottom: 1px; margin-bottom: 1px;
transition: border-color .2s ease-in-out; transition: border-color 0.2s ease-in-out;
&:focus { &:focus {
border-bottom: 2px solid var(--active-border-color); border-bottom: 2px solid var(--active-border-color);
margin-bottom: 0; margin-bottom: 0;
} }
} }
&.withHelperText { &.withHelperText {
--bottom-height: 0.8125rem; --bottom-height: 0.8125rem;
} }
& .helperText { & .helperText {
color: var(--helper-text-color); color: var(--helper-text-color);
font-size: 0.8125rem; font-size: 0.8125rem;
line-height: 100%; line-height: 100%;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
line-clamp: 1; line-clamp: 1;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
min-height: 0.8125rem; min-height: 0.8125rem;
cursor: auto; cursor: auto;
} }
} }

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import { Dynamic } from "solid-js/web";
import typography from "./typography.module.css"; import typography from "./typography.module.css";
import { mergeClass } from "../utils"; import { mergeClass } from "../utils";
type AnyElement = keyof JSX.IntrinsicElements | ParentComponent<any> type AnyElement = keyof JSX.IntrinsicElements | ParentComponent<any>;
type PropsOf<E extends AnyElement> = type PropsOf<E extends AnyElement> =
E extends ParentComponent<infer Props> E extends ParentComponent<infer Props>
@ -12,9 +12,7 @@ type PropsOf<E extends AnyElement> =
? JSX.IntrinsicElements[E] ? JSX.IntrinsicElements[E]
: JSX.HTMLAttributes<HTMLElement>; : JSX.HTMLAttributes<HTMLElement>;
export type TypographyProps< export type TypographyProps<E extends AnyElement> = {
E extends AnyElement,
> = {
ref?: Ref<E>; ref?: Ref<E>;
component?: E; component?: E;
class?: string; class?: string;
@ -33,7 +31,9 @@ type TypographyKind =
| "caption" | "caption"
| "buttonText"; | "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, [ const [managed, passthough] = splitProps(props, [
"ref", "ref",
"component", "component",
@ -50,38 +50,38 @@ export function Typography<T extends AnyElement>(props: {typography: TypographyK
{...passthough} {...passthough}
></Dynamic> ></Dynamic>
); );
}; }
export function Display4<E extends AnyElement>(props: TypographyProps<E>) { 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>) { 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>) { 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>) { 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>) { 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>) { 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>) { 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>) { 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>) { 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>) { 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>) { 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"; 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() { export function useHeroSource() {
return useContext(HeroSourceContext) return useContext(HeroSourceContext);
} }

View file

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

View file

@ -18,16 +18,14 @@ const CompactToot: Component<CompactTootProps> = (props) => {
const toot = () => props.status; const toot = () => props.status;
return ( return (
<section <section
class={[tootStyle.compact, props.class || ""].join(" ")} class={[tootStyle.compact, props.class || ""].join(" ")}
lang={toot().language || undefined} lang={toot().language || undefined}
> >
<Img <Img
src={toot().account.avatar} src={toot().account.avatar}
class={[ class={[tootStyle.tootAvatar].join(" ")}
tootStyle.tootAvatar,
].join(" ")}
/> />
<div class={[tootStyle.compactAuthorGroup].join(' ')}> <div class={[tootStyle.compactAuthorGroup].join(" ")}>
<Body2 <Body2
ref={(e: { innerHTML: string }) => { ref={(e: { innerHTML: string }) => {
appliedCustomEmoji( appliedCustomEmoji(
@ -48,7 +46,7 @@ const CompactToot: Component<CompactTootProps> = (props) => {
ref={(e: { innerHTML: string }) => { ref={(e: { innerHTML: string }) => {
appliedCustomEmoji(e, toot().content, toot().emojis); appliedCustomEmoji(e, toot().content, toot().emojis);
}} }}
class={[tootStyle.compactTootContent].join(' ')} class={[tootStyle.compactTootContent].join(" ")}
></div> ></div>
</section> </section>
); );

View file

@ -155,20 +155,20 @@ const Home: ParentComponent = (props) => {
useDocumentTitle("Timelines"); useDocumentTitle("Timelines");
const now = createTimeSource(); const now = createTimeSource();
const settings$ = useStore($settings) const settings$ = useStore($settings);
const sessions = useSessions(); const sessions = useSessions();
const client = () => sessions()[0].client; const client = () => sessions()[0].client;
const [profile] = useAcctProfile(client); const [profile] = useAcctProfile(client);
const [panelOffset, setPanelOffset] = createSignal(0); const [panelOffset, setPanelOffset] = createSignal(0);
const prefetching = () => !settings$().prefetchTootsDisabled const prefetching = () => !settings$().prefetchTootsDisabled;
const [currentFocusOn, setCurrentFocusOn] = createSignal<HTMLElement[]>([]); const [currentFocusOn, setCurrentFocusOn] = createSignal<HTMLElement[]>([]);
const [focusRange, setFocusRange] = createSignal([0, 0] as readonly [ const [focusRange, setFocusRange] = createSignal([0, 0] as readonly [
number, number,
number, number,
]); ]);
const child = children(() => props.children) const child = children(() => props.children);
let scrollEventLockReleased = true; let scrollEventLockReleased = true;
@ -229,7 +229,7 @@ const Home: ParentComponent = (props) => {
const onTabClick = (idx: number) => { const onTabClick = (idx: number) => {
const items = panelList.querySelectorAll(".tab-panel"); const items = panelList.querySelectorAll(".tab-panel");
if (items.length > idx) { 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 <Scaffold
topbar={ topbar={
<AppBar position="static"> <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()}> <Tabs onFocusChanged={setCurrentFocusOn} offset={panelOffset()}>
<Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}> <Tab focus={isTabFocus(0)} onClick={[onTabClick, 0]}>
Home Home
@ -282,7 +286,14 @@ const Home: ParentComponent = (props) => {
</Tab> </Tab>
</Tabs> </Tabs>
<ProfileMenuButton profile={profile()}> <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> <ListItemText>Prefetch Toots</ListItemText>
<ListItemSecondaryAction> <ListItemSecondaryAction>
<Switch checked={prefetching()}></Switch> <Switch checked={prefetching()}></Switch>

View file

@ -11,7 +11,7 @@ const MediaAttachmentGrid: Component<{
}> = (props) => { }> = (props) => {
let rootRef: HTMLElement; let rootRef: HTMLElement;
const [viewerIndex, setViewerIndex] = createSignal<number>(); const [viewerIndex, setViewerIndex] = createSignal<number>();
const viewerOpened = () => typeof viewerIndex() !== "undefined" const viewerOpened = () => typeof viewerIndex() !== "undefined";
const gridTemplateColumns = () => { const gridTemplateColumns = () => {
const l = props.attachments.length; const l = props.attachments.length;
if (l < 2) { 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) { 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) => { const MediaViewer: ParentComponent<MediaViewerProps> = (props) => {
@ -128,6 +128,13 @@ const MediaViewer: ParentComponent<MediaViewerProps> = (props) => {
left: 0; left: 0;
z-index: 1; z-index: 1;
cursor: ${dragging() ? "grabbing" : "grab"}; 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 { .left-dock {
@ -207,7 +214,13 @@ const MediaViewer: ParentComponent<MediaViewerProps> = (props) => {
move: number, move: number,
idx: 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 [cx, cy] = center;
const iy = clamp(cy - otop, 0, oheight), const iy = clamp(cy - otop, 0, oheight),
ix = clamp(cx - oleft, 0, owidth); // in image coordinate system ix = clamp(cx - oleft, 0, owidth); // in image coordinate system

View file

@ -8,7 +8,13 @@ import {
Menu, Menu,
MenuItem, MenuItem,
} from "@suid/material"; } from "@suid/material";
import { Show, createSignal, createUniqueId, type ParentComponent } from "solid-js"; import {
ErrorBoundary,
Show,
createSignal,
createUniqueId,
type ParentComponent,
} from "solid-js";
import { import {
Settings as SettingsIcon, Settings as SettingsIcon,
Bookmark as BookmarkIcon, Bookmark as BookmarkIcon,
@ -42,79 +48,79 @@ const ProfileMenuButton: ParentComponent<{
return ( return (
<> <>
<ButtonBase <ButtonBase
aria-haspopup="true" aria-haspopup="true"
sx={{ borderRadius: "50%" }} sx={{ borderRadius: "50%" }}
id={buttonId} id={buttonId}
onClick={onClick} onClick={onClick}
aria-controls={open() ? menuId : undefined} aria-controls={open() ? menuId : undefined}
aria-expanded={open() ? "true" : undefined} aria-expanded={open() ? "true" : undefined}
> >
<Avatar <Avatar
alt={`${props.profile?.displayName}'s avatar`} alt={`${props.profile?.displayName}'s avatar`}
src={props.profile?.avatar} src={props.profile?.avatar}
></Avatar> ></Avatar>
</ButtonBase> </ButtonBase>
<Menu <Menu
id={menuId} id={menuId}
anchorEl={anchor()} anchorEl={anchor()}
open={open()} open={open()}
onClose={onClose} onClose={onClose}
MenuListProps={{ MenuListProps={{
"aria-labelledby": buttonId, "aria-labelledby": buttonId,
sx: { sx: {
minWidth: "220px", minWidth: "220px",
} },
}} }}
anchorOrigin={{ anchorOrigin={{
vertical: "top", vertical: "top",
horizontal: "right", horizontal: "right",
}} }}
transformOrigin={{ transformOrigin={{
vertical: "top", vertical: "top",
horizontal: "right", horizontal: "right",
}} }}
> >
<MenuItem> <MenuItem>
<ListItemAvatar> <ListItemAvatar>
<Avatar src={props.profile?.avatar}></Avatar> <Avatar src={props.profile?.avatar}></Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={props.profile?.displayName} primary={props.profile?.displayName}
secondary={`@${props.profile?.username}`} secondary={`@${props.profile?.username}`}
></ListItemText> ></ListItemText>
</MenuItem> </MenuItem>
<MenuItem> <MenuItem>
<ListItemIcon> <ListItemIcon>
<BookmarkIcon /> <BookmarkIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText>Bookmarks</ListItemText> <ListItemText>Bookmarks</ListItemText>
</MenuItem> </MenuItem>
<MenuItem> <MenuItem>
<ListItemIcon> <ListItemIcon>
<LikeIcon /> <LikeIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText>Likes</ListItemText> <ListItemText>Likes</ListItemText>
</MenuItem> </MenuItem>
<MenuItem> <MenuItem>
<ListItemIcon> <ListItemIcon>
<ListIcon /> <ListIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText>Lists</ListItemText> <ListItemText>Lists</ListItemText>
</MenuItem> </MenuItem>
<Divider />
<Show when={props.children}>
{props.children}
<Divider /> <Divider />
</Show> <Show when={props.children}>
<MenuItem component={A} href="/settings" onClick={onClose}> {props.children}
<ListItemIcon> <Divider />
<SettingsIcon /> </Show>
</ListItemIcon> <MenuItem component={A} href="/settings" onClick={onClose}>
<ListItemText>Settings</ListItemText> <ListItemIcon>
</MenuItem> <SettingsIcon />
</Menu> </ListItemIcon>
<ListItemText>Settings</ListItemText>
</MenuItem>
</Menu>
</> </>
); );
}; };

View file

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

View file

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

View file

@ -38,7 +38,9 @@ const TootThread: Component<TootThreadProps> = (props) => {
css` css`
article { 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; user-select: none;
cursor: pointer; cursor: pointer;
} }
@ -64,7 +66,10 @@ const TootThread: Component<TootThreadProps> = (props) => {
`; `;
return ( 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()}> <Show when={inReplyTo()}>
<CompactToot <CompactToot
status={inReplyTo()!} status={inReplyTo()!}

View file

@ -6,13 +6,14 @@
&.toot { &.toot {
/* fix composition ordering: I think the css module processor should aware the overriding and behaves, but no */ /* 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), height 225ms var(--tutu-anim-curve-std),
var(--tutu-transition-shadow); var(--tutu-transition-shadow);
border-radius: 0; border-radius: 0;
} }
&>.toot { & > .toot {
box-shadow: none; box-shadow: none;
} }
@ -46,11 +47,11 @@
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
>* { > * {
color: var(--tutu-color-secondary-text-on-surface); color: var(--tutu-color-secondary-text-on-surface);
} }
>:last-child { > :last-child {
grid-column: 1 /3; grid-column: 1 /3;
} }
@ -80,7 +81,7 @@
} }
.tootContent { .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-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px);
margin-right: var(--card-pad, 0); margin-right: var(--card-pad, 0);
line-height: 1.5; line-height: 1.5;
@ -150,14 +151,14 @@
} }
.tootAttachmentGrp { .tootAttachmentGrp {
composes: cardNoPad from '../material/cards.module.css'; composes: cardNoPad from "../material/cards.module.css";
margin-top: 1em; margin-top: 1em;
margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px); margin-left: calc(var(--card-pad, 0) + var(--toot-avatar-size, 0) + 8px);
margin-right: var(--card-pad, 0); margin-right: var(--card-pad, 0);
display: grid; display: grid;
gap: 4px; gap: 4px;
>:where(img) { > :where(img) {
max-height: 35vh; max-height: 35vh;
min-height: 40px; min-height: 40px;
object-fit: none; object-fit: none;
@ -168,7 +169,7 @@
} }
.tootBottomActionGrp { .tootBottomActionGrp {
composes: cardGutSkip from '../material/cards.module.css'; composes: cardGutSkip from "../material/cards.module.css";
padding-block: calc((var(--card-gut) - 10px) / 2); padding-block: calc((var(--card-gut) - 10px) / 2);
animation: 225ms var(--tutu-anim-curve-std) tootBottomExpanding; animation: 225ms var(--tutu-anim-curve-std) tootBottomExpanding;
@ -176,7 +177,7 @@
flex-flow: row wrap; flex-flow: row wrap;
justify-content: space-evenly; justify-content: space-evenly;
> button{ > button {
color: var(--tutu-color-on-surface); color: var(--tutu-color-on-surface);
padding: 10px 8px; padding: 10px 8px;
@ -206,4 +207,4 @@
100% { 100% {
opacity: 1; opacity: 1;
} }
} }

View file

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