Compare commits

..

4 commits

Author SHA1 Message Date
thislight
7205fa5775
Scaffold: add safe area padding for top bar
All checks were successful
/ depoly (push) Successful in 1m18s
2024-11-20 00:31:09 +08:00
thislight
9a7710c070
Menu: fix #47, skip animateOpen if animating 2024-11-20 00:30:49 +08:00
thislight
a69a5c31e5
StackedRouter: fix content height is 0 on webkit
* for WebKit, fix the content height is 0 if the
  window width > 560px
2024-11-20 00:26:05 +08:00
thislight
9e9a831785
scripts: add count-source-lines 2024-11-19 21:12:28 +08:00
7 changed files with 150 additions and 76 deletions

View file

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

10
scripts/src-lc.sh Executable file
View file

@ -0,0 +1,10 @@
#!/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,6 +7,7 @@
width: max-content;
box-shadow: var(--tutu-shadow-e8);
contain: content;
overscroll-behavior: contain;
&.e1 {
box-shadow: var(--tutu-shadow-e9);

View file

@ -1,6 +1,7 @@
import { useWindowSize } from "@solid-primitives/resize-observer";
import { MenuList } from "@suid/material";
import {
batch,
createEffect,
createSignal,
splitProps,
@ -65,11 +66,39 @@ export function createManagedMenuState() {
return !!anchor();
},
anchor: anchor as () => Anchor,
onClose: () => setAnchor(),
onClose: (event: Event) => {
event.preventDefault();
return setAnchor();
},
},
] 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
* implemented with dialog and {@link MenuList} from SUID.
@ -78,10 +107,16 @@ export function createManagedMenuState() {
* - Use {@link createManagedMenuState} and you don't need to manage the open and close.
* - Use {@link MenuItem} from SUID as children.
*/
const Menu: Component<MenuProps> = (props) => {
const Menu: Component<MenuProps> = (oprops) => {
let root: HTMLDialogElement;
const windowSize = useWindowSize();
const [, rest] = splitProps(props, ["open", "onClose", "anchor"]);
const [props, rest] = splitProps(oprops, [
"open",
"onClose",
"anchor",
"MenuListProps",
"children",
]);
const [anchorPos, setAnchorPos] = createSignal<{
left?: number;
@ -106,51 +141,57 @@ const Menu: Component<MenuProps> = (props) => {
let openAnimationOrigin: "lt" | "rt" = "lt";
const animateOpen = () => {
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;
}
root.showModal();
const rend = root.getBoundingClientRect();
if (left > width / 2) {
openAnimationOrigin = "rt";
setAnchorPos({
left: right - rend.width,
top,
e,
});
} else {
openAnimationOrigin = "lt";
setAnchorPos({ left, top, e });
}
if (!isOpened) {
switch (openAnimationOrigin) {
case "lt":
animateGrowFromTopLeft(root, { easing: ANIM_CURVE_STD });
break;
case "rt":
animateGrowFromTopRight(root, { easing: ANIM_CURVE_STD });
break;
}
}
};
createEffect(() => {
if (props.open) {
const a = props.anchor();
if (!root.open) {
root.showModal();
const rend = root.getBoundingClientRect();
const { width } = windowSize;
const { left, top, right, e } = a;
if (left > width / 2) {
openAnimationOrigin = "rt";
setAnchorPos({
left: right - rend.width,
top,
e,
});
animateGrowFromTopRight(root, { easing: ANIM_CURVE_STD });
} else {
openAnimationOrigin = "lt";
setAnchorPos({ left, top, e });
const overflow = root.style.overflow;
root.style.overflow = "hidden";
const duration = (rend.height / 1600) * 1000;
const easing = ANIM_CURVE_STD;
const animation = root.animate(
{
height: [`${rend.height / 2}px`, `${rend.height}px`],
width: [`${(rend.width / 4) * 3}px`, `${rend.width}px`],
},
{
duration,
easing,
},
);
animation.addEventListener(
"finish",
() => (root.style.overflow = overflow),
);
}
} else {
// TODO: update the pos
}
animateOpen();
} else {
animateClose();
}
@ -185,27 +226,42 @@ const Menu: Component<MenuProps> = (props) => {
}
};
const onDialogClick = (
event: MouseEvent & { currentTarget: HTMLDialogElement },
) => {
event.stopPropagation();
if (event.currentTarget !== event.target) return;
if (!event.currentTarget.open) return;
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 (Array.isArray(props.onClose)) {
props.onClose[0](props.onClose[1], event);
} else {
(
props.onClose as (
event: Event & { currentTarget: HTMLDialogElement },
) => void
)(event);
}
}
}
};
return (
<dialog
ref={root!}
onClose={props.onClose}
onClick={(e) => {
if (e.target === root) {
if (props.onClose) {
if (Array.isArray(props.onClose)) {
props.onClose[0](props.onClose[1], e);
} else {
(
props.onClose as (
event: Event & { currentTarget: HTMLDialogElement },
) => void
)(e);
}
}
e.stopPropagation();
}
}}
class={`Menu e${anchorPos().e || 0}`}
onCancel={props.onClose}
onClick={onDialogClick}
class={`Menu e${anchorPos().e || "0"}`}
style={{
left: px(anchorPos().left),
top: px(anchorPos().top),

View file

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

View file

@ -15,6 +15,16 @@ dialog.StackedPage {
width: 560px;
max-height: 100vh;
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;
display: none;
@ -31,10 +41,9 @@ dialog.StackedPage {
@media (max-width: 560px) {
& {
margin: 0;
width: 100vw;
width: 100dvw;
height: 100vh;
height: 100dvh;
contain-intrinsic-size: 100vw 100vh;
contain-intrinsic-size: 100dvw 100dvh;
}

View file

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