initial commit
This commit is contained in:
commit
5449e361d5
46 changed files with 8309 additions and 0 deletions
21
src/material/Button.tsx
Normal file
21
src/material/Button.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { Component, JSX, splitProps } from "solid-js";
|
||||
import materialStyles from "./material.module.css";
|
||||
|
||||
/**
|
||||
* Material-styled button.
|
||||
*
|
||||
* @param type Same as `<button>`'s type property, the default is 'button'
|
||||
*/
|
||||
const Button: Component<JSX.ButtonHTMLAttributes<HTMLButtonElement>> = (
|
||||
props,
|
||||
) => {
|
||||
const [managed, passthough] = splitProps(props, ["class", 'type']);
|
||||
const classes = () =>
|
||||
managed.class
|
||||
? [materialStyles.button, managed.class].join(" ")
|
||||
: materialStyles.button;
|
||||
const type = () => managed.type ?? 'button'
|
||||
return <button type={type()} class={classes()} {...passthough}></button>;
|
||||
};
|
||||
|
||||
export default Button;
|
121
src/material/Img.tsx
Normal file
121
src/material/Img.tsx
Normal file
|
@ -0,0 +1,121 @@
|
|||
import {
|
||||
JSX,
|
||||
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;
|
||||
keepBlur?: boolean;
|
||||
} & JSX.HTMLElementTags["img"];
|
||||
|
||||
const Img: Component<ImgProps> = (props) => {
|
||||
let canvas: HTMLCanvasElement;
|
||||
let imgE: HTMLImageElement;
|
||||
const [managed, passthough] = splitProps(props, [
|
||||
"blurhash",
|
||||
"keepBlur",
|
||||
"class",
|
||||
"style",
|
||||
]);
|
||||
const [isImgLoaded, setIsImgLoaded] = createSignal(false);
|
||||
const [imgSize, setImgSize] = createSignal<{
|
||||
width: number;
|
||||
height: number;
|
||||
}>();
|
||||
|
||||
const isBlurEnabled = () => managed.keepBlur || !isImgLoaded();
|
||||
|
||||
css`
|
||||
:where(.img-root) {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
> img:first-of-type {
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
visibility: ${isBlurEnabled() ? "hidden" : "visible"};
|
||||
}
|
||||
}
|
||||
|
||||
:where(.cover) {
|
||||
display: ${isBlurEnabled() ? "block" : "none"};
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: ${`${imgSize()?.height ?? 0}px`};
|
||||
width: ${`${imgSize()?.width ?? 0}px`};
|
||||
}
|
||||
`;
|
||||
|
||||
const onImgLoaded = () => {
|
||||
setIsImgLoaded(true);
|
||||
setImgSize({
|
||||
width: imgE.width,
|
||||
height: imgE.height,
|
||||
});
|
||||
};
|
||||
|
||||
const onMetadataLoaded = () => {
|
||||
setImgSize({
|
||||
width: imgE.width,
|
||||
height: imgE.height,
|
||||
});
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
setImgSize((x) => {
|
||||
const parent = imgE.parentElement;
|
||||
if (!parent) return x;
|
||||
return x
|
||||
? x
|
||||
: {
|
||||
width: parent.clientWidth,
|
||||
height: parent.clientHeight,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div class={mergeClass(managed.class, "img-root")} style={managed.style}>
|
||||
<Show when={managed.blurhash}>
|
||||
<canvas
|
||||
ref={(canvas) => {
|
||||
createRenderEffect(() => {
|
||||
if (!managed.blurhash) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
const size = imgSize();
|
||||
if (!size) return;
|
||||
const imgd = ctx?.createImageData(size.width, size.height);
|
||||
const pixels = decode(managed.blurhash, size.width, size.height);
|
||||
imgd.data.set(pixels);
|
||||
ctx.putImageData(imgd, 0, 0);
|
||||
});
|
||||
}}
|
||||
class="cover"
|
||||
role="presentation"
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<img
|
||||
ref={imgE!}
|
||||
{...passthough}
|
||||
onLoad={onImgLoaded}
|
||||
onLoadedMetadata={onMetadataLoaded}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Img;
|
58
src/material/Scaffold.tsx
Normal file
58
src/material/Scaffold.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { createElementSize } from "@solid-primitives/resize-observer";
|
||||
import {
|
||||
Show,
|
||||
createRenderEffect,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
type JSX,
|
||||
type ParentComponent,
|
||||
} from "solid-js";
|
||||
import { css } from "solid-styled";
|
||||
|
||||
interface ScaffoldProps {
|
||||
topbar?: JSX.Element;
|
||||
fab?: JSX.Element;
|
||||
}
|
||||
|
||||
const Scaffold: ParentComponent<ScaffoldProps> = (props) => {
|
||||
const [topbarElement, setTopbarElement] = createSignal<HTMLElement>();
|
||||
|
||||
const topbarSize = createElementSize(topbarElement);
|
||||
|
||||
css`
|
||||
.scaffold-content {
|
||||
--scaffold-topbar-height: ${(topbarSize.height?.toString() ?? 0) + "px"};
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
z-index: var(--tutu-zidx-nav, auto);
|
||||
}
|
||||
|
||||
.fab-dock {
|
||||
position: fixed;
|
||||
bottom: 40px;
|
||||
right: 40px;
|
||||
z-index: var(--tutu-zidx-nav, auto);
|
||||
}
|
||||
`;
|
||||
return (
|
||||
<>
|
||||
<Show when={props.topbar}>
|
||||
<div class="topbar" ref={setTopbarElement}>
|
||||
{props.topbar}
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.fab}>
|
||||
<div class="fab-dock">{props.fab}</div>
|
||||
</Show>
|
||||
<div class="scaffold-content">{props.children}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Scaffold;
|
80
src/material/Tab.tsx
Normal file
80
src/material/Tab.tsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
import {
|
||||
Component,
|
||||
createEffect,
|
||||
splitProps,
|
||||
type JSX,
|
||||
type ParentComponent,
|
||||
} from "solid-js";
|
||||
import { css } from "solid-styled";
|
||||
import { useTabListContext } from "./Tabs";
|
||||
|
||||
const Tab: ParentComponent<
|
||||
{
|
||||
focus?: boolean;
|
||||
large?: boolean;
|
||||
} & JSX.ButtonHTMLAttributes<HTMLButtonElement>
|
||||
> = (props) => {
|
||||
const [managed, rest] = splitProps(props, [
|
||||
"focus",
|
||||
"large",
|
||||
"type",
|
||||
"role",
|
||||
"ref",
|
||||
]);
|
||||
let self: HTMLButtonElement;
|
||||
const {
|
||||
focusOn: [, setFocusOn],
|
||||
} = useTabListContext();
|
||||
|
||||
createEffect<boolean | undefined>((lastStatus) => {
|
||||
if (managed.focus && !lastStatus) {
|
||||
setFocusOn((x) => [...x, self]);
|
||||
}
|
||||
if (!managed.focus && lastStatus) {
|
||||
setFocusOn((x) => x.filter((e) => e !== self));
|
||||
}
|
||||
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 (
|
||||
<button
|
||||
ref={(x) => {
|
||||
self = x;
|
||||
(managed.ref as (e: HTMLButtonElement) => void)?.(x);
|
||||
}}
|
||||
type={managed.type ?? "button"}
|
||||
classList={{ tab: true, focus: managed.focus }}
|
||||
role={managed.role ?? "tab"}
|
||||
{...rest}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tab;
|
165
src/material/Tabs.tsx
Normal file
165
src/material/Tabs.tsx
Normal file
|
@ -0,0 +1,165 @@
|
|||
import {
|
||||
ParentComponent,
|
||||
createContext,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createRenderEffect,
|
||||
createSignal,
|
||||
useContext,
|
||||
type Signal,
|
||||
} from "solid-js";
|
||||
import { css } from "solid-styled";
|
||||
|
||||
const TabListContext = /* @__PURE__ */ createContext<{
|
||||
focusOn: Signal<HTMLElement[]>;
|
||||
}>();
|
||||
|
||||
export function useTabListContext() {
|
||||
const result = useContext(TabListContext);
|
||||
if (!result) {
|
||||
throw new TypeError("tab list context is not found");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const ANIM_SPEED = 160 / 110; // 160px/110ms
|
||||
|
||||
const TABLIST_FOCUS_CLASS = "tablist-focus";
|
||||
|
||||
const Tabs: ParentComponent<{
|
||||
offset?: number;
|
||||
onFocusChanged?: (element: HTMLElement[]) => void;
|
||||
}> = (props) => {
|
||||
let self: HTMLDivElement;
|
||||
const [focusOn, setFocusOn] = createSignal<HTMLElement[]>([]);
|
||||
|
||||
createRenderEffect<HTMLElement[] | undefined>((lastFocusElement) => {
|
||||
const current = focusOn();
|
||||
if (lastFocusElement) {
|
||||
for (const e of lastFocusElement) {
|
||||
e.classList.remove(TABLIST_FOCUS_CLASS);
|
||||
}
|
||||
}
|
||||
for (const e of current) {
|
||||
e.classList.add("tablist-focus");
|
||||
}
|
||||
return current;
|
||||
});
|
||||
|
||||
createRenderEffect(() => {
|
||||
const callback = props.onFocusChanged;
|
||||
if (!callback) return;
|
||||
callback(focusOn());
|
||||
});
|
||||
|
||||
let lastLeft = 0;
|
||||
let lastWidth = 0;
|
||||
|
||||
const getNearestDistance = (
|
||||
srcRect: { x: number; width: number },
|
||||
prevEl: Element | null,
|
||||
nextEl: Element | null,
|
||||
offset?: number,
|
||||
) => {
|
||||
if (!offset || offset === 0) return [0, 0] as const;
|
||||
if (offset > 0) {
|
||||
if (!nextEl) return [0, 0] as const;
|
||||
const rect = nextEl.getBoundingClientRect();
|
||||
return [
|
||||
(rect.x - srcRect.x) * offset,
|
||||
(rect.width - srcRect.width) * offset,
|
||||
] as const;
|
||||
} else {
|
||||
if (!prevEl) return [0, 0] as const;
|
||||
const rect = prevEl.getBoundingClientRect();
|
||||
return [
|
||||
(rect.x - srcRect.x) * offset,
|
||||
(srcRect.width - rect.width) * offset,
|
||||
] as const;
|
||||
}
|
||||
};
|
||||
|
||||
const focusBoundingClientRect = () => {
|
||||
return focusOn()
|
||||
.map((x) => x.getBoundingClientRect())
|
||||
.reduce(
|
||||
(p, c) => {
|
||||
return {
|
||||
x: Math.min(p.x, c.x),
|
||||
width: p.width + c.width,
|
||||
};
|
||||
},
|
||||
{ x: +Infinity, width: 0 },
|
||||
);
|
||||
};
|
||||
|
||||
const focusSiblings = () => {
|
||||
const rects = focusOn().map((x) => [x, x.getBoundingClientRect()] as const);
|
||||
if (rects.length === 0) return [null, null] as const;
|
||||
rects.sort(([, rect1], [, rect2]) => rect1.x - rect2.x);
|
||||
return [
|
||||
rects[0][0].previousElementSibling,
|
||||
rects[rects.length - 1][0].nextElementSibling,
|
||||
] as const;
|
||||
};
|
||||
|
||||
const indicator = () => {
|
||||
const el = focusOn();
|
||||
if (!el) {
|
||||
return ["0px", "0px", "110ms", "110ms"] as const;
|
||||
}
|
||||
const rect = focusBoundingClientRect();
|
||||
const rootRect = self.getBoundingClientRect();
|
||||
const left = rect.x - rootRect.x;
|
||||
const width = rect.width;
|
||||
const [prevEl, nextEl] = focusSiblings();
|
||||
const [offset, widthChange] = getNearestDistance(
|
||||
rect,
|
||||
prevEl,
|
||||
nextEl,
|
||||
props.offset,
|
||||
);
|
||||
const result = [
|
||||
`${left + offset}px`,
|
||||
`${width + widthChange}px`,
|
||||
`${Math.max(Math.floor(Math.abs(left + offset - lastLeft)), 160) * ANIM_SPEED}ms`,
|
||||
`${Math.max(Math.floor(Math.abs(width - lastWidth)), 160) * ANIM_SPEED}ms`,
|
||||
] as const;
|
||||
lastLeft = left;
|
||||
lastWidth = width;
|
||||
return result;
|
||||
};
|
||||
|
||||
css`
|
||||
.tablist {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
|
||||
&::after {
|
||||
transition:
|
||||
left ${indicator()[2]} var(--tutu-anim-curve-std),
|
||||
width ${indicator()[3]} var(--tutu-anim-curve-std);
|
||||
position: absolute;
|
||||
content: "";
|
||||
display: block;
|
||||
background-color: white;
|
||||
height: 2px;
|
||||
width: ${indicator()[1]};
|
||||
left: ${indicator()[0]};
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<TabListContext.Provider value={{ focusOn: [focusOn, setFocusOn] }}>
|
||||
<div ref={self!} class="tablist" role="tablist">
|
||||
{props.children}
|
||||
</div>
|
||||
</TabListContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tabs;
|
80
src/material/TextField.tsx
Normal file
80
src/material/TextField.tsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
import {
|
||||
Component,
|
||||
createEffect,
|
||||
createSignal,
|
||||
createUniqueId,
|
||||
onMount,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
import formStyles from "./form.module.css";
|
||||
|
||||
export type TextFieldProps = {
|
||||
label?: string;
|
||||
helperText?: string;
|
||||
type?: "text" | "password";
|
||||
onChange?: (value: string) => void;
|
||||
onInput?: (value: string) => void;
|
||||
inputId?: string;
|
||||
error?: boolean;
|
||||
required?: boolean;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
const TextField: Component<TextFieldProps> = (props) => {
|
||||
let input: HTMLInputElement;
|
||||
let field: HTMLDivElement;
|
||||
const [hasContent, setHasContent] = createSignal(false);
|
||||
const altInputId = createUniqueId();
|
||||
|
||||
createEffect(() => {
|
||||
if (hasContent()) {
|
||||
field.classList.add("float-label");
|
||||
} else {
|
||||
field.classList.remove("float-label");
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
setHasContent(input.value.length > 0);
|
||||
});
|
||||
|
||||
const onInputChange = (e: { currentTarget: HTMLInputElement }) => {
|
||||
const value = (e.currentTarget as HTMLInputElement).value;
|
||||
setHasContent(value.length > 0);
|
||||
props.onInput?.(value);
|
||||
};
|
||||
|
||||
const inputId = () => props.inputId ?? altInputId;
|
||||
|
||||
const fieldClass = () => {
|
||||
const cls = [formStyles.textfield];
|
||||
if (typeof props.helperText !== "undefined") {
|
||||
cls.push(formStyles.withHelperText);
|
||||
}
|
||||
if (props.error) {
|
||||
cls.push(formStyles.error);
|
||||
}
|
||||
return cls.join(" ");
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={field!} class={fieldClass()}>
|
||||
<label for={inputId()}>{props.label}</label>
|
||||
<input
|
||||
ref={input!}
|
||||
id={inputId()}
|
||||
type={props.type ?? "text"}
|
||||
onInput={onInputChange}
|
||||
onChange={(e) => props.onChange?.(e.currentTarget.value)}
|
||||
placeholder=""
|
||||
required={props.required}
|
||||
name={props.name}
|
||||
/>
|
||||
<Show when={typeof props.helperText !== "undefined"}>
|
||||
<span class={formStyles.helperText}>{props.helperText}</span>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextField;
|
55
src/material/cards.module.css
Normal file
55
src/material/cards.module.css
Normal file
|
@ -0,0 +1,55 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
}
|
80
src/material/form.module.css
Normal file
80
src/material/form.module.css
Normal file
|
@ -0,0 +1,80 @@
|
|||
.textfield {
|
||||
composes: touchTarget from 'material.module.css';
|
||||
|
||||
--border-color: var(--tutu-color-inactive-on-surface);
|
||||
--active-border-color: var(--tutu-color-primary);
|
||||
--label-color: var(--tutu-color-inactive-on-surface);
|
||||
--active-label-color: var(--tutu-color-primary);
|
||||
--helper-text-color: var(--tutu-color-inactive-on-surface);
|
||||
|
||||
&>* {
|
||||
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);
|
||||
}
|
||||
|
||||
&: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: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);
|
||||
}
|
||||
|
||||
&>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;
|
||||
|
||||
&:focus {
|
||||
border-bottom: 2px solid var(--active-border-color);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.withHelperText {
|
||||
--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;
|
||||
}
|
||||
}
|
65
src/material/material.module.css
Normal file
65
src/material/material.module.css
Normal file
|
@ -0,0 +1,65 @@
|
|||
.surface {
|
||||
background-color: var(--tutu-color-surface);
|
||||
color: var(--tutu-color-on-surface);
|
||||
}
|
||||
|
||||
.touchTarget {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button {
|
||||
composes: buttonText from './typography.module.css';
|
||||
composes: touchTarget;
|
||||
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: var(--tutu-color-primary);
|
||||
font-family: inherit;
|
||||
|
||||
&:focus,&:hover,&:focus-visible {
|
||||
background-color: var(--tutu-color-surface-dd);
|
||||
}
|
||||
|
||||
&.pressed {
|
||||
background-color: var(--tutu-color-surface-d);
|
||||
}
|
||||
|
||||
&.raised {
|
||||
background-color: var(--tutu-color-primary);
|
||||
color: var(--tutu-color-on-primary);
|
||||
}
|
||||
|
||||
&:disabled, &[aria-disabled]:not([aria-disabled="false"]) {
|
||||
color: #9e9e9e;
|
||||
|
||||
&:focus,&:hover,&:focus-visible {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar &, .appbar & {
|
||||
height: 100%;
|
||||
margin-block: 0;
|
||||
padding-block: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.appbar & {
|
||||
color: var(--tutu-color-on-primary);
|
||||
|
||||
&:focus,&:hover,&:focus-visible {
|
||||
background-color: var(--tutu-color-primary-ll);
|
||||
}
|
||||
|
||||
&.pressed {
|
||||
background-color: var(--tutu-color-primary-l);
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar & {
|
||||
color: var(--tutu-color-on-surface);
|
||||
}
|
||||
}
|
||||
|
16
src/material/mui.ts
Normal file
16
src/material/mui.ts
Normal file
|
@ -0,0 +1,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]
|
||||
},
|
||||
secondary: {
|
||||
main: amber.A200
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
135
src/material/theme.css
Normal file
135
src/material/theme.css
Normal file
|
@ -0,0 +1,135 @@
|
|||
:root,
|
||||
[lang^="en"], [lang="en"] {
|
||||
--md-typography-type: "regular";
|
||||
--title-size: 1.25rem;
|
||||
--title-weight: 500;
|
||||
--subheading-size: 1.125rem;
|
||||
--body-size: 1rem;
|
||||
--body2-weight: 500;
|
||||
--caption-size: 0.875rem;
|
||||
--button-size: 1rem;
|
||||
--button-weight: 500;
|
||||
--button-text-transform: uppercase;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
& {
|
||||
--subheading-size: 1.0625rem;
|
||||
--body-size: 0.9375rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[lang^="zh"], [lang="zh"],
|
||||
[lang^="kr"], [lang="kr"],
|
||||
[lang^="ja"], [lang="ja"] {
|
||||
--md-typography-type: "dense";
|
||||
--title-size: 1.4375rem;
|
||||
--subheading-size: 1.1875rem;
|
||||
--body-size: 1.0625rem;
|
||||
--caption-size: 0.9375rem;
|
||||
--button-size: 1.0625rem;
|
||||
--button-text-transform: none;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
& {
|
||||
--subheading-size: 1.125rem;
|
||||
--body-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--tutu-color-primary: #673ab7;
|
||||
/* Deep Purple 500 */
|
||||
--tutu-color-on-primary: white;
|
||||
--tutu-color-primary-d: #512da8;
|
||||
/* 700 */
|
||||
--tutu-color-on-primary-d: white;
|
||||
--tutu-color-primary-dd: #4527a0;
|
||||
/* 800 */
|
||||
--tutu-color-on-primary-dd: white;
|
||||
--tutu-color-primary-l: #9575cd;
|
||||
/* 200 */
|
||||
--tutu-color-on-primary-l: white;
|
||||
--tutu-color-primary-ll: #b39ddb;
|
||||
/* 100 */
|
||||
--tutu-color-on-primary-ll: black;
|
||||
|
||||
--tutu-color-secondary: #ffd740;
|
||||
/* Amber A200 */
|
||||
--tutu-color-on-secondary: black;
|
||||
|
||||
--tutu-color-surface-l: white;
|
||||
--tutu-color-surface: #fafafa;
|
||||
--tutu-color-surface-d: #99999928;
|
||||
--tutu-color-surface-dd: #99999920;
|
||||
--tutu-color-on-surface: black;
|
||||
--tutu-color-secondary-text-on-surface: rgba(0, 0, 0, 0.5);
|
||||
--tutu-color-error-on-surface: #d32f2f;
|
||||
--tutu-color-inactive-on-surface: #757575;
|
||||
|
||||
--tutu-shadow-e1: 0px 1px 2px 0px #9e9e9e;
|
||||
/* Switch */
|
||||
--tutu-shadow-e2: 0px 2px 4px 0px #9e9e9e;
|
||||
/* (Resting) cards, raised button, quick entry / search bar */
|
||||
--tutu-shadow-e3: 0px 3px 6px 0px #9e9e9e;
|
||||
/* Refresh indicator, quick entry / search bar (scrolled) */
|
||||
--tutu-shadow-e4: 0px 4px 8px 0px #9e9e9e;
|
||||
/* App bar */
|
||||
--tutu-shadow-e6: 0px 6px 12px 0px #9e9e9e;
|
||||
/* Snack bar, FAB (resting) */
|
||||
--tutu-shadow-e8: 0px 8px 16px 0px #9e9e9e;
|
||||
/* Menu, (picked-up) cards, (pressed) raise button */
|
||||
--tutu-shadow-e9: 0px 9px 18px 0px #9e9e9e;
|
||||
/* Submenu (+1dp for each submenu) */
|
||||
--tutu-shadow-e12: 0px 12px 24px 0px #9e9e9e;
|
||||
/* (pressed) FAB */
|
||||
--tutu-shadow-e16: 0px 16px 32px 0px #9e9e9e;
|
||||
/* Nav drawer, right drawer, modal bottom sheet */
|
||||
--tutu-shadow-e24: 0px 24px 48px 0px #9e9e9e;
|
||||
/* Dialog, picker */
|
||||
|
||||
--tutu-anim-curve-std: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--tutu-anim-curve-deceleration: cubic-bezier(0, 0, 0.2, 1);
|
||||
--tutu-anim-curve-aceleration: cubic-bezier(0.4, 0, 1, 1);
|
||||
--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);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
|
||||
/* Mobile */
|
||||
& {
|
||||
--tutu-transition-shadow: box-shadow 225ms var(--tutu-anim-curve-std);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
|
||||
/* Tablet */
|
||||
& {
|
||||
--tutu-transition-shadow: box-shadow 292.5ms var(--tutu-anim-curve-std);
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop */
|
||||
--tutu-transition-shadow: box-shadow 175ms var(--tutu-anim-curve-std);
|
||||
|
||||
--tutu-zidx-nav: 1100;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: Roboto, "Noto Sans", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: var(--body-size, 1rem);
|
||||
}
|
0
src/material/toolbar.module.css
Normal file
0
src/material/toolbar.module.css
Normal file
48
src/material/typography.module.css
Normal file
48
src/material/typography.module.css
Normal file
|
@ -0,0 +1,48 @@
|
|||
.display4 {
|
||||
font-size: 7rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.display3 {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
|
||||
.display2 {
|
||||
font-size: 2.8125rem;
|
||||
}
|
||||
|
||||
.display1 {
|
||||
font-size: 2.125rem;
|
||||
}
|
||||
|
||||
.headline {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--title-size);
|
||||
font-weight: var(--title-weight);
|
||||
}
|
||||
|
||||
.subheading {
|
||||
font-size: var(--subheading-size);
|
||||
}
|
||||
|
||||
.body1 {
|
||||
font-size: var(--body-size);
|
||||
}
|
||||
|
||||
.body2 {
|
||||
composes: body1;
|
||||
font-weight: var(--body2-weight);
|
||||
}
|
||||
|
||||
.caption {
|
||||
font-size: var(--caption-size);
|
||||
}
|
||||
|
||||
.buttonText {
|
||||
font-weight: var(--button-weight);
|
||||
font-size: var(--button-size);
|
||||
text-transform: var(--button-text-transform);
|
||||
}
|
87
src/material/typography.tsx
Normal file
87
src/material/typography.tsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { JSX, ParentComponent, splitProps, type Ref } from "solid-js";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
import typography from "./typography.module.css";
|
||||
import { mergeClass } from "../utils";
|
||||
|
||||
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;
|
||||
} & PropsOf<E>;
|
||||
|
||||
type TypographyKind =
|
||||
| "display4"
|
||||
| "display3"
|
||||
| "display2"
|
||||
| "display1"
|
||||
| "headline"
|
||||
| "title"
|
||||
| "subheading"
|
||||
| "body1"
|
||||
| "body2"
|
||||
| "caption"
|
||||
| "buttonText";
|
||||
|
||||
export function Typography<T extends AnyElement>(props: {typography: TypographyKind } & TypographyProps<T>) {
|
||||
const [managed, passthough] = splitProps(props, [
|
||||
"ref",
|
||||
"component",
|
||||
"class",
|
||||
"typography",
|
||||
]);
|
||||
const classes = () =>
|
||||
mergeClass(managed.class, typography[managed.typography]);
|
||||
return (
|
||||
<Dynamic
|
||||
ref={managed.ref}
|
||||
component={managed.component ?? "span"}
|
||||
class={classes()}
|
||||
{...passthough}
|
||||
></Dynamic>
|
||||
);
|
||||
};
|
||||
|
||||
export function Display4<E extends AnyElement>(props: TypographyProps<E>) {
|
||||
return <Typography typography={"display4"} {...props}></Typography>
|
||||
}
|
||||
export function Display3<E extends AnyElement>(props: TypographyProps<E>) {
|
||||
return <Typography typography={"display3"} {...props}></Typography>
|
||||
}
|
||||
export function Display2<E extends AnyElement>(props: TypographyProps<E>) {
|
||||
return <Typography typography={"display2"} {...props}></Typography>
|
||||
}
|
||||
export function Display1<E extends AnyElement>(props: TypographyProps<E>) {
|
||||
return <Typography typography={"display1"} {...props}></Typography>
|
||||
}
|
||||
export function Headline<E extends AnyElement>(props: TypographyProps<E>) {
|
||||
return <Typography typography={"headline"} {...props}></Typography>
|
||||
}
|
||||
export function Title<E extends AnyElement>(props: TypographyProps<E>) {
|
||||
return <Typography typography={"title"} {...props}></Typography>
|
||||
}
|
||||
export function Subheading<E extends AnyElement>(props: TypographyProps<E>) {
|
||||
return <Typography typography={"subheading"} {...props}></Typography>
|
||||
}
|
||||
export function Body1<E extends AnyElement>(props: TypographyProps<E>) {
|
||||
return <Typography typography={"body1"} {...props}></Typography>
|
||||
}
|
||||
export function Body2<E extends AnyElement>(props: TypographyProps<E>) {
|
||||
return <Typography typography={"body2"} {...props}></Typography>
|
||||
}
|
||||
export function Caption<E extends AnyElement>(props: TypographyProps<E>) {
|
||||
return <Typography typography={"caption"} {...props}></Typography>
|
||||
}
|
||||
export function ButtonText<E extends AnyElement>(props: TypographyProps<E>) {
|
||||
return <Typography typography={"buttonText"} {...props}></Typography>
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue