Compare commits

..

No commits in common. "4718239723fc2c7319ef3411a94ea43c25f5f72f" and "b6fbe71a51191ae366a01c20c669bf5a96224d27" have entirely different histories.

23 changed files with 163 additions and 234 deletions

View file

@ -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 <div class="root"></div>
};
```
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.

View file

@ -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;
}
h1 {
margin: 0;
}
* {
user-select: none;
}

View file

@ -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;
}
}
}

View file

@ -8,7 +8,7 @@ import {
} from "solid-js";
import { acceptAccountViaAuthCode } from "./stores";
import { $settings } from "../settings/stores";
import "~material/cards.css";
import cards from "~material/cards.module.css";
import { LinearProgress } from "@suid/material";
import Img from "~material/Img";
import { createRestAPIClient } from "masto";
@ -92,11 +92,11 @@ const MastodonOAuth2Callback: Component = () => {
});
return (
<>
<DocumentTitle>Back from {siteTitle()}</DocumentTitle>
<main class="MastodonOAuth2Callback">
<div class="card card-auto-margin" aria-busy="true" aria-describedby={progressId}>
<DocumentTitle>Back from {siteTitle()}</DocumentTitle>
<div class={cards.layoutCentered}>
<div class={cards.card} aria-busy="true" aria-describedby={progressId}>
<LinearProgress
class="card-no-pad card-gut-skip"
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
id={progressId}
aria-labelledby={titleId}
/>
@ -114,7 +114,7 @@ const MastodonOAuth2Callback: Component = () => {
src={siteImg()?.src}
srcset={siteImg()?.srcset}
blurhash={siteImg()?.blurhash}
class="card-no-pad card-gut-skip"
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
alt={`Banner image for ${siteTitle()}`}
style={{ height: "235px", display: "block" }}
/>
@ -128,7 +128,7 @@ const MastodonOAuth2Callback: Component = () => {
again.
</p>
</div>
</main>
</div>
</>
);
};

View file

@ -6,7 +6,7 @@ import {
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 { Title } from "~material/typography";
@ -115,7 +115,7 @@ const SignIn: Component = () => {
<DocumentTitle>Sign In</DocumentTitle>
<main class="SignIn">
<Show when={params.error || params.errorDescription}>
<div class="card card-auto-margin" style={{ "margin-bottom": "20px" }}>
<div class={cards.card} style={{ "margin-bottom": "20px" }}>
<p>Authorization is failed.</p>
<p>{params.errorDescription}</p>
<p>
@ -125,7 +125,7 @@ const SignIn: Component = () => {
</div>
</Show>
<div
class="card card-auto-margin key-content"
class={`${cards.card} key-content`}
aria-busy={currentState() !== "inactive" ? "true" : "false"}
aria-describedby={
currentState() !== "inactive" ? progressId : undefined
@ -135,7 +135,7 @@ const SignIn: Component = () => {
}}
>
<LinearProgress
class={"card-no-pad card-gut-skip"}
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
id={progressId}
sx={currentState() === "inactive" ? { display: "none" } : undefined}
/>

View file

@ -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,
@ -133,7 +134,7 @@ const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
return (
<dialog
class={`BottomSheet surface ${props.class || ""}`}
class={`BottomSheet ${material.surface} ${props.class || ""}`}
classList={{
["bottom"]: props.bottomUp,
}}

View file

@ -1,4 +1,5 @@
import { Component, JSX, splitProps } from "solid-js";
import materialStyles from "./material.module.css";
import "./typography.css";
/**
@ -9,12 +10,13 @@ import "./typography.css";
const Button: Component<JSX.ButtonHTMLAttributes<HTMLButtonElement>> = (
props,
) => {
const [managed, passthough] = splitProps(props, [ "type"]);
const [managed, passthough] = splitProps(props, ["class", "type"]);
const type = () => managed.type ?? "button";
return (
<button
type={type()}
class={`${materialStyles.button} buttonText ${managed.class || ""}`}
{...passthough}
></button>
);

View file

@ -6,7 +6,7 @@ import {
onMount,
Show,
} from "solid-js";
import "./TextField.css";
import formStyles from "./form.module.css";
export type TextFieldProps = {
label?: string;
@ -47,12 +47,12 @@ const TextField: Component<TextFieldProps> = (props) => {
const inputId = () => props.inputId ?? altInputId;
const fieldClass = () => {
const cls = ["TextField"];
const cls = [formStyles.textfield];
if (typeof props.helperText !== "undefined") {
cls.push("withHelperText");
cls.push(formStyles.withHelperText);
}
if (props.error) {
cls.push("error");
cls.push(formStyles.error);
}
return cls.join(" ");
};
@ -71,7 +71,7 @@ const TextField: Component<TextFieldProps> = (props) => {
name={props.name}
/>
<Show when={typeof props.helperText !== "undefined"}>
<span class="helperText">{props.helperText}</span>
<span class={formStyles.helperText}>{props.helperText}</span>
</Show>
</div>
);

View file

@ -1,56 +0,0 @@
@layer material {
.card {
--card-pad: 20px;
--card-gut: 20px;
background-color: var(--tutu-color-surface);
color: var(--tutu-color-on-surface);
border-radius: 2px;
box-shadow: var(--tutu-shadow-e2);
transition: var(--tutu-transition-shadow);
overflow: hidden;
background-color: var(--tutu-color-surface-l);
&:focus-within,
&:focus-visible {
box-shadow: var(--tutu-shadow-e8);
}
&>.card-pad {
margin-inline: var(--card-pad);
}
&>.card-gut {
&:first-child {
margin-top: var(--card-gut);
}
&+.card-gut {
margin-top: var(--card-gut);
}
&:last-child {
margin-bottom: var(--card-gut);
}
}
&.card-auto-margin {
&> :not(.card-no-pad) {
margin-inline: var(--card-pad, 20px);
}
> :not(.card-gut-skip):first-child {
margin-top: var(--card-gut, 20px);
}
>.card-gut-skip+*:not(.card-gut-skip) {
margin-top: var(--card-gut, 20px);
}
> :not(.card-gut-skip):last-child {
margin-bottom: var(--card-gut, 20px);
}
}
}
}

View file

@ -0,0 +1,54 @@
.card {
composes: surface from "material.module.css";
border-radius: 2px;
box-shadow: var(--tutu-shadow-e2);
transition: var(--tutu-transition-shadow);
overflow: hidden;
background-color: var(--tutu-color-surface-l);
&:focus-within,
&:focus-visible {
box-shadow: var(--tutu-shadow-e8);
}
&:not(.manualMargin) {
&> :not(.cardNoPad) {
margin-inline: var(--card-pad, 20px);
}
> :not(.cardGutSkip):first-child {
margin-top: var(--card-gut, 20px);
}
>.cardGutSkip+*:not(.cardGutSkip) {
margin-top: var(--card-gut, 20px);
}
> :not(.cardGutSkip):last-child {
margin-bottom: var(--card-gut, 20px);
}
}
}
.layoutCentered {
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;
}
}
}

View file

@ -1,7 +1,5 @@
.TextField {
min-width: 44px;
min-height: 44px;
cursor: pointer;
.textfield {
composes: touchTarget from "material.module.css";
--border-color: var(--tutu-color-inactive-on-surface);
--active-border-color: var(--tutu-color-primary);

View file

@ -1,14 +1,16 @@
@import "./typography.css";
.surface {
background-color: var(--tutu-color-surface);
color: var(--tutu-color-on-surface);
}
button {
.touchTarget {
min-width: 44px;
min-height: 44px;
cursor: pointer;
}
.button {
composes: touchTarget;
border: none;
background-color: transparent;

View file

View file

@ -1,6 +1,3 @@
/* Don't import this file directly. This file is already included in material.css */
.display4 {
font-size: 7rem;
font-weight: 300;

View file

@ -1,11 +1,21 @@
import { splitProps, type Ref, ComponentProps, ValidComponent } from "solid-js";
import { JSX, ParentComponent, splitProps, type Ref } from "solid-js";
import { Dynamic } from "solid-js/web";
import "./typography.css";
export type TypographyProps<E extends ValidComponent> = {
type AnyElement = keyof JSX.IntrinsicElements | ParentComponent<any>;
type PropsOf<E extends AnyElement> =
E extends ParentComponent<infer Props>
? Props
: E extends keyof JSX.IntrinsicElements
? JSX.IntrinsicElements[E]
: JSX.HTMLAttributes<HTMLElement>;
export type TypographyProps<E extends AnyElement> = {
ref?: Ref<E>;
component?: E;
class?: string;
} & ComponentProps<E>;
} & PropsOf<E>;
type TypographyKind =
| "display4"
@ -20,7 +30,7 @@ type TypographyKind =
| "caption"
| "buttonText";
export function Typography<T extends ValidComponent>(
export function Typography<T extends AnyElement>(
props: { typography: TypographyKind } & TypographyProps<T>,
) {
const [managed, passthough] = splitProps(props, [
@ -39,36 +49,36 @@ export function Typography<T extends ValidComponent>(
);
}
export function Display4<E extends ValidComponent>(props: TypographyProps<E>) {
export function Display4<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"display4"} {...props}></Typography>;
}
export function Display3<E extends ValidComponent>(props: TypographyProps<E>) {
export function Display3<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"display3"} {...props}></Typography>;
}
export function Display2<E extends ValidComponent>(props: TypographyProps<E>) {
export function Display2<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"display2"} {...props}></Typography>;
}
export function Display1<E extends ValidComponent>(props: TypographyProps<E>) {
export function Display1<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"display1"} {...props}></Typography>;
}
export function Headline<E extends ValidComponent>(props: TypographyProps<E>) {
export function Headline<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"headline"} {...props}></Typography>;
}
export function Title<E extends ValidComponent>(props: TypographyProps<E>) {
export function Title<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"title"} {...props}></Typography>;
}
export function Subheading<E extends ValidComponent>(props: TypographyProps<E>) {
export function Subheading<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"subheading"} {...props}></Typography>;
}
export function Body1<E extends ValidComponent>(props: TypographyProps<E>) {
export function Body1<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"body1"} {...props}></Typography>;
}
export function Body2<E extends ValidComponent>(props: TypographyProps<E>) {
export function Body2<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"body2"} {...props}></Typography>;
}
export function Caption<E extends ValidComponent>(props: TypographyProps<E>) {
export function Caption<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"caption"} {...props}></Typography>;
}
export function ButtonText<E extends ValidComponent>(props: TypographyProps<E>) {
export function ButtonText<E extends AnyElement>(props: TypographyProps<E>) {
return <Typography typography={"buttonText"} {...props}></Typography>;
}

View file

@ -13,6 +13,7 @@ import { Body2 } from "~material/typography.js";
import { useTimeSource } from "~platform/timesrc.js";
import { resolveCustomEmoji } from "../masto/toot.js";
import { Divider } from "@suid/material";
import cardStyle from "~material/cards.module.css";
import MediaAttachmentGrid from "./toots/MediaAttachmentGrid.jsx";
import { makeAcctText, useDefaultSession } from "../masto/clients";
import TootContent from "./toots/TootContent";
@ -24,7 +25,6 @@ import TootAuthorGroup from "./toots/TootAuthorGroup.js";
import "./RegularToot.css";
import { vibrate } from "~platform/hardware.js";
import { Transition } from "solid-transition-group";
import "~material/cards.css";
export type TootEnv = {
boost: (value: mastodon.v1.Status) => void;
@ -251,7 +251,6 @@ const RegularToot: Component<RegularTootProps> = (oprops) => {
<article
classList={{
RegularToot: true,
"card": true,
expanded: props.evaluated,
"thread-top": props.thread === "top",
"thread-mid": props.thread === "middle",
@ -263,7 +262,7 @@ const RegularToot: Component<RegularTootProps> = (oprops) => {
{...rest}
>
<Show when={!!status().reblog}>
<div class="retoot-grp card-gut card-pad">
<div class="retoot-grp">
<BoostIcon />
<Body2
innerHTML={resolveCustomEmoji(
@ -285,6 +284,7 @@ const RegularToot: Component<RegularTootProps> = (oprops) => {
source={toot().content}
emojis={toot().emojis}
mentions={toot().mentions}
class={cardStyle.cardNoPad}
sensitive={toot().sensitive}
spoilerText={toot().spoilerText}
reveal={reveal()}
@ -308,6 +308,7 @@ const RegularToot: Component<RegularTootProps> = (oprops) => {
</Show>
{props.actionable && (
<Divider
class={cardStyle.cardNoPad}
style={{ "margin-top": "8px" }}
/>
)}
@ -318,7 +319,7 @@ const RegularToot: Component<RegularTootProps> = (oprops) => {
}}
>
<Show when={props.actionable}>
<TootActionGroup value={status()} />
<TootActionGroup value={status()} class={cardStyle.cardGutSkip} />
</Show>
</Transition>
</article>

View file

@ -10,6 +10,7 @@ import RegularToot, {
findElementActionable,
TootEnvProvider,
} from "./RegularToot";
import cards from "~material/cards.module.css";
import { css } from "solid-styled";
import { createTimeSource, TimeSourceProvider } from "~platform/timesrc";
import TootComposer from "./TootComposer";
@ -176,6 +177,7 @@ const TootBottomSheet: Component = (props) => {
<TootEnvProvider value={mainTootEnv}>
<RegularToot
id={`toot-${toot()!.id}`}
class={cards.card}
style={{
"scroll-margin-top":
"calc(var(--scaffold-topbar-height) + 20px)",

View file

@ -1,6 +1,7 @@
import {
createEffect,
createMemo,
createRenderEffect,
createSignal,
Show,
type Accessor,
@ -38,13 +39,14 @@ import {
Close,
MoreVert,
} from "@suid/icons-material";
import type { Account } from "../accounts/stores";
import "./TootComposer.css";
import BottomSheet from "~material/BottomSheet";
import { useAppLocale } from "~platform/i18n";
import iso639_1 from "iso-639-1";
import ChooseTootLang from "./TootLangPicker";
import type { mastodon } from "masto";
import "~material/cards.css";
import cardStyles from "~material/cards.module.css";
import Menu, { createManagedMenuState } from "~material/Menu";
import { useDefaultSession } from "../masto/clients";
import { resolveCustomEmoji } from "../masto/toot";
@ -316,7 +318,7 @@ const TootComposer: Component<{
return (
<div
ref={props.ref}
class={/* @once */ `TootComposer card`}
class={/* @once */ `TootComposer ${cardStyles.card}`}
style={containerStyle()}
on:touchend={
cancelEvent
@ -326,7 +328,7 @@ const TootComposer: Component<{
on:wheel={cancelEvent}
>
<Show when={active()}>
<Toolbar class="card-gut">
<Toolbar class={cardStyles.cardNoPad}>
<IconButton
onClick={[setActive, false]}
aria-label="Close the composer"
@ -339,7 +341,7 @@ const TootComposer: Component<{
<MoreVert />
</IconButton>
</Toolbar>
<div class="card-gut">
<div class={cardStyles.cardNoPad}>
<Menu {...menuState}>
<MenuItem>
<ListItemAvatar>
@ -358,7 +360,7 @@ const TootComposer: Component<{
</div>
</Show>
<div class="reply-input card-gut card-pad">
<div class="reply-input">
<Show when={props.profile}>
<Avatar
src={props.profile!.avatar}
@ -404,7 +406,7 @@ const TootComposer: Component<{
</div>
<Show when={active()}>
<div class="options card-pad card-gut">
<div class="options">
<Button
startIcon={<Translate />}
endIcon={<ArrowDropDown />}
@ -428,6 +430,7 @@ const TootComposer: Component<{
</div>
<TootVisibilityPickerDialog
class={cardStyles.cardNoPad}
open={permPicker()}
onClose={() => setPermPicker(false)}
visibility={visibility()}
@ -435,6 +438,7 @@ const TootComposer: Component<{
/>
<TootLanguagePickerDialog
class={cardStyles.cardNoPad}
open={langPickerOpen()}
onClose={() => setLangPickerOpen(false)}
code={language()}

View file

@ -15,6 +15,7 @@ import RegularToot, {
findRootToot,
TootEnvProvider,
} from "./RegularToot";
import cardStyle from "~material/cards.module.css";
import type { ThreadNode } from "../masto/timelines";
import { useNavigator } from "~platform/StackedRouter";
import { ANIM_CURVE_STD } from "~material/theme";
@ -220,6 +221,7 @@ const TootList: Component<{
? positionTootInThread(index, threadLength())
: undefined
}
class={cardStyle.card}
evaluated={isExpanded(id)}
actionable={isExpanded(id)}
onClick={[onItemClick, status()]}

View file

@ -20,7 +20,7 @@ import { useStore } from "@nanostores/solid";
import { $settings } from "../../settings/stores";
import { averageColorHex } from "~platform/blurhash";
import "./MediaAttachmentGrid.css";
import "~material/cards.css";
import cardStyle from "~material/cards.module.css";
import { Preview } from "@suid/icons-material";
import { IconButton } from "@suid/material";
import Masonry from "~platform/Masonry";
@ -153,7 +153,7 @@ const MediaAttachmentGrid: Component<{
<Masonry
component="section"
ref={setRootRef}
class={`MediaAttachmentGrid card-gut`}
class={`MediaAttachmentGrid ${cardStyle.cardNoPad}`}
classList={{
sensitive: props.sensitive,
}}

View file

@ -75,7 +75,7 @@ export function PreviewCard(props: {
return (
<a
ref={root!}
class={"PreviewCard card-pad card-gut"}
class={"PreviewCard"}
href={props.src.url}
target="_blank"
referrerPolicy="unsafe-url"

View file

@ -19,7 +19,7 @@ function TootAuthorGroup(
const { dateFn: dateFnLocale } = useAppLocale();
return (
<div class="TootAuthorGroup card-gut card-pad" {...rest}>
<div class="TootAuthorGroup" {...rest}>
<Img src={toot().account.avatar} class="avatar" />
<div class="name-grp">
<div class="name-primary">

View file

@ -75,7 +75,7 @@ const TootContent: Component<TootContentProps> = (oprops) => {
}
});
}}
class={`TootContent card-gut card-pad ${props.class || ""}`}
class={`TootContent ${props.class || ""}`}
{...rest}
>
<Show when={props.sensitive}>