Compare commits

...

2 commits

Author SHA1 Message Date
thislight
4718239723
fix #32: remove rest css modules
All checks were successful
/ depoly (push) Successful in 1m28s
2025-01-16 22:06:20 +08:00
thislight
8854a3b86a
devnotes: add section for managing css 2025-01-16 20:39:41 +08:00
23 changed files with 234 additions and 163 deletions

View file

@ -84,3 +84,64 @@ 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,41 +1,46 @@
@import "normalize.css/normalize.css";
@import "./material/theme.css";
@layer compat, theme, 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);
}
@import "normalize.css/normalize.css" layer(compat);
@import "./material/theme.css" layer(theme);
@import "./material/material.css" layer(material);
/*
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;
@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);
}
#root {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
/*
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;
}
}
.custom-emoji {
width: 1em;
}
h1 {
margin: 0;
}
* {
user-select: none;
}
}

View file

@ -0,0 +1,22 @@
.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 cards from "~material/cards.module.css";
import "~material/cards.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>
<div class={cards.layoutCentered}>
<div class={cards.card} aria-busy="true" aria-describedby={progressId}>
<DocumentTitle>Back from {siteTitle()}</DocumentTitle>
<main class="MastodonOAuth2Callback">
<div class="card card-auto-margin" aria-busy="true" aria-describedby={progressId}>
<LinearProgress
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
class="card-no-pad card-gut-skip"
id={progressId}
aria-labelledby={titleId}
/>
@ -114,7 +114,7 @@ const MastodonOAuth2Callback: Component = () => {
src={siteImg()?.src}
srcset={siteImg()?.srcset}
blurhash={siteImg()?.blurhash}
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
class="card-no-pad card-gut-skip"
alt={`Banner image for ${siteTitle()}`}
style={{ height: "235px", display: "block" }}
/>
@ -128,7 +128,7 @@ const MastodonOAuth2Callback: Component = () => {
again.
</p>
</div>
</div>
</main>
</>
);
};

View file

@ -6,7 +6,7 @@ import {
createUniqueId,
onMount,
} from "solid-js";
import cards from "~material/cards.module.css";
import "~material/cards.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={cards.card} style={{ "margin-bottom": "20px" }}>
<div class="card card-auto-margin" 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={`${cards.card} key-content`}
class="card card-auto-margin key-content"
aria-busy={currentState() !== "inactive" ? "true" : "false"}
aria-describedby={
currentState() !== "inactive" ? progressId : undefined
@ -135,7 +135,7 @@ const SignIn: Component = () => {
}}
>
<LinearProgress
class={[cards.cardNoPad, cards.cardGutSkip].join(" ")}
class={"card-no-pad card-gut-skip"}
id={progressId}
sx={currentState() === "inactive" ? { display: "none" } : undefined}
/>

View file

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

View file

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

View file

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

View file

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

56
src/material/cards.css Normal file
View file

@ -0,0 +1,56 @@
@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

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

View file

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

View file

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

View file

@ -13,7 +13,6 @@ 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";
@ -25,6 +24,7 @@ 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,6 +251,7 @@ const RegularToot: Component<RegularTootProps> = (oprops) => {
<article
classList={{
RegularToot: true,
"card": true,
expanded: props.evaluated,
"thread-top": props.thread === "top",
"thread-mid": props.thread === "middle",
@ -262,7 +263,7 @@ const RegularToot: Component<RegularTootProps> = (oprops) => {
{...rest}
>
<Show when={!!status().reblog}>
<div class="retoot-grp">
<div class="retoot-grp card-gut card-pad">
<BoostIcon />
<Body2
innerHTML={resolveCustomEmoji(
@ -284,7 +285,6 @@ 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,7 +308,6 @@ const RegularToot: Component<RegularTootProps> = (oprops) => {
</Show>
{props.actionable && (
<Divider
class={cardStyle.cardNoPad}
style={{ "margin-top": "8px" }}
/>
)}
@ -319,7 +318,7 @@ const RegularToot: Component<RegularTootProps> = (oprops) => {
}}
>
<Show when={props.actionable}>
<TootActionGroup value={status()} class={cardStyle.cardGutSkip} />
<TootActionGroup value={status()} />
</Show>
</Transition>
</article>

View file

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

View file

@ -15,7 +15,6 @@ 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";
@ -221,7 +220,6 @@ 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 cardStyle from "~material/cards.module.css";
import "~material/cards.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 ${cardStyle.cardNoPad}`}
class={`MediaAttachmentGrid card-gut`}
classList={{
sensitive: props.sensitive,
}}

View file

@ -75,7 +75,7 @@ export function PreviewCard(props: {
return (
<a
ref={root!}
class={"PreviewCard"}
class={"PreviewCard card-pad card-gut"}
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" {...rest}>
<div class="TootAuthorGroup card-gut card-pad" {...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 ${props.class || ""}`}
class={`TootContent card-gut card-pad ${props.class || ""}`}
{...rest}
>
<Show when={props.sensitive}>