Compare commits

..

No commits in common. "7205fa5775f40bc0286c5077fc562597519dce0f" and "e174b7aafdb20f8cbdbf65a606acb54bd6a7b50e" have entirely different histories.

7 changed files with 76 additions and 150 deletions

View file

@ -8,8 +8,7 @@
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0", "dev": "vite --host 0.0.0.0",
"preview": "vite preview", "preview": "vite preview",
"dist": "vite build", "dist": "vite build"
"count-source-lines": "exec scripts/src-lc.sh"
}, },
"keywords": [], "keywords": [],
"author": "Rubicon", "author": "Rubicon",

View file

@ -1,10 +0,0 @@
#!/bin/sh
# Count the source lines.
find . '(' ! -path "./node_modules/**" ')' \
-and '(' ! -path "./.git/**" ')' \
-and '(' ! -path "./*dist/**" ')' \
-and '(' ! -path "./bun.lockb" ')' \
-and '(' ! -path "./docs/**" ')' \
-type f -print0 \
| wc -l --files0-from=-

View file

@ -7,7 +7,6 @@
width: max-content; width: max-content;
box-shadow: var(--tutu-shadow-e8); box-shadow: var(--tutu-shadow-e8);
contain: content; contain: content;
overscroll-behavior: contain;
&.e1 { &.e1 {
box-shadow: var(--tutu-shadow-e9); box-shadow: var(--tutu-shadow-e9);

View file

@ -1,7 +1,6 @@
import { useWindowSize } from "@solid-primitives/resize-observer"; import { useWindowSize } from "@solid-primitives/resize-observer";
import { MenuList } from "@suid/material"; import { MenuList } from "@suid/material";
import { import {
batch,
createEffect, createEffect,
createSignal, createSignal,
splitProps, splitProps,
@ -66,39 +65,11 @@ export function createManagedMenuState() {
return !!anchor(); return !!anchor();
}, },
anchor: anchor as () => Anchor, anchor: anchor as () => Anchor,
onClose: (event: Event) => { onClose: () => setAnchor(),
event.preventDefault();
return setAnchor();
},
}, },
] as const; ] as const;
} }
function animateGrowFromTopLeft(
element: HTMLElement,
opts?: Omit<KeyframeAnimationOptions, "duration">,
) {
const rend = element.getBoundingClientRect();
const overflow = element.style.overflow;
element.style.overflow = "hidden";
const duration = (rend.height / 1600) * 1000;
const animation = element.animate(
{
height: [`${rend.height / 2}px`, `${rend.height}px`],
width: [`${(rend.width / 4) * 3}px`, `${rend.width}px`],
},
{
duration,
...opts,
},
);
animation.addEventListener(
"finish",
() => (element.style.overflow = overflow),
);
return animation;
}
/** /**
* Material Menu Component. This component is * Material Menu Component. This component is
* implemented with dialog and {@link MenuList} from SUID. * implemented with dialog and {@link MenuList} from SUID.
@ -107,16 +78,10 @@ function animateGrowFromTopLeft(
* - Use {@link createManagedMenuState} and you don't need to manage the open and close. * - Use {@link createManagedMenuState} and you don't need to manage the open and close.
* - Use {@link MenuItem} from SUID as children. * - Use {@link MenuItem} from SUID as children.
*/ */
const Menu: Component<MenuProps> = (oprops) => { const Menu: Component<MenuProps> = (props) => {
let root: HTMLDialogElement; let root: HTMLDialogElement;
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const [props, rest] = splitProps(oprops, [ const [, rest] = splitProps(props, ["open", "onClose", "anchor"]);
"open",
"onClose",
"anchor",
"MenuListProps",
"children",
]);
const [anchorPos, setAnchorPos] = createSignal<{ const [anchorPos, setAnchorPos] = createSignal<{
left?: number; left?: number;
@ -141,30 +106,16 @@ const Menu: Component<MenuProps> = (oprops) => {
let openAnimationOrigin: "lt" | "rt" = "lt"; let openAnimationOrigin: "lt" | "rt" = "lt";
const animateOpen = () => { createEffect(() => {
if (props.open) {
const a = props.anchor(); const a = props.anchor();
const { width } = windowSize;
const { left, top, right, e } = a;
const isOpened = root.open;
// There are incomplete animations.
// For `getBoundingClientRect()`, WebKit reports the initial state
// of the element, whilst Firefox reports the final state.
//
// We skip if animations are still on the element
// to avoid the problem on WebKit.
// Here use the final state.
//
// This is a dirty workaround. It's here because the feature is still
// works with it.
// I am curious that why the ones on the other parts are works. (Rubicon)
if (root.getAnimations().length > 0) {
return;
}
if (!root.open) {
root.showModal(); root.showModal();
const rend = root.getBoundingClientRect(); const rend = root.getBoundingClientRect();
const { width } = windowSize;
const { left, top, right, e } = a;
if (left > width / 2) { if (left > width / 2) {
openAnimationOrigin = "rt"; openAnimationOrigin = "rt";
setAnchorPos({ setAnchorPos({
@ -172,26 +123,34 @@ const Menu: Component<MenuProps> = (oprops) => {
top, top,
e, e,
}); });
animateGrowFromTopRight(root, { easing: ANIM_CURVE_STD });
} else { } else {
openAnimationOrigin = "lt"; openAnimationOrigin = "lt";
setAnchorPos({ left, top, e }); setAnchorPos({ left, top, e });
}
if (!isOpened) { const overflow = root.style.overflow;
switch (openAnimationOrigin) { root.style.overflow = "hidden";
case "lt": const duration = (rend.height / 1600) * 1000;
animateGrowFromTopLeft(root, { easing: ANIM_CURVE_STD }); const easing = ANIM_CURVE_STD;
break; const animation = root.animate(
case "rt": {
animateGrowFromTopRight(root, { easing: ANIM_CURVE_STD }); height: [`${rend.height / 2}px`, `${rend.height}px`],
break; width: [`${(rend.width / 4) * 3}px`, `${rend.width}px`],
},
{
duration,
easing,
},
);
animation.addEventListener(
"finish",
() => (root.style.overflow = overflow),
);
} }
} else {
// TODO: update the pos
} }
};
createEffect(() => {
if (props.open) {
animateOpen();
} else { } else {
animateClose(); animateClose();
} }
@ -226,42 +185,27 @@ const Menu: Component<MenuProps> = (oprops) => {
} }
}; };
const onDialogClick = ( return (
event: MouseEvent & { currentTarget: HTMLDialogElement }, <dialog
) => { ref={root!}
event.stopPropagation(); onClose={props.onClose}
if (event.currentTarget !== event.target) return; onClick={(e) => {
if (!event.currentTarget.open) return; if (e.target === root) {
const rect = event.currentTarget.getBoundingClientRect();
const isNotInDialog =
event.clientY < rect.top ||
event.clientY > rect.bottom ||
event.clientX < rect.left ||
event.clientX > rect.right;
if (isNotInDialog) {
if (props.onClose) { if (props.onClose) {
if (Array.isArray(props.onClose)) { if (Array.isArray(props.onClose)) {
props.onClose[0](props.onClose[1], event); props.onClose[0](props.onClose[1], e);
} else { } else {
( (
props.onClose as ( props.onClose as (
event: Event & { currentTarget: HTMLDialogElement }, event: Event & { currentTarget: HTMLDialogElement },
) => void ) => void
)(event); )(e);
} }
} }
e.stopPropagation();
} }
}; }}
class={`Menu e${anchorPos().e || 0}`}
return (
<dialog
ref={root!}
onClose={props.onClose}
onCancel={props.onClose}
onClick={onDialogClick}
class={`Menu e${anchorPos().e || "0"}`}
style={{ style={{
left: px(anchorPos().left), left: px(anchorPos().left),
top: px(anchorPos().top), top: px(anchorPos().top),

View file

@ -4,9 +4,6 @@
z-index: var(--tutu-zidx-nav, auto); z-index: var(--tutu-zidx-nav, auto);
.MuiToolbar-root { .MuiToolbar-root {
margin-left: var(--safe-area-inset-left);
margin-right: var(--safe-area-inset-right);
>.MuiButtonBase-root { >.MuiButtonBase-root {
&:first-child { &:first-child {
margin-left: -0.5em; margin-left: -0.5em;

View file

@ -15,16 +15,6 @@ dialog.StackedPage {
width: 560px; width: 560px;
max-height: 100vh; max-height: 100vh;
max-height: 100dvh; max-height: 100dvh;
/*
* WebKit does not see contain-instric-size as the real element size.
* If the container does not have height, the child element using 100%
* height (usually Scafflod in our case) was have 0px computed height.
*
* This behaviour is different from Firefox. So we need to actually
* define the box height here. (Rubicon)
*/
height: 100vh;
height: 100dvh;
background: none; background: none;
display: none; display: none;
@ -41,9 +31,10 @@ dialog.StackedPage {
@media (max-width: 560px) { @media (max-width: 560px) {
& { & {
margin: 0;
width: 100vw; width: 100vw;
width: 100dvw; width: 100dvw;
height: 100vh;
height: 100dvh;
contain-intrinsic-size: 100vw 100vh; contain-intrinsic-size: 100vw 100vh;
contain-intrinsic-size: 100dvw 100dvh; contain-intrinsic-size: 100dvw 100dvh;
} }

View file

@ -7,7 +7,11 @@ import {
ListItemText, ListItemText,
MenuItem, MenuItem,
} from "@suid/material"; } from "@suid/material";
import { Show, createUniqueId, type ParentComponent } from "solid-js"; import {
Show,
createUniqueId,
type ParentComponent,
} from "solid-js";
import { import {
Settings as SettingsIcon, Settings as SettingsIcon,
Bookmark as BookmarkIcon, Bookmark as BookmarkIcon,
@ -35,7 +39,9 @@ const ProfileMenuButton: ParentComponent<{
const [open, state] = createManagedMenuState(); const [open, state] = createManagedMenuState();
const onClick = (event: { currentTarget: HTMLElement }) => { const onClick = (
event: MouseEvent & { currentTarget: HTMLButtonElement },
) => {
open(event.currentTarget.getBoundingClientRect()); open(event.currentTarget.getBoundingClientRect());
}; };
@ -48,8 +54,8 @@ const ProfileMenuButton: ParentComponent<{
sx={{ borderRadius: "50%" }} sx={{ borderRadius: "50%" }}
id={buttonId} id={buttonId}
onClick={onClick} onClick={onClick}
aria-controls={state.open ? menuId : undefined} aria-controls={open() ? menuId : undefined}
aria-expanded={state.open ? "true" : "false"} aria-expanded={open() ? "true" : undefined}
> >
<Avatar <Avatar
alt={`${inf()?.displayName}'s avatar`} alt={`${inf()?.displayName}'s avatar`}