Skip to content

Commit

Permalink
WIP: Solid UI
Browse files Browse the repository at this point in the history
  • Loading branch information
areknawo committed Jul 19, 2024
1 parent 1116662 commit 7b174f5
Show file tree
Hide file tree
Showing 10 changed files with 592 additions and 700 deletions.
45 changes: 5 additions & 40 deletions apps/docs/src/components/fragments/header.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
import { setSearchPaletteOpened } from "./search-palette";
import { mdiAppleKeyboardCommand, mdiGithub, mdiMagnify } from "@mdi/js";
import { For, type Component, Show, createSignal, createEffect, onMount } from "solid-js";
import { SearchPalette } from "./search-palette";
import { mdiGithub } from "@mdi/js";
import { type Component } from "solid-js";
import clsx from "clsx";
import { Button, Icon, IconButton, Tooltip } from "#components/primitives";
import { discordIcon, logoIcon } from "#assets/icons";
import { discordIcon } from "#assets/icons";

const isAppleDevice = (): boolean => {
const platform = typeof navigator === "object" ? navigator.platform : "";
const appleDeviceRegex = /Mac|iPod|iPhone|iPad/;

return appleDeviceRegex.test(platform);
};
const externalLinks = [
{
label: "GitHub",
Expand All @@ -24,12 +17,6 @@ const externalLinks = [
}
];
const Header: Component = () => {
const [showShortcut, setShowShortcut] = createSignal(false);

onMount(() => {
setShowShortcut(true);
});

return (
<div
class="top-0 md:px-8 h-16 bg-gray-50 dark:bg-gray-800 backdrop-blur-md z-1 items-center justify-center flex w-full md:w-[calc(100%+4rem)] absolute left-1/2 -translate-x-1/2 shadow-2xl shadow-gray-50 dark:shadow-gray-800"
Expand All @@ -43,29 +30,7 @@ const Header: Component = () => {
"from-gray-50 dark:from-gray-800 top-16"
)}
/>
<IconButton
path={mdiMagnify}
label={
<Show when={showShortcut()}>
<div class="flex w-full items-center">
<span class="pl-1 flex-1 text-start pr-3">Search</span>
<kbd class="hidden border-0 bg-gray-300 dark:bg-gray-700 group-hover:bg-gray-200 dark:group-hover:bg-gray-800 md:flex justify-center items-center rounded-md px-1 h-5 text-sm">
<Show when={isAppleDevice()} fallback={<span>Ctrl </span>}>
<Icon path={mdiAppleKeyboardCommand} class="h-3 w-3" />
</Show>
K
</kbd>
</div>
</Show>
}
text="soft"
class="w-full justify-start m-0 group max-w-screen-md rounded-xl bg-gray-100 dark:bg-gray-900 p-2"
onClick={() => {
// Force mobile keyboard to open (focus must be in user-triggered event handler)
document.getElementById("search-palette-input")?.focus({ preventScroll: true });
setSearchPaletteOpened((opened) => !opened);
}}
/>
<SearchPalette />
</div>
);
};
Expand Down
158 changes: 65 additions & 93 deletions apps/docs/src/components/fragments/on-this-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { mdiListBox } from "@mdi/js";
import clsx from "clsx";
import { scroll } from "seamless-scroll-polyfill";
import { type TOCItem, TOC } from "@vrite/solid-ui";
import type { MarkdownHeading } from "astro";
import { Button, IconButton } from "#components/primitives";

Expand All @@ -20,95 +21,46 @@ interface OnThisPageProps {
}

const OnThisPage: Component<OnThisPageProps> = (props) => {
const [activeHeading, setActiveHeading] = createSignal(props.headings[0]?.slug || "");
const headings = createMemo(() => {
return props.headings.filter((heading) => {
return heading.depth === 2 || heading.depth === 3;
const items = createMemo(() => {
const items: TOCItem[] = [];

let parent: TOCItem[] | null = null;

props.headings.forEach((heading) => {
if (heading.depth === 2) {
const children: TOCItem[] = [];

items.push({
label: heading.text,
id: heading.slug,
children
});
parent = children;
} else if (heading.depth === 3) {
parent?.push({
label: heading.text,
id: heading.slug
});
}
});
});
const scrollToActiveHeading = (smooth?: boolean): void => {
const heading = activeHeading();
const element = document.getElementById(heading);

if (!element) return;

const rect = element.getBoundingClientRect();
const y = rect.top + window.scrollY - 60;

scroll(window, {
top: y,
behavior: smooth === false ? "instant" : "smooth"
});
};
return items;
});
const handleClick = (event: MouseEvent): void => {
const target = event.target as HTMLElement;

if (target.matches("h2, h3")) {
const { id } = target;

if (id) {
setActiveHeading(id);
scrollToActiveHeading();
history.replaceState(null, "", `#${id}`);
navigator.clipboard.writeText(window.location.href);
}
}
};

onMount(() => {
if (!headings().length) return;

const hash = location.hash.slice(1);
const setCurrent: IntersectionObserverCallback = (entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const { id } = entry.target;

setActiveHeading(entry.target.id);
break;
}
}
};
const container = document.body;
const observerOptions: IntersectionObserverInit = {
rootMargin: "-100px 0% -66%",
threshold: 0
};
const headingsObserver = new IntersectionObserver(setCurrent, observerOptions);
const handleScroll = (): void => {
if (!container) return;

const threshold = 50;
const isEnd =
container.scrollTop + container.clientHeight + threshold >= container.scrollHeight;
const isStart = container.scrollTop <= threshold;

if (isEnd) {
setActiveHeading(headings()[headings().length - 1].slug);
} else if (isStart) {
setActiveHeading(headings()[0].slug);
}
};

document
.querySelectorAll(
headings()
.map((heading) => `#${heading.slug}`)
.join(", ")
)
.forEach((h) => headingsObserver.observe(h));
container?.addEventListener("scroll", handleScroll);
document.body.addEventListener("click", handleClick);
onCleanup(() => {
headingsObserver.disconnect();
container?.removeEventListener("scroll", handleScroll);
document.body.removeEventListener("click", handleClick);
});

if (hash) {
setActiveHeading(hash);
scrollToActiveHeading(false);
}
document.addEventListener("click", handleClick);
});

return (
Expand All @@ -120,7 +72,7 @@ const OnThisPage: Component<OnThisPageProps> = (props) => {
)}
>
<Show when={props.hide !== true}>
<Show when={headings().length}>
<Show when={items().length}>
<IconButton
text="soft"
class="font-bold justify-start m-0"
Expand All @@ -131,25 +83,45 @@ const OnThisPage: Component<OnThisPageProps> = (props) => {
label="On This Page"
/>
</Show>
<For each={headings()}>
{(heading) => {
return (
<Button
variant="text"
text={activeHeading() === heading.slug ? "base" : "soft"}
color={activeHeading() === heading.slug ? "primary" : "base"}
class={clsx("text-start m-0", heading.depth === 3 && "ml-6")}
size={heading.depth === 2 ? "medium" : "small"}
onClick={() => {
setActiveHeading(heading.slug);
scrollToActiveHeading();
}}
>
{heading.text}
</Button>
);
}}
</For>
<div class="flex flex-col">
<TOC.Root items={items()} offset={80} getId={() => ""}>
{(props) => {
const isActive = (): boolean => props.isActive;
const level = (): number => props.level;

return (
<>
<TOC.Item
item={props.item}
as={(props) => {
return (
<Button
variant="text"
hover={false}
onClick={props.onClick}
class={clsx(
"text-start m-0 bg-gradient-to-tr text-transparent bg-clip-text from-gray-500 to-gray-500 dark:from-gray-400 dark:to-gray-400",
isActive() && "!from-orange-500 !to-red-500",
!isActive() &&
"hover:!from-gray-700 hover:!to-gray-700 dark:hover:!from-gray-200 dark:hover:!to-gray-200",
level() === 1 && "font-semibold"
)}
>
{props.children}
</Button>
);
}}
>
{props.item.label}
</TOC.Item>
<Show when={props.item.children?.length}>
<div class="ml-4 flex flex-col">{props.children}</div>
</Show>
</>
);
}}
</TOC.Root>
</div>
</Show>
</div>
<div class="min-w-64 hidden xl:flex" />
Expand Down
Loading

0 comments on commit 7b174f5

Please sign in to comment.