Compare commits
	
		
			5 commits
		
	
	
		
			cd02dc2053
			...
			66366e6486
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 66366e6486 | ||
|  | 94088768ba | ||
|  | 4b17c426ab | ||
|  | 93b4cd065a | ||
|  | f06a7a6da1 | 
					 31 changed files with 380 additions and 264 deletions
				
			
		
							
								
								
									
										
											BIN
										
									
								
								bun.lockb
									
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								bun.lockb
									
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -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" | ||||
| } | ||||
|  |  | |||
|  | @ -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
									
								
							
							
						
						
									
										40
									
								
								src/UnexpectedError.tsx
									
										
									
									
									
										Normal 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; | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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( | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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")!); | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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>; | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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); | ||||
|  | @ -12,7 +12,7 @@ | |||
|   } | ||||
| 
 | ||||
|   &:not(.manualMargin) { | ||||
|     &>:not(.cardNoPad) { | ||||
|     & > :not(.cardNoPad) { | ||||
|       margin-inline: var(--card-pad, 20px); | ||||
|     } | ||||
| 
 | ||||
|  | @ -20,7 +20,7 @@ | |||
|       margin-top: var(--card-gut, 20px); | ||||
|     } | ||||
| 
 | ||||
|     >.cardGutSkip+*:not(.cardGutSkip) { | ||||
|     > .cardGutSkip + *:not(.cardGutSkip) { | ||||
|       margin-top: var(--card-gut, 20px); | ||||
|     } | ||||
| 
 | ||||
|  | @ -28,7 +28,6 @@ | |||
|       margin-bottom: var(--card-gut, 20px); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| .layoutCentered { | ||||
|  |  | |||
|  | @ -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); | ||||
|  | @ -7,74 +7,78 @@ | |||
|   --active-label-color: var(--tutu-color-primary); | ||||
|   --helper-text-color: var(--tutu-color-inactive-on-surface); | ||||
| 
 | ||||
|   &>* { | ||||
|       width: 100%; | ||||
|   & > * { | ||||
|     width: 100%; | ||||
|   } | ||||
| 
 | ||||
|   &.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); | ||||
|           --helper-text-color: var(--tutu-color-error-on-surface); | ||||
|       } | ||||
|   &.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); | ||||
|       --helper-text-color: var(--tutu-color-error-on-surface); | ||||
|     } | ||||
| 
 | ||||
|       &:focus-within { | ||||
|           --helper-text-color: var(--tutu-color-error-on-surface); | ||||
|       } | ||||
|     &:focus-within { | ||||
|       --helper-text-color: var(--tutu-color-error-on-surface); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   position: relative; | ||||
| 
 | ||||
|   &>label { | ||||
|       position: absolute; | ||||
|       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; | ||||
|       cursor: text; | ||||
|       font-size: 0.8125rem; | ||||
|   & > label { | ||||
|     position: absolute; | ||||
|     left: 0; | ||||
|     bottom: calc(10px + var(--bottom-height, 0px)); | ||||
|     color: var(--label-color); | ||||
|     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; | ||||
|   } | ||||
| 
 | ||||
|   &>label:has(+ input:not(:placeholder-shown)) { | ||||
|       bottom: calc(100% - 0.8125rem); | ||||
|   & > label:has(+ input:not(:placeholder-shown)) { | ||||
|     bottom: calc(100% - 0.8125rem); | ||||
|   } | ||||
| 
 | ||||
|   &:focus-within>label, &.float-label>label { | ||||
|       bottom: calc(100% - 0.8125rem); | ||||
|       color: var(--active-label-color); | ||||
|   &:focus-within > label, | ||||
|   &.float-label > label { | ||||
|     bottom: calc(100% - 0.8125rem); | ||||
|     color: var(--active-label-color); | ||||
|   } | ||||
| 
 | ||||
|   &>input[type='text'], | ||||
|   &>input[type='password'] { | ||||
|       border: none; | ||||
|       outline: none; | ||||
|       border-bottom: 1px solid var(--border-color); | ||||
|       background-color: transparent; | ||||
|       padding-top: 16px; | ||||
|       padding-bottom: 8px; | ||||
|       margin-bottom: 1px; | ||||
|       transition: border-color .2s ease-in-out; | ||||
|   & > input[type="text"], | ||||
|   & > input[type="password"] { | ||||
|     border: none; | ||||
|     outline: none; | ||||
|     border-bottom: 1px solid var(--border-color); | ||||
|     background-color: transparent; | ||||
|     padding-top: 16px; | ||||
|     padding-bottom: 8px; | ||||
|     margin-bottom: 1px; | ||||
|     transition: border-color 0.2s ease-in-out; | ||||
| 
 | ||||
|       &:focus { | ||||
|           border-bottom: 2px solid var(--active-border-color); | ||||
|           margin-bottom: 0; | ||||
|       } | ||||
|     &:focus { | ||||
|       border-bottom: 2px solid var(--active-border-color); | ||||
|       margin-bottom: 0; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &.withHelperText { | ||||
|       --bottom-height: 0.8125rem; | ||||
|     --bottom-height: 0.8125rem; | ||||
|   } | ||||
| 
 | ||||
|   & .helperText { | ||||
|       color: var(--helper-text-color); | ||||
|       font-size: 0.8125rem; | ||||
|       line-height: 100%; | ||||
|       -webkit-line-clamp: 1; | ||||
|       line-clamp: 1; | ||||
|       display: flex; | ||||
|       justify-content: space-between; | ||||
|       min-height: 0.8125rem; | ||||
|       cursor: auto; | ||||
|     color: var(--helper-text-color); | ||||
|     font-size: 0.8125rem; | ||||
|     line-height: 100%; | ||||
|     -webkit-line-clamp: 1; | ||||
|     line-clamp: 1; | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     min-height: 0.8125rem; | ||||
|     cursor: auto; | ||||
|   } | ||||
| } | ||||
|  | @ -10,7 +10,7 @@ | |||
| } | ||||
| 
 | ||||
| .button { | ||||
|   composes: buttonText from './typography.module.css'; | ||||
|   composes: buttonText from "./typography.module.css"; | ||||
|   composes: touchTarget; | ||||
| 
 | ||||
|   border: none; | ||||
|  | @ -18,28 +18,34 @@ | |||
|   color: var(--tutu-color-primary); | ||||
|   font-family: inherit; | ||||
| 
 | ||||
|   &:focus,&:hover,&:focus-visible { | ||||
|       background-color: var(--tutu-color-surface-dd); | ||||
|   &:focus, | ||||
|   &:hover, | ||||
|   &:focus-visible { | ||||
|     background-color: var(--tutu-color-surface-dd); | ||||
|   } | ||||
| 
 | ||||
|   &.pressed { | ||||
|       background-color: var(--tutu-color-surface-d); | ||||
|     background-color: var(--tutu-color-surface-d); | ||||
|   } | ||||
| 
 | ||||
|   &.raised { | ||||
|       background-color: var(--tutu-color-primary); | ||||
|       color: var(--tutu-color-on-primary); | ||||
|     background-color: var(--tutu-color-primary); | ||||
|     color: var(--tutu-color-on-primary); | ||||
|   } | ||||
| 
 | ||||
|   &:disabled, &[aria-disabled]:not([aria-disabled="false"]) { | ||||
|       color: #9e9e9e; | ||||
|   &:disabled, | ||||
|   &[aria-disabled]:not([aria-disabled="false"]) { | ||||
|     color: #9e9e9e; | ||||
| 
 | ||||
|       &:focus,&:hover,&:focus-visible { | ||||
|           background-color: transparent; | ||||
|       } | ||||
|     &: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); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,15 +2,16 @@ import { Theme, createTheme } from "@suid/material/styles"; | |||
| import { deepPurple, amber } from "@suid/material/colors"; | ||||
| import { Accessor } from "solid-js"; | ||||
| 
 | ||||
| export function useRootTheme() : Accessor<Theme> { | ||||
|   return () => createTheme({ | ||||
|     palette: { | ||||
|       primary: { | ||||
|         main: deepPurple[500] | ||||
| export function useRootTheme(): Accessor<Theme> { | ||||
|   return () => | ||||
|     createTheme({ | ||||
|       palette: { | ||||
|         primary: { | ||||
|           main: deepPurple[500], | ||||
|         }, | ||||
|         secondary: { | ||||
|           main: amber.A200, | ||||
|         }, | ||||
|       }, | ||||
|       secondary: { | ||||
|         main: amber.A200 | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
|     }); | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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>; | ||||
| } | ||||
|  |  | |||
|  | @ -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); | ||||
| } | ||||
|  |  | |||
|  | @ -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::", {}, { | ||||
|   encode: JSON.stringify, | ||||
|   decode: JSON.parse | ||||
| }) | ||||
| export const $settings = persistentMap<Settings>( | ||||
|   "settings::", | ||||
|   {}, | ||||
|   { | ||||
|     encode: JSON.stringify, | ||||
|     decode: JSON.parse, | ||||
|   }, | ||||
| ); | ||||
|  |  | |||
|  | @ -18,16 +18,14 @@ const CompactToot: Component<CompactTootProps> = (props) => { | |||
|   const toot = () => props.status; | ||||
|   return ( | ||||
|     <section | ||||
|       class={[tootStyle.compact,  props.class || ""].join(" ")} | ||||
|       class={[tootStyle.compact, props.class || ""].join(" ")} | ||||
|       lang={toot().language || undefined} | ||||
|     > | ||||
|       <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> | ||||
|   ); | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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, | ||||
|  | @ -42,79 +48,79 @@ const ProfileMenuButton: ParentComponent<{ | |||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <ButtonBase | ||||
|         aria-haspopup="true" | ||||
|         sx={{ borderRadius: "50%" }} | ||||
|         id={buttonId} | ||||
|         onClick={onClick} | ||||
|         aria-controls={open() ? menuId : undefined} | ||||
|         aria-expanded={open() ? "true" : undefined} | ||||
|       > | ||||
|         <Avatar | ||||
|           alt={`${props.profile?.displayName}'s avatar`} | ||||
|           src={props.profile?.avatar} | ||||
|         ></Avatar> | ||||
|       </ButtonBase> | ||||
|       <Menu | ||||
|         id={menuId} | ||||
|         anchorEl={anchor()} | ||||
|         open={open()} | ||||
|         onClose={onClose} | ||||
|         MenuListProps={{ | ||||
|           "aria-labelledby": buttonId, | ||||
|           sx: { | ||||
|             minWidth: "220px", | ||||
|           } | ||||
|         }} | ||||
|         anchorOrigin={{ | ||||
|           vertical: "top", | ||||
|           horizontal: "right", | ||||
|         }} | ||||
|         transformOrigin={{ | ||||
|           vertical: "top", | ||||
|           horizontal: "right", | ||||
|         }} | ||||
|       > | ||||
|         <MenuItem> | ||||
|           <ListItemAvatar> | ||||
|             <Avatar src={props.profile?.avatar}></Avatar> | ||||
|           </ListItemAvatar> | ||||
|           <ListItemText | ||||
|             primary={props.profile?.displayName} | ||||
|             secondary={`@${props.profile?.username}`} | ||||
|           ></ListItemText> | ||||
|         </MenuItem> | ||||
|         <ButtonBase | ||||
|           aria-haspopup="true" | ||||
|           sx={{ borderRadius: "50%" }} | ||||
|           id={buttonId} | ||||
|           onClick={onClick} | ||||
|           aria-controls={open() ? menuId : undefined} | ||||
|           aria-expanded={open() ? "true" : undefined} | ||||
|         > | ||||
|           <Avatar | ||||
|             alt={`${props.profile?.displayName}'s avatar`} | ||||
|             src={props.profile?.avatar} | ||||
|           ></Avatar> | ||||
|         </ButtonBase> | ||||
|         <Menu | ||||
|           id={menuId} | ||||
|           anchorEl={anchor()} | ||||
|           open={open()} | ||||
|           onClose={onClose} | ||||
|           MenuListProps={{ | ||||
|             "aria-labelledby": buttonId, | ||||
|             sx: { | ||||
|               minWidth: "220px", | ||||
|             }, | ||||
|           }} | ||||
|           anchorOrigin={{ | ||||
|             vertical: "top", | ||||
|             horizontal: "right", | ||||
|           }} | ||||
|           transformOrigin={{ | ||||
|             vertical: "top", | ||||
|             horizontal: "right", | ||||
|           }} | ||||
|         > | ||||
|           <MenuItem> | ||||
|             <ListItemAvatar> | ||||
|               <Avatar src={props.profile?.avatar}></Avatar> | ||||
|             </ListItemAvatar> | ||||
|             <ListItemText | ||||
|               primary={props.profile?.displayName} | ||||
|               secondary={`@${props.profile?.username}`} | ||||
|             ></ListItemText> | ||||
|           </MenuItem> | ||||
| 
 | ||||
|         <MenuItem> | ||||
|           <ListItemIcon> | ||||
|             <BookmarkIcon /> | ||||
|           </ListItemIcon> | ||||
|           <ListItemText>Bookmarks</ListItemText> | ||||
|         </MenuItem> | ||||
|         <MenuItem> | ||||
|           <ListItemIcon> | ||||
|             <LikeIcon /> | ||||
|           </ListItemIcon> | ||||
|           <ListItemText>Likes</ListItemText> | ||||
|         </MenuItem> | ||||
|         <MenuItem> | ||||
|           <ListItemIcon> | ||||
|             <ListIcon /> | ||||
|           </ListItemIcon> | ||||
|           <ListItemText>Lists</ListItemText> | ||||
|         </MenuItem> | ||||
|         <Divider /> | ||||
|         <Show when={props.children}> | ||||
|           {props.children} | ||||
|           <MenuItem> | ||||
|             <ListItemIcon> | ||||
|               <BookmarkIcon /> | ||||
|             </ListItemIcon> | ||||
|             <ListItemText>Bookmarks</ListItemText> | ||||
|           </MenuItem> | ||||
|           <MenuItem> | ||||
|             <ListItemIcon> | ||||
|               <LikeIcon /> | ||||
|             </ListItemIcon> | ||||
|             <ListItemText>Likes</ListItemText> | ||||
|           </MenuItem> | ||||
|           <MenuItem> | ||||
|             <ListItemIcon> | ||||
|               <ListIcon /> | ||||
|             </ListItemIcon> | ||||
|             <ListItemText>Lists</ListItemText> | ||||
|           </MenuItem> | ||||
|           <Divider /> | ||||
|         </Show> | ||||
|         <MenuItem component={A} href="/settings" onClick={onClose}> | ||||
|           <ListItemIcon> | ||||
|             <SettingsIcon /> | ||||
|           </ListItemIcon> | ||||
|           <ListItemText>Settings</ListItemText> | ||||
|         </MenuItem> | ||||
|       </Menu> | ||||
|           <Show when={props.children}> | ||||
|             {props.children} | ||||
|             <Divider /> | ||||
|           </Show> | ||||
|           <MenuItem component={A} href="/settings" onClick={onClose}> | ||||
|             <ListItemIcon> | ||||
|               <SettingsIcon /> | ||||
|             </ListItemIcon> | ||||
|             <ListItemText>Settings</ListItemText> | ||||
|           </MenuItem> | ||||
|         </Menu> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|  |  | |||
|  | @ -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} | ||||
|  |  | |||
|  | @ -1,8 +1,7 @@ | |||
| import type { Component } from "solid-js"; | ||||
| 
 | ||||
| 
 | ||||
| const TootBottomSheet: Component = (props) => { | ||||
|   return <></> | ||||
| } | ||||
|   return <></>; | ||||
| }; | ||||
| 
 | ||||
| export default TootBottomSheet | ||||
| export default TootBottomSheet; | ||||
|  |  | |||
|  | @ -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()!} | ||||
|  |  | |||
|  | @ -6,13 +6,14 @@ | |||
| 
 | ||||
|   &.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; | ||||
|   } | ||||
| 
 | ||||
|   &>.toot { | ||||
|   & > .toot { | ||||
|     box-shadow: none; | ||||
|   } | ||||
| 
 | ||||
|  | @ -46,11 +47,11 @@ | |||
|   display: grid; | ||||
|   grid-template-columns: 1fr auto; | ||||
| 
 | ||||
|   >* { | ||||
|   > * { | ||||
|     color: var(--tutu-color-secondary-text-on-surface); | ||||
|   } | ||||
| 
 | ||||
|   >:last-child { | ||||
|   > :last-child { | ||||
|     grid-column: 1 /3; | ||||
|   } | ||||
| 
 | ||||
|  | @ -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,14 +151,14 @@ | |||
| } | ||||
| 
 | ||||
| .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); | ||||
|   display: grid; | ||||
|   gap: 4px; | ||||
| 
 | ||||
|   >:where(img) { | ||||
|   > :where(img) { | ||||
|     max-height: 35vh; | ||||
|     min-height: 40px; | ||||
|     object-fit: none; | ||||
|  | @ -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; | ||||
|  | @ -176,7 +177,7 @@ | |||
|   flex-flow: row wrap; | ||||
|   justify-content: space-evenly; | ||||
| 
 | ||||
|   > button{ | ||||
|   > button { | ||||
|     color: var(--tutu-color-on-surface); | ||||
|     padding: 10px 8px; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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(" "); | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue