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,51 +141,57 @@ const Menu: Component<MenuProps> = (props) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  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) {
 | 
				
			||||||
      const a = props.anchor();
 | 
					      animateOpen();
 | 
				
			||||||
 | 
					 | 
				
			||||||
      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();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -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 (
 | 
					  return (
 | 
				
			||||||
    <dialog
 | 
					    <dialog
 | 
				
			||||||
      ref={root!}
 | 
					      ref={root!}
 | 
				
			||||||
      onClose={props.onClose}
 | 
					      onClose={props.onClose}
 | 
				
			||||||
      onClick={(e) => {
 | 
					      onCancel={props.onClose}
 | 
				
			||||||
        if (e.target === root) {
 | 
					      onClick={onDialogClick}
 | 
				
			||||||
          if (props.onClose) {
 | 
					      class={`Menu e${anchorPos().e || "0"}`}
 | 
				
			||||||
            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,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…
	
	Add table
		Add a link
		
	
		Reference in a new issue