Compare commits
4 commits
e174b7aafd
...
7205fa5775
Author | SHA1 | Date | |
---|---|---|---|
|
7205fa5775 | ||
|
9a7710c070 | ||
|
a69a5c31e5 | ||
|
9e9a831785 |
7 changed files with 150 additions and 76 deletions
|
@ -8,7 +8,8 @@
|
||||||
"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",
|
||||||
|
|
10
scripts/src-lc.sh
Executable file
10
scripts/src-lc.sh
Executable 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=-
|
|
@ -7,6 +7,7 @@
|
||||||
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,6 +1,7 @@
|
||||||
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,
|
||||||
|
@ -65,11 +66,39 @@ export function createManagedMenuState() {
|
||||||
return !!anchor();
|
return !!anchor();
|
||||||
},
|
},
|
||||||
anchor: anchor as () => Anchor,
|
anchor: anchor as () => Anchor,
|
||||||
onClose: () => setAnchor(),
|
onClose: (event: Event) => {
|
||||||
|
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.
|
||||||
|
@ -78,10 +107,16 @@ export function createManagedMenuState() {
|
||||||
* - 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> = (props) => {
|
const Menu: Component<MenuProps> = (oprops) => {
|
||||||
let root: HTMLDialogElement;
|
let root: HTMLDialogElement;
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
const [, rest] = splitProps(props, ["open", "onClose", "anchor"]);
|
const [props, rest] = splitProps(oprops, [
|
||||||
|
"open",
|
||||||
|
"onClose",
|
||||||
|
"anchor",
|
||||||
|
"MenuListProps",
|
||||||
|
"children",
|
||||||
|
]);
|
||||||
|
|
||||||
const [anchorPos, setAnchorPos] = createSignal<{
|
const [anchorPos, setAnchorPos] = createSignal<{
|
||||||
left?: number;
|
left?: number;
|
||||||
|
@ -106,16 +141,30 @@ const Menu: Component<MenuProps> = (props) => {
|
||||||
|
|
||||||
let openAnimationOrigin: "lt" | "rt" = "lt";
|
let openAnimationOrigin: "lt" | "rt" = "lt";
|
||||||
|
|
||||||
createEffect(() => {
|
const animateOpen = () => {
|
||||||
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({
|
||||||
|
@ -123,34 +172,26 @@ const Menu: Component<MenuProps> = (props) => {
|
||||||
top,
|
top,
|
||||||
e,
|
e,
|
||||||
});
|
});
|
||||||
|
|
||||||
animateGrowFromTopRight(root, { easing: ANIM_CURVE_STD });
|
|
||||||
} else {
|
} else {
|
||||||
openAnimationOrigin = "lt";
|
openAnimationOrigin = "lt";
|
||||||
setAnchorPos({ left, top, e });
|
setAnchorPos({ left, top, e });
|
||||||
|
}
|
||||||
|
|
||||||
const overflow = root.style.overflow;
|
if (!isOpened) {
|
||||||
root.style.overflow = "hidden";
|
switch (openAnimationOrigin) {
|
||||||
const duration = (rend.height / 1600) * 1000;
|
case "lt":
|
||||||
const easing = ANIM_CURVE_STD;
|
animateGrowFromTopLeft(root, { easing: ANIM_CURVE_STD });
|
||||||
const animation = root.animate(
|
break;
|
||||||
{
|
case "rt":
|
||||||
height: [`${rend.height / 2}px`, `${rend.height}px`],
|
animateGrowFromTopRight(root, { easing: ANIM_CURVE_STD });
|
||||||
width: [`${(rend.width / 4) * 3}px`, `${rend.width}px`],
|
break;
|
||||||
},
|
|
||||||
{
|
|
||||||
duration,
|
|
||||||
easing,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
animation.addEventListener(
|
|
||||||
"finish",
|
|
||||||
() => (root.style.overflow = overflow),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// TODO: update the pos
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.open) {
|
||||||
|
animateOpen();
|
||||||
} else {
|
} else {
|
||||||
animateClose();
|
animateClose();
|
||||||
}
|
}
|
||||||
|
@ -185,27 +226,42 @@ const Menu: Component<MenuProps> = (props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const onDialogClick = (
|
||||||
<dialog
|
event: MouseEvent & { currentTarget: HTMLDialogElement },
|
||||||
ref={root!}
|
) => {
|
||||||
onClose={props.onClose}
|
event.stopPropagation();
|
||||||
onClick={(e) => {
|
if (event.currentTarget !== event.target) return;
|
||||||
if (e.target === root) {
|
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 (props.onClose) {
|
||||||
if (Array.isArray(props.onClose)) {
|
if (Array.isArray(props.onClose)) {
|
||||||
props.onClose[0](props.onClose[1], e);
|
props.onClose[0](props.onClose[1], event);
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
props.onClose as (
|
props.onClose as (
|
||||||
event: Event & { currentTarget: HTMLDialogElement },
|
event: Event & { currentTarget: HTMLDialogElement },
|
||||||
) => void
|
) => void
|
||||||
)(e);
|
)(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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),
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
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,6 +15,16 @@ 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;
|
||||||
|
|
||||||
|
@ -31,10 +41,9 @@ 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,11 +7,7 @@ import {
|
||||||
ListItemText,
|
ListItemText,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
} from "@suid/material";
|
} from "@suid/material";
|
||||||
import {
|
import { Show, createUniqueId, type ParentComponent } from "solid-js";
|
||||||
Show,
|
|
||||||
createUniqueId,
|
|
||||||
type ParentComponent,
|
|
||||||
} from "solid-js";
|
|
||||||
import {
|
import {
|
||||||
Settings as SettingsIcon,
|
Settings as SettingsIcon,
|
||||||
Bookmark as BookmarkIcon,
|
Bookmark as BookmarkIcon,
|
||||||
|
@ -39,9 +35,7 @@ const ProfileMenuButton: ParentComponent<{
|
||||||
|
|
||||||
const [open, state] = createManagedMenuState();
|
const [open, state] = createManagedMenuState();
|
||||||
|
|
||||||
const onClick = (
|
const onClick = (event: { currentTarget: HTMLElement }) => {
|
||||||
event: MouseEvent & { currentTarget: HTMLButtonElement },
|
|
||||||
) => {
|
|
||||||
open(event.currentTarget.getBoundingClientRect());
|
open(event.currentTarget.getBoundingClientRect());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -54,8 +48,8 @@ const ProfileMenuButton: ParentComponent<{
|
||||||
sx={{ borderRadius: "50%" }}
|
sx={{ borderRadius: "50%" }}
|
||||||
id={buttonId}
|
id={buttonId}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
aria-controls={open() ? menuId : undefined}
|
aria-controls={state.open ? menuId : undefined}
|
||||||
aria-expanded={open() ? "true" : undefined}
|
aria-expanded={state.open ? "true" : "false"}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
alt={`${inf()?.displayName}'s avatar`}
|
alt={`${inf()?.displayName}'s avatar`}
|
||||||
|
|
Loading…
Reference in a new issue