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": {
 | 
			
		||||
    "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
									
								
							
							
						
						
									
										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;
 | 
			
		||||
  box-shadow: var(--tutu-shadow-e8);
 | 
			
		||||
  contain: content;
 | 
			
		||||
  overscroll-behavior: contain;
 | 
			
		||||
 | 
			
		||||
  &.e1 {
 | 
			
		||||
    box-shadow: var(--tutu-shadow-e9);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,16 +141,30 @@ const Menu: Component<MenuProps> = (props) => {
 | 
			
		|||
 | 
			
		||||
  let openAnimationOrigin: "lt" | "rt" = "lt";
 | 
			
		||||
 | 
			
		||||
  createEffect(() => {
 | 
			
		||||
    if (props.open) {
 | 
			
		||||
  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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      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({
 | 
			
		||||
| 
						 | 
				
			
			@ -123,34 +172,26 @@ const Menu: Component<MenuProps> = (props) => {
 | 
			
		|||
        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),
 | 
			
		||||
          );
 | 
			
		||||
    if (!isOpened) {
 | 
			
		||||
      switch (openAnimationOrigin) {
 | 
			
		||||
        case "lt":
 | 
			
		||||
          animateGrowFromTopLeft(root, { easing: ANIM_CURVE_STD });
 | 
			
		||||
          break;
 | 
			
		||||
        case "rt":
 | 
			
		||||
          animateGrowFromTopRight(root, { easing: ANIM_CURVE_STD });
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
      } else {
 | 
			
		||||
        // TODO: update the pos
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  createEffect(() => {
 | 
			
		||||
    if (props.open) {
 | 
			
		||||
      animateOpen();
 | 
			
		||||
    } else {
 | 
			
		||||
      animateClose();
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -185,27 +226,42 @@ const Menu: Component<MenuProps> = (props) => {
 | 
			
		|||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <dialog
 | 
			
		||||
      ref={root!}
 | 
			
		||||
      onClose={props.onClose}
 | 
			
		||||
      onClick={(e) => {
 | 
			
		||||
        if (e.target === root) {
 | 
			
		||||
  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], e);
 | 
			
		||||
          props.onClose[0](props.onClose[1], event);
 | 
			
		||||
        } else {
 | 
			
		||||
          (
 | 
			
		||||
            props.onClose as (
 | 
			
		||||
              event: Event & { currentTarget: HTMLDialogElement },
 | 
			
		||||
            ) => 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={{
 | 
			
		||||
        left: px(anchorPos().left),
 | 
			
		||||
        top: px(anchorPos().top),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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`}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue