From 7b174f51639365701e75b476f4003203e130bcaf Mon Sep 17 00:00:00 2001 From: Arek Nawo Date: Sat, 20 Jul 2024 00:03:12 +0200 Subject: [PATCH] WIP: Solid UI --- apps/docs/src/components/fragments/header.tsx | 45 +- .../src/components/fragments/on-this-page.tsx | 158 ++-- .../components/fragments/search-palette.tsx | 743 +++++++----------- .../src/components/fragments/side-bar.tsx | 10 +- .../docs/src/components/layouts/default.astro | 6 +- apps/docs/src/styles/base.css | 6 +- apps/web/public/sandbox.js | 60 +- packages/ui/solid/src/components/TOC.tsx | 110 ++- packages/ui/solid/src/components/search.tsx | 152 ++-- packages/ui/solid/tsup.config.ts | 2 +- 10 files changed, 592 insertions(+), 700 deletions(-) diff --git a/apps/docs/src/components/fragments/header.tsx b/apps/docs/src/components/fragments/header.tsx index 195d5cad..e1bc3cff 100644 --- a/apps/docs/src/components/fragments/header.tsx +++ b/apps/docs/src/components/fragments/header.tsx @@ -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", @@ -24,12 +17,6 @@ const externalLinks = [ } ]; const Header: Component = () => { - const [showShortcut, setShowShortcut] = createSignal(false); - - onMount(() => { - setShowShortcut(true); - }); - return (
{ "from-gray-50 dark:from-gray-800 top-16" )} /> - -
- Search - -
- - } - 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); - }} - /> +
); }; diff --git a/apps/docs/src/components/fragments/on-this-page.tsx b/apps/docs/src/components/fragments/on-this-page.tsx index d2d66145..65063ad8 100644 --- a/apps/docs/src/components/fragments/on-this-page.tsx +++ b/apps/docs/src/components/fragments/on-this-page.tsx @@ -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"; @@ -20,26 +21,31 @@ interface OnThisPageProps { } const OnThisPage: Component = (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; @@ -47,8 +53,6 @@ const OnThisPage: Component = (props) => { const { id } = target; if (id) { - setActiveHeading(id); - scrollToActiveHeading(); history.replaceState(null, "", `#${id}`); navigator.clipboard.writeText(window.location.href); } @@ -56,59 +60,7 @@ const OnThisPage: Component = (props) => { }; 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 ( @@ -120,7 +72,7 @@ const OnThisPage: Component = (props) => { )} > - + = (props) => { label="On This Page" /> - - {(heading) => { - return ( - - ); - }} - +
+ ""}> + {(props) => { + const isActive = (): boolean => props.isActive; + const level = (): number => props.level; + + return ( + <> + { + return ( + + ); + }} + > + {props.item.label} + + +
{props.children}
+
+ + ); + }} +
+