Compare commits
No commits in common. "7205fa5775f40bc0286c5077fc562597519dce0f" and "e174b7aafdb20f8cbdbf65a606acb54bd6a7b50e" have entirely different histories.
7205fa5775
...
e174b7aafd
7 changed files with 76 additions and 150 deletions
|
@ -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",
|
||||||
|
|
|
@ -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=-
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,57 +106,51 @@ const Menu: Component<MenuProps> = (oprops) => {
|
||||||
|
|
||||||
let openAnimationOrigin: "lt" | "rt" = "lt";
|
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(() => {
|
createEffect(() => {
|
||||||
if (props.open) {
|
if (props.open) {
|
||||||
animateOpen();
|
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
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
animateClose();
|
animateClose();
|
||||||
}
|
}
|
||||||
|
@ -226,42 +185,27 @@ const Menu: Component<MenuProps> = (oprops) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<dialog
|
<dialog
|
||||||
ref={root!}
|
ref={root!}
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
onCancel={props.onClose}
|
onClick={(e) => {
|
||||||
onClick={onDialogClick}
|
if (e.target === root) {
|
||||||
class={`Menu e${anchorPos().e || "0"}`}
|
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}`}
|
||||||
style={{
|
style={{
|
||||||
left: px(anchorPos().left),
|
left: px(anchorPos().left),
|
||||||
top: px(anchorPos().top),
|
top: px(anchorPos().top),
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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`}
|
||||||
|
|
Loading…
Reference in a new issue