diff --git a/.browserlist b/.browserlist new file mode 100644 index 0000000..b547231 --- /dev/null +++ b/.browserlist @@ -0,0 +1 @@ +>0.3% and not dead, firefox>=98, safari>=15.4, chrome>=84 \ No newline at end of file diff --git a/README.md b/README.md index a4c432a..0bb7288 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,17 @@ Tutu is a comfortable experience for tooting. Designed to work on any device - d The code is built against those targets and Tutu must run on those platforms: -| Firefox | Safari | iOS | Chrome & Edge | -| ------- | ------ | ----- | ------------- | -| 115 | 15.6 | 15.6 | 108 | +| Firefox | Safari | iOS | Chrome | Edge | +| ------- | ------ | ----- | ------ | ---- | +| 98 | 15.4 | 15.4 | 84 | 87 | Tutu trys to push the Web technology to its limit. Some features might not be available on the platform does not meet the requirement. -## The "Nightly" Branch +## The "Next" Branch -Tutu built on the latest code, called the nightly version. You can tatse latest change but risks your data. +The "next" branch of the app is built on every commit pushed into "master". You can tatse latest change but risks your data. -[Launch Tutu (Nightly)](https://master.tututheapp.pages.dev) +[Launch Tutu (Next)](https://master.tututheapp.pages.dev) ## Build & Depoly diff --git a/bun.lockb b/bun.lockb index cd4221a..bec25b1 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/devnotes.md b/docs/devnotes.md index 6e10652..577c968 100644 --- a/docs/devnotes.md +++ b/docs/devnotes.md @@ -84,64 +84,3 @@ But, sometimes you need a redesigned (sometimes better) tool for the generic usa - *What* this new tool does? - *How* this tool works? - Clean up code regularly. Don't keep the unused code forever. - -## Managing CSS - -Two techniques are still: - -- Styled compoenent (solid-styled) -- Native CSS with CSS layering - -The second is recommended for massive use. A stylesheet for a component can be placed alongside -the component's file. The stylesheet must use the same name as the component's file name, but replace the extension with -`.css`. Say there is a component file "PreviewCard.tsx", the corresponding stylesheet is "PreviewCard.css". They are imported -by the component file, so the side effect will be applied by the bundler. - -The speicifc component uses a root class to scope the rulesets' scope. This convention allows the component's style can be influenced -by the other stylesheets. It works because Tutu is an end-user application, we gain the control of all stylesheets in the app (kind of). -Keep in mind that the native stylesheets will be applied globally at any time, you must carefully craft the stylesheet to avoid leaking -of style. - -Three additional CSS layers are declared as: - -- compat: Compatibility rules, like normalize.css -- theme: The theme rules -- material: The internal material styles - -When working on the material package, if the style is intended to work with the user styles, -it must be declared under the material layer. Otherwise the unlayer, which has the -highest priority in the author's, can be used. - -Styled component is still existing. Though styled component, using attributes for scoping, -may not be as performant as the techniques with CSS class names; -it's still provided in the code infrastructure for its ease. - -The following is an example of the recommended usage of solid-styled: - -```tsx -// An example of using solid-styled -import { css } from "solid-styled"; -import { createSignal } from "solid-js"; - -const Component = () => { - const [width, setWidth] = createSignal(100); - - css` - .root { - width: ${width()}%; - } - ` - return
-}; -``` - -When developing new component, you can use styled component at first, and migrate -to native css slowly. - -Before v2.0.0, there are CSS modules in use, but they are removed: - -- Duplicated loads -- Unaware of order (failed composing) -- Not-ready for hot reload - -In short, CSS module does not works well if the stylesheet will be accessed from more than one component. diff --git a/docs/versioning.md b/docs/versioning.md deleted file mode 100644 index 4d50356..0000000 --- a/docs/versioning.md +++ /dev/null @@ -1,32 +0,0 @@ -# Versioning & Development Cycle - -The versioning policy follows the [Semantic Versioning](https://semver.org/). -Since Tutu is an app for the end user, we redefine the some words in the policy: - -- API changes: the app is no longer available on certain platforms. - -## Development Cycle - -Dependency Freeze -> Development -> Release - -### Dependency Freeze - -This step is for: - -- Update dependencies -- Prepare the new version (like, bump the version number). - -New dependencies should not be added in this step. - -### Development - -In this step, dependencies can only be updated if it's required to fix bugs. - -New dependencies should be added as their use, in this step. - -### Release - -The version is released to production in this step. - -Before the next development step, new versions can still be released to -fix bugs. diff --git a/package.json b/package.json index 69b71d9..6222ecd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "tutu", - "version": "2.0.0", + "version": "1.1.0", "description": "", "private": true, "type": "module", @@ -10,40 +10,30 @@ "preview": "vite preview", "dist": "vite build", "count-source-lines": "exec scripts/src-lc.sh", - "typecheck": "tsc --noEmit --skipLibCheck", - "wdio": "wdio run ./wdio.conf.ts" + "typecheck": "tsc --noEmit --skipLibCheck" }, "keywords": [], "author": "Rubicon", "license": "Apache-2.0", "devDependencies": { - "@solid-devtools/overlay": "^0.33.0", + "@solid-devtools/overlay": "^0.30.1", "@suid/vite-plugin": "^0.3.1", - "@testing-library/webdriverio": "^3.2.1", "@types/hammerjs": "^2.0.46", "@types/masonry-layout": "^4.2.8", "@vite-pwa/assets-generator": "^0.2.6", - "@wdio/cli": "^9.5.1", - "@wdio/lighthouse-service": "^9.5.1", - "@wdio/local-runner": "^9.5.1", - "@wdio/mocha-framework": "^9.5.0", - "@wdio/spec-reporter": "^9.5.0", "postcss": "^8.4.49", - "prettier": "^3.4.2", - "tsx": "^4.19.2", - "typescript": "^5.7.2", - "vite": "^6.0.7", + "prettier": "^3.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.11", "vite-plugin-package-version": "^1.1.0", - "vite-plugin-pwa": "^0.21.1", - "vite-plugin-solid": "^2.11.0", + "vite-plugin-pwa": "^0.20.5", + "vite-plugin-solid": "^2.10.2", "vite-plugin-solid-styled": "^0.11.1", - "wdio-vite-service": "^2.0.0", - "wdio-wait-for": "^3.0.11", "workbox-build": "^7.3.0", - "wrangler": "^3.99.0" + "wrangler": "^3.86.1" }, "dependencies": { - "@formatjs/intl-localematcher": "^0.5.10", + "@formatjs/intl-localematcher": "^0.5.7", "@nanostores/persistent": "^0.10.2", "@nanostores/solid": "^0.5.0", "@solid-primitives/event-listener": "^2.3.3", @@ -52,8 +42,8 @@ "@solid-primitives/map": "^0.4.13", "@solid-primitives/page-visibility": "^2.0.17", "@solid-primitives/resize-observer": "^2.0.26", - "@solidjs/router": "^0.15.2", "@solid-primitives/rootless": "^1.4.5", + "@solidjs/router": "^0.15.1", "@suid/icons-material": "^0.8.1", "@suid/material": "^0.18.0", "blurhash": "^2.0.5", @@ -66,10 +56,9 @@ "masto": "^6.10.1", "nanostores": "^0.11.3", "normalize.css": "^8.0.1", - "solid-devtools": "^0.33.0", + "solid-devtools": "^0.30.1", "solid-js": "^1.9.3", "solid-styled": "^0.11.1", - "solid-transition-group": "^0.2.3", "stacktrace-js": "^2.0.2", "workbox-core": "^7.3.0", "workbox-precaching": "^7.3.0" diff --git a/src/App.css b/src/App.css index d2643a9..70ee2fb 100644 --- a/src/App.css +++ b/src/App.css @@ -1,46 +1,41 @@ -@layer compat, theme, material; +@import "normalize.css/normalize.css"; +@import "./material/theme.css"; -@import "normalize.css/normalize.css" layer(compat); -@import "./material/theme.css" layer(theme); -@import "./material/material.css" layer(material); +:root { + --safe-area-inset-top: env(safe-area-inset-top); + --safe-area-inset-left: env(safe-area-inset-left); + --safe-area-inset-bottom: env(safe-area-inset-bottom); + --safe-area-inset-right: env(safe-area-inset-right); + background-color: var(--tutu-color-surface, transparent); +} -@layer compat { - :root { - --safe-area-inset-top: env(safe-area-inset-top); - --safe-area-inset-left: env(safe-area-inset-left); - --safe-area-inset-bottom: env(safe-area-inset-bottom); - --safe-area-inset-right: env(safe-area-inset-right); - background-color: var(--tutu-color-surface, transparent); +/* +Fix the bottom gap on iOS standalone. +https://stackoverflow.com/questions/66005655/pwa-ios-child-of-body-not-taking-100-height-gap-on-bottom +*/ +@media screen and (display-mode: standalone) { + body { + width: 100%; + height: 100vh; } - /* - Fix the bottom gap on iOS standalone. - https://stackoverflow.com/questions/66005655/pwa-ios-child-of-body-not-taking-100-height-gap-on-bottom - */ - @media screen and (display-mode: standalone) { - body { - width: 100%; - height: 100vh; - } - - #root { - position: fixed; - top: 0; - left: 0; - height: 100vh; - width: 100vw; - } - } - - h1 { - margin: 0; - } - - * { - user-select: none; + #root { + position: fixed; + top: 0; + left: 0; + height: 100vh; + width: 100vw; } } .custom-emoji { width: 1em; -} \ No newline at end of file +} + +h1 { + margin: 0; +} + +* { + user-select: none; +} diff --git a/src/accounts/MastodonOAuth2Callback.css b/src/accounts/MastodonOAuth2Callback.css deleted file mode 100644 index c150d03..0000000 --- a/src/accounts/MastodonOAuth2Callback.css +++ /dev/null @@ -1,22 +0,0 @@ -.MastodonOAuth2Callback { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - width: 448px; - - @media (max-width: 600px) { - & { - position: static; - height: 100%; - width: 100%; - left: 0; - right: 0; - transform: none; - display: grid; - grid-template-rows: 1fr auto; - height: 100vh; - overflow: auto; - } - } -} \ No newline at end of file diff --git a/src/accounts/MastodonOAuth2Callback.tsx b/src/accounts/MastodonOAuth2Callback.tsx index d530095..398d79d 100644 --- a/src/accounts/MastodonOAuth2Callback.tsx +++ b/src/accounts/MastodonOAuth2Callback.tsx @@ -8,13 +8,13 @@ import { } from "solid-js"; import { acceptAccountViaAuthCode } from "./stores"; import { $settings } from "../settings/stores"; -import "~material/cards.css"; +import { useDocumentTitle } from "../utils"; +import cards from "~material/cards.module.css"; import { LinearProgress } from "@suid/material"; import Img from "~material/Img"; import { createRestAPIClient } from "masto"; import { Title } from "~material/typography"; import { useNavigator } from "~platform/StackedRouter"; -import DocumentTitle from "~platform/DocumentTitle"; type OAuth2CallbackParams = { code?: string; @@ -27,12 +27,13 @@ const MastodonOAuth2Callback: Component = () => { const titleId = createUniqueId(); const [params] = useSearchParams(); const { push: navigate } = useNavigator(); + const setDocumentTitle = useDocumentTitle("Back from Mastodon..."); const [siteImg, setSiteImg] = createSignal<{ src: string; srcset?: string; blurhash: string; }>(); - const [siteTitle, setSiteTitle] = createSignal("Mastodon"); + const [siteTitle, setSiteTitle] = createSignal("the Mastodon server"); onMount(async () => { const onGoingOAuth2Process = $settings.get().onGoingOAuth2Process; @@ -41,6 +42,7 @@ const MastodonOAuth2Callback: Component = () => { url: onGoingOAuth2Process, }); const ins = await client.v2.instance.fetch(); + setDocumentTitle(`Back from ${ins.title}...`); setSiteTitle(ins.title); const srcset = []; @@ -91,45 +93,42 @@ const MastodonOAuth2Callback: Component = () => { }); }); return ( - <> - Back from {siteTitle()} -
-
- - - } - > - {`Banner +
+ + - + > + } + > + {`Banner + - - Contracting {siteTitle}... - -

- If this page stays too long, you can close this page and sign in - again. -

-
-
- + + Contracting {siteTitle}... + +

+ If this page stays too long, you can close this page and sign in + again. +

+ + ); }; diff --git a/src/accounts/SignIn.css b/src/accounts/SignIn.css deleted file mode 100644 index 11f467a..0000000 --- a/src/accounts/SignIn.css +++ /dev/null @@ -1,27 +0,0 @@ -.SignIn { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - width: 448px; - - @media (max-width: 600px) { - & { - position: static; - height: 100vh; - width: 100%; - transform: none; - overflow: auto; - } - } - - >.key-content { - height: 100%; - - >form { - display: flex; - flex-flow: column; - gap: 16px; - } - } -} \ No newline at end of file diff --git a/src/accounts/SignIn.tsx b/src/accounts/SignIn.tsx index 3a6233d..c8699d2 100644 --- a/src/accounts/SignIn.tsx +++ b/src/accounts/SignIn.tsx @@ -2,21 +2,22 @@ import { Component, Show, createEffect, + createSelector, createSignal, createUniqueId, onMount, } from "solid-js"; -import "~material/cards.css"; +import cards from "~material/cards.module.css"; import TextField from "~material/TextField.js"; import Button from "~material/Button.js"; +import { useDocumentTitle } from "../utils"; import { Title } from "~material/typography"; +import { css } from "solid-styled"; import { LinearProgress } from "@suid/material"; import { createRestAPIClient } from "masto"; import { getOrRegisterApp } from "./stores"; import { useSearchParams } from "@solidjs/router"; import { $settings } from "../settings/stores"; -import "./SignIn.css"; -import DocumentTitle from "~platform/DocumentTitle"; type ErrorParams = { error: string; @@ -34,6 +35,15 @@ const SignIn: Component = () => { const [serverUrlError, setServerUrlError] = createSignal(false); const [targetSiteTitle, setTargetSiteTitle] = createSignal(""); + useDocumentTitle("Sign In"); + css` + form { + display: flex; + flex-flow: column; + gap: 16px; + } + `; + const serverUrl = () => { const url = rawServerUrl(); if (url.length === 0 || /^%w:/.test(url)) { @@ -111,58 +121,55 @@ const SignIn: Component = () => { }; return ( - <> - Sign In -
- -
-

Authorization is failed.

-

{params.errorDescription}

-

- Please try again later. If the problem persists, you can ask for - help from the server administrator. -

-
-
-
- -
- Sign in with Your Mastodon Account - - -
- -
- +
+ +
+

Authorization is failed.

+

{params.errorDescription}

+

+ Please try again later. If the problem persist, you can seek for + help from the server administrator. +

-
- + +
+ +
+ Sign in with Your Mastodon Account + + +
+ +
+ +
+ ); }; diff --git a/src/accounts/stores.ts b/src/accounts/stores.ts index 006ea9e..c44a012 100644 --- a/src/accounts/stores.ts +++ b/src/accounts/stores.ts @@ -6,19 +6,10 @@ import { } from "masto"; import { createMastoClientFor } from "../masto/clients"; -export type RemoteServer = { +export type Account = { site: string; -}; - -export type AccountKey = RemoteServer & { accessToken: string; -}; -export function isAccountKey(object: RemoteServer): object is AccountKey { - return !!(object as Record)["accessToken"]; -} - -export type Account = AccountKey & { tokenType: string; scope: string; createdAt: number; @@ -26,10 +17,6 @@ export type Account = AccountKey & { inf?: mastodon.v1.AccountCredentials; }; -export function isAccount(object: RemoteServer) { - return isAccountKey(object) && !!(object as Record)["tokenType"]; -} - export const $accounts = persistentAtom("accounts", [], { encode: JSON.stringify, decode: JSON.parse, diff --git a/src/masto/base.ts b/src/masto/base.ts deleted file mode 100644 index 79d9755..0000000 --- a/src/masto/base.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { createCacheBucket } from "~platform/cache"; -import { MastoHttpError, MastoTimeoutError, MastoUnexpectedError } from "masto"; - -export const CACHE_BUCKET_NAME = "mastodon"; - -export const cacheBucket = /* @__PURE__ */ createCacheBucket(CACHE_BUCKET_NAME); - -export function toSmallCamelCase(object: T): T { - if (!object || typeof object !== "object") { - return object; - } else if (Array.isArray(object)) { - return object.map(toSmallCamelCase) as T; - } - - const result = {} as Record; - for (const k in object) { - const value = toSmallCamelCase(object[k]); - const nk = - typeof k === "string" - ? k.replace(/_(.)/g, (_, match) => match.toUpperCase()) - : k; - result[nk] = value; - } - - return result as T; -} - -function contentTypeOf(headers: Headers) { - const raw = headers.get("Content-Type")?.replace(/\s*;.*$/, ""); - if (!raw) { - return; - } - - return raw; -} - -/** - * Wrpas the error reason into masto's errors: - * {@link MastoHttpError} if the reason is a {@link Response}, - * {@link MastoTimeoutError} if the reason is a timeout error. - * - * @throws If the reason is unexpected, {@link MastoUnexpectedError} will be thrown. - */ -export async function wrapsError(reason: Response): Promise; -export async function wrapsError(reason: { - name: "TimeoutError"; -}): Promise; -export async function wrapsError(reason: T): Promise; - -export async function wrapsError(reason: unknown) { - if (reason instanceof Response) { - const contentType = contentTypeOf(reason.headers); - if (!contentType) { - throw new MastoUnexpectedError( - "The server returned data with an unknown encoding. The server may be down", - ); - } - - const data = await reason.json(); - const { - error: message, - errorDescription, - details, - ...additionalProperties - } = data; - - return new MastoHttpError( - { - statusCode: reason.status, - message, - description: errorDescription, - details, - additionalProperties, - }, - { cause: reason }, - ); - } - - if (reason && (reason as { name?: string }).name === "TimeoutError") { - return new MastoTimeoutError("Request timed out", { cause: reason }); - } - - return reason; -} diff --git a/src/masto/clients.ts b/src/masto/clients.ts index 7cd22e7..f853fa7 100644 --- a/src/masto/clients.ts +++ b/src/masto/clients.ts @@ -4,10 +4,9 @@ import { createMemo, createRenderEffect, createResource, - untrack, useContext, } from "solid-js"; -import { Account, type RemoteServer } from "../accounts/stores"; +import { Account } from "../accounts/stores"; import { createRestAPIClient, mastodon } from "masto"; import { useLocation } from "@solidjs/router"; import { useNavigator } from "~platform/StackedRouter"; @@ -58,15 +57,15 @@ export const Provider = Context.Provider; export function useSessions() { const sessions = useSessionsRaw(); - const { push } = useNavigator(); + const {push} = useNavigator(); const location = useLocation(); createRenderEffect(() => { - if (untrack(() => sessions().length) > 0) return; - - push("/accounts/sign-in?back=" + encodeURIComponent(location.pathname), { - replace: true, - }); + if (sessions().length > 0) return; + push( + "/accounts/sign-in?back=" + encodeURIComponent(location.pathname), + { replace: "all" }, + ); }); return sessions; @@ -115,7 +114,7 @@ export function useDefaultSession() { * - If the username is not present, any session on the site is returned; or, * - If no available session available for the pattern, an unauthorised session is returned. * - * In an unauthorised session, the `.account` is {@link RemoteServer} and the `client` is an + * In an unauthorised session, the `.account` is `undefined` and the `client` is an * unauthorised client for the site. This client may not available for some operations. */ export function useSessionForAcctStr(acct: Accessor) { @@ -131,8 +130,8 @@ export function useSessionForAcctStr(acct: Accessor) { return ( authedSession ?? { client: createUnauthorizedClient(inputSite), - account: { site: inputSite } as RemoteServer, // TODO: we need some security checks here? - } as const + account: undefined, + } ); }); } diff --git a/src/masto/statuses.ts b/src/masto/statuses.ts deleted file mode 100644 index 4da437e..0000000 --- a/src/masto/statuses.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { CachedFetch } from "~platform/cache"; -import { cacheBucket, toSmallCamelCase, wrapsError } from "./base"; -import { isAccountKey, type RemoteServer } from "../accounts/stores"; -import { type mastodon } from "masto"; - -export const fetchStatus = /* @__PURE__ */ new CachedFetch( - cacheBucket, - (session: RemoteServer, id: string) => { - const headers = new Headers({ - Accept: "application/json", - }); - if (isAccountKey(session)) { - headers.set("Authorization", `Bearer ${session.accessToken}`); - } - return { - url: new URL(`./api/v1/statuses/${id}`, session.site).toString(), - headers, - }; - }, - async (response) => { - try { - if (!response.ok) { - throw response; - } - return toSmallCamelCase( - await response.json(), - ) as unknown as mastodon.v1.Status; - } catch (reason) { - throw wrapsError(reason); - } - }, -); diff --git a/src/masto/timelines.ts b/src/masto/timelines.ts index 0f04e83..19abea7 100644 --- a/src/masto/timelines.ts +++ b/src/masto/timelines.ts @@ -248,64 +248,6 @@ export type TimelineResource = [ { refetch(info?: TimelineFetchDirection): void }, ]; -export const emptyTimeline = { - list() { - return emptyTimeline; - }, - - setDirection() { - return emptyTimeline; - }, - - async next(): Promise> { - return { - value: undefined, - done: true, - }; - }, - - getDirection(): TimelineFetchDirection { - return "next"; - }, - - clone() { - return emptyTimeline; - }, - - async return(): Promise> { - return { - value: undefined, - done: true, - }; - }, - - async throw(e?: unknown) { - throw e; - }, - - async *values() {}, - async *[Symbol.asyncIterator](): AsyncIterator { - return undefined; - }, - - async then( - onresolve?: null | ((value: any[]) => TNext | PromiseLike), - onrejected?: null | ((reason: unknown) => ENext | PromiseLike), - ) { - try { - if (!onresolve) { - throw new TypeError("no onresolve"); - } - return await onresolve([]); - } catch (reason) { - if (!onrejected) { - throw reason; - } - return await onrejected(reason); - } - }, -}; - /** * Create auto managed timeline controls. * diff --git a/src/material/AppTopBar.css b/src/material/AppTopBar.css index d0976ca..de3426d 100644 --- a/src/material/AppTopBar.css +++ b/src/material/AppTopBar.css @@ -1,27 +1,6 @@ .AppTopBar { - &::before { - contain: strict; - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - height: var(--safe-area-inset-top, 0); - background-color: rgba(0, 0, 0, 0.2); - } - - &>.MuiToolbar-root { + & > .toolbar { padding-top: var(--safe-area-inset-top, 0px); gap: 8px; - - &>button:first-child, - &>.MuiButtonBase-root:first-child { - margin-left: -0.15em; - } - - &>button:last-child, - &>.MuiButtonBase-root:last-child { - margin-right: -0.15em; - } } -} \ No newline at end of file +} diff --git a/src/material/AppTopBar.tsx b/src/material/AppTopBar.tsx index a75c7c3..a9ad382 100644 --- a/src/material/AppTopBar.tsx +++ b/src/material/AppTopBar.tsx @@ -20,6 +20,7 @@ const AppTopBar: ParentComponent<{ > windowSize.height ? "dense" : "regular"} + class="toolbar" sx={{ paddingTop: "var(--safe-area-inset-top, 0px)" }} > {props.children} diff --git a/src/material/BottomSheet.tsx b/src/material/BottomSheet.tsx index 843f245..99bfbd0 100644 --- a/src/material/BottomSheet.tsx +++ b/src/material/BottomSheet.tsx @@ -7,6 +7,7 @@ import { type ParentComponent, } from "solid-js"; import "./BottomSheet.css"; +import material from "./material.module.css"; import { ANIM_CURVE_ACELERATION, ANIM_CURVE_DECELERATION } from "./theme"; import { animateSlideInFromRight, @@ -50,7 +51,7 @@ function animateSlideInFromBottom(element: HTMLElement, reverse?: boolean) { } const BottomSheet: ParentComponent = (props) => { - let element!: HTMLDialogElement; + let element: HTMLDialogElement; let animation: Animation | undefined; const child = children(() => props.children); @@ -133,7 +134,7 @@ const BottomSheet: ParentComponent = (props) => { return ( > = ( props, ) => { - const [managed, passthough] = splitProps(props, [ "type"]); + const [managed, passthough] = splitProps(props, ["class", "type"]); const type = () => managed.type ?? "button"; return ( ); diff --git a/src/material/Img.tsx b/src/material/Img.tsx index 8cb33c9..774460c 100644 --- a/src/material/Img.tsx +++ b/src/material/Img.tsx @@ -3,12 +3,14 @@ import { splitProps, Component, createSignal, + createEffect, onMount, createRenderEffect, Show, } from "solid-js"; import { css } from "solid-styled"; import { decode } from "blurhash"; +import { mergeClass } from "../utils"; type ImgProps = { blurhash?: string; @@ -22,7 +24,6 @@ const Img: Component = (props) => { "blurhash", "keepBlur", "class", - "classList", "style", ]); const [isImgLoaded, setIsImgLoaded] = createSignal(false); @@ -60,21 +61,21 @@ const Img: Component = (props) => { const onImgLoaded = () => { setIsImgLoaded(true); setImgSize({ - width: imgE!.width, - height: imgE!.height, + width: imgE.width, + height: imgE.height, }); }; const onMetadataLoaded = () => { setImgSize({ - width: imgE!.width, - height: imgE!.height, + width: imgE.width, + height: imgE.height, }); }; onMount(() => { setImgSize((x) => { - const parent = imgE!.parentElement; + const parent = imgE.parentElement; if (!parent) return x; return x ? x @@ -86,14 +87,7 @@ const Img: Component = (props) => { }); return ( -
+
{ diff --git a/src/material/Menu.tsx b/src/material/Menu.tsx index 3bc2ab0..931b756 100644 --- a/src/material/Menu.tsx +++ b/src/material/Menu.tsx @@ -108,7 +108,7 @@ function animateGrowFromTopLeft( * - Use {@link MenuItem} from SUID as children. */ const Menu: Component = (oprops) => { - let root!: HTMLDialogElement; + let root: HTMLDialogElement; const windowSize = useWindowSize(); const [props, rest] = splitProps(oprops, [ "open", diff --git a/src/material/Tab.css b/src/material/Tab.css deleted file mode 100644 index 38f49d3..0000000 --- a/src/material/Tab.css +++ /dev/null @@ -1,32 +0,0 @@ -.Tab { - cursor: pointer; - background: none; - border: none; - height: 100%; - max-width: min(calc(100% - 56px), 264px); - padding: 10px 24px; - font-size: 0.8135rem; - font-weight: 600; - text-transform: uppercase; - transition: color 120ms var(--tutu-anim-curve-std); - - :root:where([lang^="zh"], - [lang="zh"], - [lang^="kr"], - [lang="kr"], - [lang^="ja"], - [lang="ja"]) & { - font-size: 0.85rem; - } -} - -.MuiToolbar-root .Tab { - color: rgba(255, 255, 255, 0.7); - - &:hover, - &:focus, - &.focus, - &.Tabs-focus { - color: white; - } -} \ No newline at end of file diff --git a/src/material/Tab.tsx b/src/material/Tab.tsx index 10d781a..2145b89 100644 --- a/src/material/Tab.tsx +++ b/src/material/Tab.tsx @@ -1,18 +1,26 @@ import { + Component, createEffect, splitProps, type JSX, type ParentComponent, } from "solid-js"; +import { css } from "solid-styled"; import { useTabListContext } from "./Tabs"; -import "./Tab.css"; const Tab: ParentComponent< { focus?: boolean; + large?: boolean; } & JSX.ButtonHTMLAttributes > = (props) => { - const [managed, rest] = splitProps(props, ["focus", "type", "role", "ref"]); + const [managed, rest] = splitProps(props, [ + "focus", + "large", + "type", + "role", + "ref", + ]); let self: HTMLButtonElement; const { focusOn: [, setFocusOn], @@ -27,7 +35,32 @@ const Tab: ParentComponent< } return managed.focus; }); + css` + .tab { + cursor: pointer; + background: none; + border: none; + min-width: ${managed.large ? "160px" : "72px"}; + height: 48px; + max-width: min(calc(100% - 56px), 264px); + padding: 10px 24px; + font-size: 0.8135rem; + font-weight: 600; + text-transform: uppercase; + transition: color 120ms var(--tutu-anim-curve-std); + } + :global(.MuiToolbar-root) .tab { + color: rgba(255, 255, 255, 0.7); + + &:hover, + &:focus, + &.focus, + &:global(.tablist-focus) { + color: white; + } + } + `; return (