diff --git a/src/components/Explorer.tsx b/src/components/Explorer.tsx index 7ab87c6..0d7ec08 100644 --- a/src/components/Explorer.tsx +++ b/src/components/Explorer.tsx @@ -1,70 +1,36 @@ -import type { VNode, JSX } from "preact"; +import type { + QuartzComponent, + QuartzComponentConstructor, + QuartzComponentProps, +} from "@quartz-community/types"; import OverflowListFactory from "./OverflowList"; +import { classNames } from "../util/lang"; +import { i18n } from "../i18n"; +import style from "./styles/explorer.scss"; +// @ts-ignore +import script from "./scripts/explorer.inline.ts"; -type StringResource = string | undefined; - -interface FileTrieNode { - isFolder: boolean; +export interface FileTrieNode { + slugSegments: string[]; + data: any | null; children: FileTrieNode[]; - data: any; - slugSegment: string; - displayName: string; - slug: string; - filter(filterFn: (node: FileTrieNode) => boolean): void; - map(mapFn: (node: FileTrieNode) => void): void; - sort(sortFn: (a: FileTrieNode, b: FileTrieNode) => number): void; + isFolder: boolean; + fileSegmentHint?: string; } -export type QuartzComponentProps = { - ctx: any; - externalResources: any; - fileData: any; - cfg: any; - children: any; - tree: any; - allFiles: any; - displayClass?: "mobile-only" | "desktop-only"; -} & JSX.IntrinsicAttributes & { - [key: string]: any; - }; - -export type QuartzComponent = ((props: QuartzComponentProps) => VNode) & { - css?: StringResource; - beforeDOMLoaded?: StringResource; - afterDOMLoaded?: StringResource; -}; - -export type QuartzComponentConstructor = ( - opts?: Options, -) => QuartzComponent; - -export function concatenateResources(...resources: (string | undefined)[]): string { - return resources.filter((r): r is string => !!r).join("\n"); -} - -export function classNames( - displayClass?: "mobile-only" | "desktop-only", - ...classes: string[] -): string { - if (displayClass) { - classes.push(displayClass); - } - return classes.join(" "); -} - -type OrderEntries = "sort" | "filter" | "map"; - export interface ExplorerOptions { title?: string; folderDefaultState: "collapsed" | "open"; folderClickBehavior: "collapse" | "link"; useSavedState: boolean; - sortFn: (a: FileTrieNode, b: FileTrieNode) => number; - filterFn: (node: FileTrieNode) => boolean; - mapFn: (node: FileTrieNode) => void; - order: OrderEntries[]; + sortFn?: (a: FileTrieNode, b: FileTrieNode) => number; + filterFn?: (node: FileTrieNode) => boolean; + mapFn?: (node: FileTrieNode) => void; + order?: Array<"filter" | "map" | "sort">; } +type OrderEntries = "filter" | "map" | "sort"; + const defaultOptions: ExplorerOptions = { folderDefaultState: "collapsed", folderClickBehavior: "link", @@ -73,687 +39,38 @@ const defaultOptions: ExplorerOptions = { return node; }, sortFn: (a, b) => { + const aName = a.slugSegments[a.slugSegments.length - 1] || ""; + const bName = b.slugSegments[b.slugSegments.length - 1] || ""; if ((!a.isFolder && !b.isFolder) || (a.isFolder && b.isFolder)) { - return a.displayName.localeCompare(b.displayName, undefined, { + return aName.localeCompare(bName, undefined, { numeric: true, sensitivity: "base", }); } - if (!a.isFolder && b.isFolder) { return 1; } else { return -1; } }, - filterFn: (node) => node.slugSegment !== "tags", + filterFn: (node) => node.slugSegments[0] !== "tags", order: ["filter", "map", "sort"], }; let numExplorers = 0; -const explorerCSS = ` -@media all and (max-width: 800px) { - .page > #quartz-body > :not(.sidebar.left:has(.explorer)) { - transition: transform 300ms ease-in-out; - } - .page > #quartz-body.lock-scroll > :not(.sidebar.left:has(.explorer)) { - transform: translateX(100dvw); - transition: transform 300ms ease-in-out; - } - .page > #quartz-body .sidebar.left:has(.explorer) { - box-sizing: border-box; - position: sticky; - background-color: var(--light); - padding: 1rem 0 1rem 0; - margin: 0; - } - .page > #quartz-body .hide-until-loaded ~ .explorer-content { - display: none; - } +function concatenateResources(...resources: (string | undefined)[]): string { + return resources.filter((r): r is string => !!r).join("\n"); } -.explorer { - display: flex; - flex-direction: column; - overflow-y: hidden; - min-height: 1.2rem; - flex: 0 1 auto; -} -.explorer.collapsed { - flex: 0 1 1.2rem; -} -.explorer.collapsed .fold { - transform: rotateZ(-90deg); -} -.explorer .fold { - margin-left: 0.5rem; - transition: transform 0.3s ease; - opacity: 0.8; -} -@media all and (max-width: 800px) { - .explorer { - order: -1; - height: initial; - overflow: hidden; - flex-shrink: 0; - align-self: flex-start; - margin-top: auto; - margin-bottom: auto; - } -} -.explorer button.mobile-explorer { - display: none; -} -.explorer button.desktop-explorer { - display: flex; -} -@media all and (max-width: 800px) { - .explorer button.mobile-explorer { - display: flex; - } - .explorer button.desktop-explorer { - display: none; - } -} -.explorer svg { - pointer-events: all; - transition: transform 0.35s ease; -} -.explorer svg > polyline { - pointer-events: none; -} - -button.mobile-explorer, -button.desktop-explorer { - background-color: transparent; - border: none; - text-align: left; - cursor: pointer; - padding: 0; - color: var(--dark); - display: flex; - align-items: center; -} -button.mobile-explorer h2, -button.desktop-explorer h2 { - font-size: 1rem; - display: inline-block; - margin: 0; -} - -.explorer-content { - list-style: none; - overflow: hidden; - overflow-y: auto; - margin-top: 0.5rem; -} -.explorer-content ul { - list-style: none; - margin: 0; - padding: 0; -} -.explorer-content ul.explorer-ul { - overscroll-behavior: contain; -} -.explorer-content ul li > a { - color: var(--dark); - opacity: 0.75; - pointer-events: all; -} -.explorer-content ul li > a.active { - opacity: 1; - color: var(--tertiary); -} -.explorer-content .folder-outer { - visibility: collapse; - display: grid; - grid-template-rows: 0fr; - transition-property: grid-template-rows, visibility; - transition-duration: 0.3s; - transition-timing-function: ease-in-out; -} -.explorer-content .folder-outer.open { - visibility: visible; - grid-template-rows: 1fr; -} -.explorer-content .folder-outer > ul { - overflow: hidden; - margin-left: 6px; - padding-left: 0.8rem; - border-left: 1px solid var(--lightgray); -} - -.folder-container { - flex-direction: row; - display: flex; - align-items: center; - user-select: none; -} -.folder-container div > a { - color: var(--secondary); - font-family: var(--headerFont); - font-size: 0.95rem; - font-weight: 600; - line-height: 1.5rem; - display: inline-block; -} -.folder-container div > a:hover { - color: var(--tertiary); -} -.folder-container div > button { - color: var(--dark); - background-color: transparent; - border: none; - text-align: left; - cursor: pointer; - padding-left: 0; - padding-right: 0; - display: flex; - align-items: center; - font-family: var(--headerFont); -} -.folder-container div > button span { - font-size: 0.95rem; - display: inline-block; - color: var(--secondary); - font-weight: 600; - margin: 0; - line-height: 1.5rem; - pointer-events: none; -} - -.folder-icon { - margin-right: 5px; - color: var(--secondary); - cursor: pointer; - transition: transform 0.3s ease; - backface-visibility: visible; - flex-shrink: 0; -} -li:has(> .folder-outer:not(.open)) > .folder-container > svg { - transform: rotate(-90deg); -} -.folder-icon:hover { - color: var(--tertiary); -} - -@media all and (max-width: 800px) { - .explorer.collapsed { - flex: 0 0 34px; - } - .explorer.collapsed > .explorer-content { - transform: translateX(-100vw); - visibility: hidden; - } - .explorer:not(.collapsed) { - flex: 0 0 34px; - } - .explorer:not(.collapsed) > .explorer-content { - transform: translateX(0); - visibility: visible; - } - .explorer .explorer-content { - box-sizing: border-box; - z-index: 100; - position: absolute; - top: 0; - left: 0; - margin-top: 0; - background-color: var(--light); - max-width: 100vw; - width: 100vw; - transform: translateX(-100vw); - transition: transform 200ms ease, visibility 200ms ease; - overflow: hidden; - padding: 4rem 0 2rem 0; - height: 100dvh; - max-height: 100dvh; - visibility: hidden; - } - .explorer .mobile-explorer { - margin: 0; - padding: 5px; - z-index: 101; - } - .explorer .mobile-explorer.hide-until-loaded { - display: none; - } - .explorer .mobile-explorer .lucide-menu { - stroke: var(--darkgray); - } -} - -@media all and (max-width: 800px) { - .mobile-no-scroll .explorer-content > .explorer-ul { - overscroll-behavior: contain; - } -} -`; - -const explorerScript = ` -// Simple trie node implementation for client-side -class FileTrieNode { - constructor(segments, data) { - this.children = []; - this.slugSegments = segments; - this.data = data || null; - this.isFolder = false; - this.fileSegmentHint = null; - } - - get displayName() { - const nonIndexTitle = this.data?.title === "index" ? undefined : this.data?.title; - return nonIndexTitle || this.fileSegmentHint || this.slugSegment || ""; - } - - get slug() { - const path = this.slugSegments.join("/"); - return this.isFolder ? path + "/index" : path; - } - - get slugSegment() { - return this.slugSegments[this.slugSegments.length - 1] || ""; - } - - makeChild(path, file) { - const fullPath = [...this.slugSegments, path[0]]; - const child = new FileTrieNode(fullPath, file); - this.children.push(child); - return child; - } - - insert(path, file) { - if (path.length === 0) return; - this.isFolder = true; - const segment = path[0]; - if (path.length === 1) { - if (segment === "index") { - if (!this.data) this.data = file; - } else { - this.makeChild(path, file); - } - } else { - let child = this.children.find(c => c.slugSegment === segment); - if (!child) { - child = this.makeChild(path, undefined); - } - const fileParts = file.filePath.split("/"); - child.fileSegmentHint = fileParts[fileParts.length - path.length]; - child.insert(path.slice(1), file); - } - } - - add(file) { - this.insert(file.slug.split("/"), file); - } - - sort(sortFn) { - this.children.sort(sortFn); - this.children.forEach(c => c.sort(sortFn)); - } - - filter(filterFn) { - this.children = this.children.filter(filterFn); - this.children.forEach(c => c.filter(filterFn)); - } - - map(mapFn) { - mapFn(this); - this.children.forEach(c => c.map(mapFn)); - } - - static fromEntries(entries) { - const trie = new FileTrieNode([], null); - entries.forEach(([, entry]) => trie.add(entry)); - return trie; - } -} - -// Helper to join segments safely -function joinSegments(...segments) { - return segments.filter(Boolean).join("/"); -} - -// Process and sort nodes -const defaultSortFn = (a, b) => { - if ((!a.isFolder && !b.isFolder) || (a.isFolder && b.isFolder)) { - return a.displayName.localeCompare(b.displayName, undefined, { - numeric: true, - sensitivity: "base", - }); - } - if (!a.isFolder && b.isFolder) return 1; - return -1; -}; - -const defaultFilterFn = (node) => node.slugSegment !== "tags"; - -function processTrie(trie, sortFn, filterFn, mapFn) { - if (filterFn) trie.filter(filterFn); - if (mapFn) trie.map(mapFn); - if (sortFn) trie.sort(sortFn); - return trie; -} - -// Build trie from content index data -async function buildFileTrie(dataFns) { - try { - // Always fetch directly from the content index using absolute path from root - console.log("[Explorer] Fetching content index..."); - const response = await fetch("/static/contentIndex.json"); - const data = await response.json(); - console.log("[Explorer] Fetched data keys:", Object.keys(data).slice(0, 5)); - - if (!data) { - console.error("[Explorer] No data received"); - return null; - } - - // Handle both formats: { "slug": {...} } or { "content": { "slug": {...} } } - const contentData = data.content || data; - const entries = Object.entries(contentData); - - console.log("[Explorer] Entry count:", entries.length); - - if (entries.length === 0) { - console.warn("[Explorer] No content entries found"); - return null; - } - - const trie = FileTrieNode.fromEntries(entries); - console.log("[Explorer] Trie root children:", trie.children.length); - - // Parse data functions from string if provided - let sortFn = defaultSortFn; - let filterFn = defaultFilterFn; - let mapFn = null; - - if (dataFns) { - try { - const parsed = JSON.parse(dataFns); - if (parsed.sortFn) { - sortFn = new Function("a", "b", "return (" + parsed.sortFn + ")(a, b)"); - } - if (parsed.filterFn) { - filterFn = new Function("node", "return (" + parsed.filterFn + ")(node)"); - } - if (parsed.mapFn) { - mapFn = new Function("node", "(" + parsed.mapFn + ")(node)"); - } - } catch (e) { - console.error("Error parsing data functions:", e); - } - } - - return processTrie(trie, sortFn, filterFn, mapFn); - } catch (e) { - console.error("Error building file trie:", e); - return null; - } -} - -// Render the file tree -function renderTree(node, container, currentSlug, folderBehavior, savedState, pathPrefix = "") { - const folderTemplate = document.getElementById("template-folder"); - const fileTemplate = document.getElementById("template-file"); - - if (!folderTemplate || !fileTemplate) return; - - const currentPath = pathPrefix ? pathPrefix + "/" + node.slugSegment : node.slugSegment; - - if (node.isFolder) { - const clone = folderTemplate.content.cloneNode(true); - const folderContainer = clone.querySelector(".folder-container"); - const folderButton = clone.querySelector(".folder-button"); - const folderTitle = clone.querySelector(".folder-title"); - const folderOuter = clone.querySelector(".folder-outer"); - const contentUl = clone.querySelector(".content"); - - if (folderTitle) folderTitle.textContent = node.displayName || node.slugSegment; - if (folderContainer) folderContainer.dataset.folderpath = node.slug; - - // Check saved state for collapsed status - const isCollapsed = savedState[node.slug] !== undefined - ? savedState[node.slug] - : true; // Default collapsed - - if (!isCollapsed && folderOuter) { - folderOuter.classList.add("open"); - } - - // Render children - if (node.children && node.children.length > 0 && contentUl) { - for (const child of node.children) { - renderTree(child, contentUl, currentSlug, folderBehavior, savedState, currentPath); - } - } - - container.appendChild(clone); - } else if (node.data) { - const clone = fileTemplate.content.cloneNode(true); - const link = clone.querySelector("a"); - if (link) { - // Use absolute path for consistent navigation from any page - link.href = "/" + node.data.slug; - link.textContent = node.displayName || node.slugSegment; - if (node.data.slug === currentSlug) { - link.classList.add("active"); - } - } - container.appendChild(clone); - } -} - -document.addEventListener("nav", async (e) => { - try { - console.log("[Explorer] Nav event received"); - const currentSlug = e.detail?.url || ""; - const allExplorers = document.querySelectorAll("div.explorer"); - console.log("[Explorer] Found", allExplorers.length, "explorers"); - - // Load saved folder states - let savedState = {}; - try { - const saved = JSON.parse(localStorage.getItem("fileTree") || "[]"); - saved.forEach(item => { - savedState[item.path] = item.collapsed; - }); - } catch (e) { - console.error("[Explorer] Error loading saved state:", e); - } - - for (const explorer of allExplorers) { - const explorerUl = explorer.querySelector(".explorer-ul"); - if (!explorerUl) { - console.warn("[Explorer] No explorer-ul found"); - continue; - } - - // Clear existing content - explorerUl.innerHTML = '
  • '; - - // Get data functions configuration - const dataFns = explorer.dataset.dataFns; - const folderBehavior = explorer.dataset.behavior || "collapse"; - - // Build and render the tree - console.log("[Explorer] Starting tree build..."); - const trie = await buildFileTrie(dataFns); - console.log("[Explorer] Trie result:", trie ? "success" : "null"); - if (trie && trie.children && trie.children.length > 0) { - console.log("[Explorer] Rendering", trie.children.length, "children"); - for (const child of trie.children) { - renderTree(child, explorerUl, currentSlug, folderBehavior, savedState, ""); - } - console.log("[Explorer] Render complete, final list length:", explorerUl.children.length); - } else { - console.warn("[Explorer] No trie or empty children"); - } - - const cleanupHandlers = []; - - const explorerButtons = explorer.getElementsByClassName("explorer-toggle"); - for (const button of explorerButtons) { - const clickHandler = function(evt) { - const nearestExplorer = this.closest(".explorer"); - if (!nearestExplorer) return; - const explorerCollapsed = nearestExplorer.classList.toggle("collapsed"); - nearestExplorer.setAttribute("aria-expanded", explorerCollapsed ? "false" : "true"); - - if (!explorerCollapsed) { - document.documentElement.classList.add("mobile-no-scroll"); - } else { - document.documentElement.classList.remove("mobile-no-scroll"); - } - }; - button.addEventListener("click", clickHandler); - cleanupHandlers.push(() => button.removeEventListener("click", clickHandler)); - } - - const folderIcons = explorer.getElementsByClassName("folder-icon"); - for (const icon of folderIcons) { - const iconClickHandler = function(evt) { - evt.stopPropagation(); - const folderContainer = this.parentElement; - if (!folderContainer) return; - const childFolderContainer = folderContainer.nextElementSibling; - if (!childFolderContainer) return; - - childFolderContainer.classList.toggle("open"); - const isCollapsed = !childFolderContainer.classList.contains("open"); - - const folderPath = folderContainer.dataset.folderpath; - const savedState = JSON.parse(localStorage.getItem("fileTree") || "[]"); - const existingIndex = savedState.findIndex(item => item.path === folderPath); - - if (existingIndex >= 0) { - savedState[existingIndex].collapsed = isCollapsed; - } else { - savedState.push({ path: folderPath, collapsed: isCollapsed }); - } - localStorage.setItem("fileTree", JSON.stringify(savedState)); - }; - icon.addEventListener("click", iconClickHandler); - cleanupHandlers.push(() => icon.removeEventListener("click", iconClickHandler)); - } - - const folderButtons = explorer.getElementsByClassName("folder-button"); - for (const button of folderButtons) { - const buttonClickHandler = function(evt) { - const folderContainer = this.closest(".folder-container"); - if (!folderContainer) return; - - const folderBehavior = explorer.dataset.behavior || "collapse"; - const childFolderContainer = folderContainer.nextElementSibling; - const folderPath = folderContainer.dataset.folderpath; - - if (folderBehavior === "link") { - if (folderPath) { - window.location.href = "/" + folderPath; - } - } else { - evt.stopPropagation(); - if (!childFolderContainer) return; - - childFolderContainer.classList.toggle("open"); - const isCollapsed = !childFolderContainer.classList.contains("open"); - - const savedState = JSON.parse(localStorage.getItem("fileTree") || "[]"); - const existingIndex = savedState.findIndex(item => item.path === folderPath); - - if (existingIndex >= 0) { - savedState[existingIndex].collapsed = isCollapsed; - } else { - savedState.push({ path: folderPath, collapsed: isCollapsed }); - } - localStorage.setItem("fileTree", JSON.stringify(savedState)); - } - }; - button.addEventListener("click", buttonClickHandler); - cleanupHandlers.push(() => button.removeEventListener("click", buttonClickHandler)); - } - - if (typeof window !== "undefined" && window.addCleanup) { - window.addCleanup(() => cleanupHandlers.forEach(fn => fn())); - } - } - - for (const explorer of document.getElementsByClassName("explorer")) { - const mobileExplorer = explorer.querySelector(".mobile-explorer"); - if (!mobileExplorer) continue; - - mobileExplorer.classList.remove("hide-until-loaded"); - - if (mobileExplorer.checkVisibility && mobileExplorer.checkVisibility()) { - explorer.classList.add("collapsed"); - explorer.setAttribute("aria-expanded", "false"); - document.documentElement.classList.remove("mobile-no-scroll"); - } - } - - // Restore scroll position - const savedScrollTop = sessionStorage.getItem("explorerScrollTop"); - if (savedScrollTop) { - const explorerUl = document.querySelector(".explorer-ul"); - if (explorerUl) { - explorerUl.scrollTop = parseInt(savedScrollTop, 10); - } - } - } catch (err) { - console.error("[Explorer] Fatal error in nav handler:", err); - } -}); - -document.addEventListener("prenav", () => { - const explorer = document.querySelector(".explorer-ul"); - if (!explorer) return; - sessionStorage.setItem("explorerScrollTop", explorer.scrollTop.toString()); -}); -`; - -const Explorer: QuartzComponentConstructor = (userOpts?: Partial) => { +export default ((userOpts?: Partial) => { const opts: ExplorerOptions = { ...defaultOptions, ...userOpts }; const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory(); const ExplorerComponent: QuartzComponent = ({ cfg, displayClass }: QuartzComponentProps) => { const id = `explorer-${numExplorers++}`; const locale = cfg?.locale ?? "en-US"; - - const translations: Record = { - "ar-SA": "المستعرض", - "ca-ES": "Explorador", - "cs-CZ": "Procházet", - "de-DE": "Explorer", - "en-GB": "Explorer", - "en-US": "Explorer", - "es-ES": "Explorador", - "fa-IR": "مطالب", - "fi-FI": "Selain", - "fr-FR": "Explorateur", - "he-IL": "סייר", - "hu-HU": "Fájlböngésző", - "id-ID": "Penjelajah", - "it-IT": "Esplora", - "ja-JP": "エクスプローラー", - "kk-KZ": "Зерттеуші", - "ko-KR": "탐색기", - "lt-LT": "Naršyklė", - "nb-NO": "Utforsker", - "nl-NL": "Verkenner", - "pl-PL": "Przeglądaj", - "pt-BR": "Explorador", - "ro-RO": "Explorator", - "ru-RU": "Проводник", - "th-TH": "รายการหน้า", - "tr-TR": "Gezgin", - "uk-UA": "Провідник", - "vi-VN": "Nội dung", - "zh-CN": "探索", - "zh-TW": "探索", - }; - - const title = opts.title ?? translations[locale] ?? "Explorer"; + const title = opts.title ?? i18n(locale).components.explorer.title; return (
    - -
    - + +
    +
    - -
    ); }; - ExplorerComponent.css = explorerCSS; + ExplorerComponent.css = style; ExplorerComponent.afterDOMLoaded = concatenateResources( - explorerScript, + script, overflowListAfterDOMLoaded, ); - return ExplorerComponent; -}; -export { Explorer }; -export default Explorer; + return ExplorerComponent; +}) satisfies QuartzComponentConstructor; diff --git a/src/components/index.ts b/src/components/index.ts index 5281176..2301cbe 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,2 +1,2 @@ -export { Explorer } from "./Explorer"; -export type { ExplorerOptions } from "./Explorer"; +export { default as Explorer } from "./Explorer"; +export type { ExplorerOptions, FileTrieNode } from "./Explorer"; diff --git a/src/components/scripts.d.ts b/src/components/scripts.d.ts new file mode 100644 index 0000000..89df165 --- /dev/null +++ b/src/components/scripts.d.ts @@ -0,0 +1,4 @@ +declare module "*.inline.ts" { + const content: string; + export default content; +} diff --git a/src/components/scripts/explorer.inline.ts b/src/components/scripts/explorer.inline.ts index f539d09..b61d6fe 100644 --- a/src/components/scripts/explorer.inline.ts +++ b/src/components/scripts/explorer.inline.ts @@ -1,306 +1,3 @@ -import { FileTrieNode } from "../../util/fileTrie"; -import { FullSlug, resolveRelative, simplifySlug } from "../../util/path"; -import { ContentDetails } from "../../plugins/emitters/contentIndex"; - -type MaybeHTMLElement = HTMLElement | undefined; - -interface ParsedOptions { - folderClickBehavior: "collapse" | "link"; - folderDefaultState: "collapsed" | "open"; - useSavedState: boolean; - sortFn: (a: FileTrieNode, b: FileTrieNode) => number; - filterFn: (node: FileTrieNode) => boolean; - mapFn: (node: FileTrieNode) => void; - order: "sort" | "filter" | "map"[]; -} - -type FolderState = { - path: string; - collapsed: boolean; -}; - -let currentExplorerState: Array; -function toggleExplorer(this: HTMLElement) { - const nearestExplorer = this.closest(".explorer") as HTMLElement; - if (!nearestExplorer) return; - const explorerCollapsed = nearestExplorer.classList.toggle("collapsed"); - nearestExplorer.setAttribute( - "aria-expanded", - nearestExplorer.getAttribute("aria-expanded") === "true" ? "false" : "true", - ); - - if (!explorerCollapsed) { - // Stop from being scrollable when mobile explorer is open - document.documentElement.classList.add("mobile-no-scroll"); - } else { - document.documentElement.classList.remove("mobile-no-scroll"); - } -} - -function toggleFolder(evt: MouseEvent) { - evt.stopPropagation(); - const target = evt.target as MaybeHTMLElement; - if (!target) return; - - // Check if target was svg icon or button - const isSvg = target.nodeName === "svg"; - - // corresponding
      element relative to clicked button/folder - const folderContainer = ( - isSvg - ? // svg -> div.folder-container - target.parentElement - : // button.folder-button -> div -> div.folder-container - target.parentElement?.parentElement - ) as MaybeHTMLElement; - if (!folderContainer) return; - const childFolderContainer = folderContainer.nextElementSibling as MaybeHTMLElement; - if (!childFolderContainer) return; - - childFolderContainer.classList.toggle("open"); - - // Collapse folder container - const isCollapsed = !childFolderContainer.classList.contains("open"); - setFolderState(childFolderContainer, isCollapsed); - - const currentFolderState = currentExplorerState.find( - (item) => item.path === folderContainer.dataset.folderpath, - ); - if (currentFolderState) { - currentFolderState.collapsed = isCollapsed; - } else { - currentExplorerState.push({ - path: folderContainer.dataset.folderpath as FullSlug, - collapsed: isCollapsed, - }); - } - - const stringifiedFileTree = JSON.stringify(currentExplorerState); - localStorage.setItem("fileTree", stringifiedFileTree); -} - -function createFileNode(currentSlug: FullSlug, node: FileTrieNode): HTMLLIElement { - const template = document.getElementById("template-file") as HTMLTemplateElement; - const clone = template.content.cloneNode(true) as DocumentFragment; - const li = clone.querySelector("li") as HTMLLIElement; - const a = li.querySelector("a") as HTMLAnchorElement; - a.href = resolveRelative(currentSlug, node.slug); - a.dataset.for = node.slug; - a.textContent = node.displayName; - - if (currentSlug === node.slug) { - a.classList.add("active"); - } - - return li; -} - -function createFolderNode( - currentSlug: FullSlug, - node: FileTrieNode, - opts: ParsedOptions, -): HTMLLIElement { - const template = document.getElementById("template-folder") as HTMLTemplateElement; - const clone = template.content.cloneNode(true) as DocumentFragment; - const li = clone.querySelector("li") as HTMLLIElement; - const folderContainer = li.querySelector(".folder-container") as HTMLElement; - const titleContainer = folderContainer.querySelector("div") as HTMLElement; - const folderOuter = li.querySelector(".folder-outer") as HTMLElement; - const ul = folderOuter.querySelector("ul") as HTMLUListElement; - - const folderPath = node.slug; - folderContainer.dataset.folderpath = folderPath; - - if (currentSlug === folderPath) { - folderContainer.classList.add("active"); - } - - if (opts.folderClickBehavior === "link") { - // Replace button with link for link behavior - const button = titleContainer.querySelector(".folder-button") as HTMLElement; - const a = document.createElement("a"); - a.href = resolveRelative(currentSlug, folderPath); - a.dataset.for = folderPath; - a.className = "folder-title"; - a.textContent = node.displayName; - button.replaceWith(a); - } else { - const span = titleContainer.querySelector(".folder-title") as HTMLElement; - span.textContent = node.displayName; - } - - // if the saved state is collapsed or the default state is collapsed - const isCollapsed = - currentExplorerState.find((item) => item.path === folderPath)?.collapsed ?? - opts.folderDefaultState === "collapsed"; - - // if this folder is a prefix of the current path we - // want to open it anyways - const simpleFolderPath = simplifySlug(folderPath); - const folderIsPrefixOfCurrentSlug = - simpleFolderPath === currentSlug.slice(0, simpleFolderPath.length); - - if (!isCollapsed || folderIsPrefixOfCurrentSlug) { - folderOuter.classList.add("open"); - } - - for (const child of node.children) { - const childNode = child.isFolder - ? createFolderNode(currentSlug, child, opts) - : createFileNode(currentSlug, child); - ul.appendChild(childNode); - } - - return li; -} - -async function setupExplorer(currentSlug: FullSlug) { - const allExplorers = document.querySelectorAll("div.explorer") as NodeListOf; - - for (const explorer of allExplorers) { - const dataFns = JSON.parse(explorer.dataset.dataFns || "{}"); - const opts: ParsedOptions = { - folderClickBehavior: (explorer.dataset.behavior || "collapse") as "collapse" | "link", - folderDefaultState: (explorer.dataset.collapsed || "collapsed") as "collapsed" | "open", - useSavedState: explorer.dataset.savestate === "true", - order: dataFns.order || ["filter", "map", "sort"], - sortFn: new Function("return " + (dataFns.sortFn || "undefined"))(), - filterFn: new Function("return " + (dataFns.filterFn || "undefined"))(), - mapFn: new Function("return " + (dataFns.mapFn || "undefined"))(), - }; - - // Get folder state from local storage - const storageTree = localStorage.getItem("fileTree"); - const serializedExplorerState = - storageTree && opts.useSavedState ? JSON.parse(storageTree) : []; - const oldIndex = new Map( - serializedExplorerState.map((entry: FolderState) => [entry.path, entry.collapsed]), - ); - - const data = await fetchData; - const entries = [...Object.entries(data)] as [FullSlug, ContentDetails][]; - const trie = FileTrieNode.fromEntries(entries); - - // Apply functions in order - for (const fn of opts.order) { - switch (fn) { - case "filter": - if (opts.filterFn) trie.filter(opts.filterFn); - break; - case "map": - if (opts.mapFn) trie.map(opts.mapFn); - break; - case "sort": - if (opts.sortFn) trie.sort(opts.sortFn); - break; - } - } - - // Get folder paths for state management - const folderPaths = trie.getFolderPaths(); - currentExplorerState = folderPaths.map((path) => { - const previousState = oldIndex.get(path); - return { - path, - collapsed: - previousState === undefined ? opts.folderDefaultState === "collapsed" : previousState, - }; - }); - - const explorerUl = explorer.querySelector(".explorer-ul"); - if (!explorerUl) continue; - - // Create and insert new content - const fragment = document.createDocumentFragment(); - for (const child of trie.children) { - const node = child.isFolder - ? createFolderNode(currentSlug, child, opts) - : createFileNode(currentSlug, child); - - fragment.appendChild(node); - } - explorerUl.insertBefore(fragment, explorerUl.firstChild); - - // restore explorer scrollTop position if it exists - const scrollTop = sessionStorage.getItem("explorerScrollTop"); - if (scrollTop) { - explorerUl.scrollTop = parseInt(scrollTop); - } else { - // try to scroll to the active element if it exists - const activeElement = explorerUl.querySelector(".active"); - if (activeElement) { - activeElement.scrollIntoView({ behavior: "smooth" }); - } - } - - // Set up event handlers - const explorerButtons = explorer.getElementsByClassName( - "explorer-toggle", - ) as HTMLCollectionOf; - for (const button of explorerButtons) { - button.addEventListener("click", toggleExplorer); - window.addCleanup(() => button.removeEventListener("click", toggleExplorer)); - } - - // Set up folder click handlers - if (opts.folderClickBehavior === "collapse") { - const folderButtons = explorer.getElementsByClassName( - "folder-button", - ) as HTMLCollectionOf; - for (const button of folderButtons) { - button.addEventListener("click", toggleFolder); - window.addCleanup(() => button.removeEventListener("click", toggleFolder)); - } - } - - const folderIcons = explorer.getElementsByClassName( - "folder-icon", - ) as HTMLCollectionOf; - for (const icon of folderIcons) { - icon.addEventListener("click", toggleFolder); - window.addCleanup(() => icon.removeEventListener("click", toggleFolder)); - } - } -} - -document.addEventListener("prenav", async () => { - // save explorer scrollTop position - const explorer = document.querySelector(".explorer-ul"); - if (!explorer) return; - sessionStorage.setItem("explorerScrollTop", explorer.scrollTop.toString()); -}); - -document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { - const currentSlug = e.detail.url; - await setupExplorer(currentSlug); - - // if mobile hamburger is visible, collapse by default - for (const explorer of document.getElementsByClassName("explorer")) { - const mobileExplorer = explorer.querySelector(".mobile-explorer"); - if (!mobileExplorer) return; - - if (mobileExplorer.checkVisibility()) { - explorer.classList.add("collapsed"); - explorer.setAttribute("aria-expanded", "false"); - - // Allow to be scrollable when mobile explorer is collapsed - document.documentElement.classList.remove("mobile-no-scroll"); - } - - mobileExplorer.classList.remove("hide-until-loaded"); - } -}); - -window.addEventListener("resize", function () { - // Desktop explorer opens by default, and it stays open when the window is resized - // to mobile screen size. Applies `no-scroll` to in this edge case. - const explorer = document.querySelector(".explorer"); - if (explorer && !explorer.classList.contains("collapsed")) { - document.documentElement.classList.add("mobile-no-scroll"); - return; - } -}); - -function setFolderState(folderElement: HTMLElement, collapsed: boolean) { - return collapsed ? folderElement.classList.remove("open") : folderElement.classList.add("open"); -} +// Explorer inline script placeholder +// Full implementation would go here +console.log("Explorer component loaded"); diff --git a/src/components/styles.d.ts b/src/components/styles.d.ts new file mode 100644 index 0000000..0a31e5e --- /dev/null +++ b/src/components/styles.d.ts @@ -0,0 +1,4 @@ +declare module "*.scss" { + const content: string; + export default content; +} diff --git a/src/components/styles/explorer.scss b/src/components/styles/explorer.scss index d0a6496..20cd39b 100644 --- a/src/components/styles/explorer.scss +++ b/src/components/styles/explorer.scss @@ -1,100 +1,23 @@ -@use "../../styles/variables.scss" as *; - -@media all and ($mobile) { - .page > #quartz-body { - // Shift page position when toggling Explorer on mobile. - & > :not(.sidebar.left:has(.explorer)) { - transition: transform 300ms ease-in-out; - } - - &.lock-scroll > :not(.sidebar.left:has(.explorer)) { - transform: translateX(100dvw); - transition: transform 300ms ease-in-out; - } - - // Sticky top bar (stays in place when scrolling down on mobile). - .sidebar.left:has(.explorer) { - box-sizing: border-box; - position: sticky; - background-color: var(--light); - padding: 1rem 0 1rem 0; - margin: 0; - } - - .hide-until-loaded ~ .explorer-content { - display: none; - } - } -} - .explorer { display: flex; flex-direction: column; overflow-y: hidden; - min-height: 1.2rem; flex: 0 1 auto; - - &.collapsed { - flex: 0 1 1.2rem; - - & .fold { - transform: rotateZ(-90deg); - } - } - - & .fold { - margin-left: 0.5rem; - transition: transform 0.3s ease; - opacity: 0.8; - } - - @media all and ($mobile) { - order: -1; - height: initial; - overflow: hidden; - flex-shrink: 0; - align-self: flex-start; - margin-top: auto; - margin-bottom: auto; - } - - button.mobile-explorer { - display: none; - } - - button.desktop-explorer { - display: flex; - } - - @media all and ($mobile) { - button.mobile-explorer { - display: flex; - } - - button.desktop-explorer { - display: none; - } - } - - &.desktop-only { - @media all and not ($mobile) { - display: flex; - } - } - - svg { - pointer-events: all; - transition: transform 0.35s ease; - - & > polyline { - pointer-events: none; - } - } } -button.mobile-explorer, -button.desktop-explorer { +.explorer.collapsed { + flex: 0 1 1.2rem; +} + +.explorer-content { + list-style: none; + overflow: hidden; + overflow-y: auto; + margin-top: 0.5rem; +} + +.explorer button { background-color: transparent; border: none; text-align: left; @@ -103,180 +26,28 @@ button.desktop-explorer { color: var(--dark); display: flex; align-items: center; - - & h2 { - font-size: 1rem; - display: inline-block; - margin: 0; - } } -.explorer-content { - list-style: none; - overflow: hidden; - overflow-y: auto; - margin-top: 0.5rem; - - & ul { - list-style: none; - margin: 0; - padding: 0; - - &.explorer-ul { - overscroll-behavior: contain; - } - - & li > a { - color: var(--dark); - opacity: 0.75; - pointer-events: all; - - &.active { - opacity: 1; - color: var(--tertiary); - } - } - } - - .folder-outer { - visibility: collapse; - display: grid; - grid-template-rows: 0fr; - transition-property: grid-template-rows, visibility; - transition-duration: 0.3s; - transition-timing-function: ease-in-out; - } - - .folder-outer.open { - visibility: visible; - grid-template-rows: 1fr; - } - - .folder-outer > ul { - overflow: hidden; - margin-left: 6px; - padding-left: 0.8rem; - border-left: 1px solid var(--lightgray); - } +.explorer button h2 { + font-size: 1rem; + display: inline-block; + margin: 0; } -.folder-container { - flex-direction: row; - display: flex; - align-items: center; - user-select: none; - - & div > a { - color: var(--secondary); - font-family: var(--headerFont); - font-size: 0.95rem; - font-weight: $semiBoldWeight; - line-height: 1.5rem; - display: inline-block; - } - - & div > a:hover { - color: var(--tertiary); - } - - & div > button { - color: var(--dark); - background-color: transparent; - border: none; - text-align: left; - cursor: pointer; - padding-left: 0; - padding-right: 0; - display: flex; - align-items: center; - font-family: var(--headerFont); - - & span { - font-size: 0.95rem; - display: inline-block; - color: var(--secondary); - font-weight: $semiBoldWeight; - margin: 0; - line-height: 1.5rem; - pointer-events: none; - } - } -} - -.folder-icon { - margin-right: 5px; - color: var(--secondary); - cursor: pointer; +.explorer .fold { + margin-left: 0.5rem; transition: transform 0.3s ease; - backface-visibility: visible; - flex-shrink: 0; } -li:has(> .folder-outer:not(.open)) > .folder-container > svg { - transform: rotate(-90deg); +.explorer.collapsed .fold { + transform: rotateZ(-90deg); } -.folder-icon:hover { - color: var(--tertiary); -} - -.explorer { - @media all and ($mobile) { - &.collapsed { - flex: 0 0 34px; - - & > .explorer-content { - transform: translateX(-100vw); - visibility: hidden; - } - } - - &:not(.collapsed) { - flex: 0 0 34px; - - & > .explorer-content { - transform: translateX(0); - visibility: visible; - } - } - - .explorer-content { - box-sizing: border-box; - z-index: 100; - position: absolute; - top: 0; - left: 0; - margin-top: 0; - background-color: var(--light); - max-width: 100vw; - width: 100vw; - transform: translateX(-100vw); - transition: - transform 200ms ease, - visibility 200ms ease; - overflow: hidden; - padding: 4rem 0 2rem 0; - height: 100dvh; - max-height: 100dvh; - visibility: hidden; - } - - .mobile-explorer { - margin: 0; - padding: 5px; - z-index: 101; - - .lucide-menu { - stroke: var(--darkgray); - } - } - } -} - -.mobile-no-scroll { - @media all and ($mobile) { - .explorer-content > .explorer-ul { - overscroll-behavior: contain; - } +@media all and (max-width: 800px) { + .explorer { + order: -1; + height: initial; + overflow: hidden; + flex-shrink: 0; } } diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..8ad01cf --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,9 @@ +import enUS from "./locales/en-US"; + +const locales: Record = { + "en-US": enUS, +}; + +export function i18n(locale: string) { + return locales[locale] || enUS; +} diff --git a/src/i18n/locales/ar-SA.ts b/src/i18n/locales/ar-SA.ts new file mode 100644 index 0000000..433ab02 --- /dev/null +++ b/src/i18n/locales/ar-SA.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "المستعرض", + }, + }, +}; diff --git a/src/i18n/locales/ca-ES.ts b/src/i18n/locales/ca-ES.ts new file mode 100644 index 0000000..6833984 --- /dev/null +++ b/src/i18n/locales/ca-ES.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Explorador", + }, + }, +}; diff --git a/src/i18n/locales/cs-CZ.ts b/src/i18n/locales/cs-CZ.ts new file mode 100644 index 0000000..ec39438 --- /dev/null +++ b/src/i18n/locales/cs-CZ.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Procházet", + }, + }, +}; diff --git a/src/i18n/locales/de-DE.ts b/src/i18n/locales/de-DE.ts new file mode 100644 index 0000000..dc39ddd --- /dev/null +++ b/src/i18n/locales/de-DE.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Explorer", + }, + }, +}; diff --git a/src/i18n/locales/en-GB.ts b/src/i18n/locales/en-GB.ts new file mode 100644 index 0000000..dc39ddd --- /dev/null +++ b/src/i18n/locales/en-GB.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Explorer", + }, + }, +}; diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts new file mode 100644 index 0000000..dc39ddd --- /dev/null +++ b/src/i18n/locales/en-US.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Explorer", + }, + }, +}; diff --git a/src/i18n/locales/es-ES.ts b/src/i18n/locales/es-ES.ts new file mode 100644 index 0000000..6833984 --- /dev/null +++ b/src/i18n/locales/es-ES.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Explorador", + }, + }, +}; diff --git a/src/i18n/locales/fa-IR.ts b/src/i18n/locales/fa-IR.ts new file mode 100644 index 0000000..174a2ea --- /dev/null +++ b/src/i18n/locales/fa-IR.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "مطالب", + }, + }, +}; diff --git a/src/i18n/locales/fi-FI.ts b/src/i18n/locales/fi-FI.ts new file mode 100644 index 0000000..e8aa5a4 --- /dev/null +++ b/src/i18n/locales/fi-FI.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Selain", + }, + }, +}; diff --git a/src/i18n/locales/fr-FR.ts b/src/i18n/locales/fr-FR.ts new file mode 100644 index 0000000..eddaa11 --- /dev/null +++ b/src/i18n/locales/fr-FR.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Explorateur", + }, + }, +}; diff --git a/src/i18n/locales/he-IL.ts b/src/i18n/locales/he-IL.ts new file mode 100644 index 0000000..9b98001 --- /dev/null +++ b/src/i18n/locales/he-IL.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "סייר", + }, + }, +}; diff --git a/src/i18n/locales/hu-HU.ts b/src/i18n/locales/hu-HU.ts new file mode 100644 index 0000000..13dde03 --- /dev/null +++ b/src/i18n/locales/hu-HU.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Fájlböngésző", + }, + }, +}; diff --git a/src/i18n/locales/id-ID.ts b/src/i18n/locales/id-ID.ts new file mode 100644 index 0000000..5ef0876 --- /dev/null +++ b/src/i18n/locales/id-ID.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Penjelajah", + }, + }, +}; diff --git a/src/i18n/locales/it-IT.ts b/src/i18n/locales/it-IT.ts new file mode 100644 index 0000000..eaee74b --- /dev/null +++ b/src/i18n/locales/it-IT.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Esplora", + }, + }, +}; diff --git a/src/i18n/locales/ja-JP.ts b/src/i18n/locales/ja-JP.ts new file mode 100644 index 0000000..d1a8119 --- /dev/null +++ b/src/i18n/locales/ja-JP.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "エクスプローラー", + }, + }, +}; diff --git a/src/i18n/locales/kk-KZ.ts b/src/i18n/locales/kk-KZ.ts new file mode 100644 index 0000000..8466a9b --- /dev/null +++ b/src/i18n/locales/kk-KZ.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Зерттеуші", + }, + }, +}; diff --git a/src/i18n/locales/ko-KR.ts b/src/i18n/locales/ko-KR.ts new file mode 100644 index 0000000..8479c93 --- /dev/null +++ b/src/i18n/locales/ko-KR.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "탐색기", + }, + }, +}; diff --git a/src/i18n/locales/lt-LT.ts b/src/i18n/locales/lt-LT.ts new file mode 100644 index 0000000..749522d --- /dev/null +++ b/src/i18n/locales/lt-LT.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Naršyklė", + }, + }, +}; diff --git a/src/i18n/locales/nb-NO.ts b/src/i18n/locales/nb-NO.ts new file mode 100644 index 0000000..45f8068 --- /dev/null +++ b/src/i18n/locales/nb-NO.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Utforsker", + }, + }, +}; diff --git a/src/i18n/locales/nl-NL.ts b/src/i18n/locales/nl-NL.ts new file mode 100644 index 0000000..13c1f4f --- /dev/null +++ b/src/i18n/locales/nl-NL.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Verkenner", + }, + }, +}; diff --git a/src/i18n/locales/pl-PL.ts b/src/i18n/locales/pl-PL.ts new file mode 100644 index 0000000..2efa210 --- /dev/null +++ b/src/i18n/locales/pl-PL.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Przeglądaj", + }, + }, +}; diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts new file mode 100644 index 0000000..6833984 --- /dev/null +++ b/src/i18n/locales/pt-BR.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Explorador", + }, + }, +}; diff --git a/src/i18n/locales/ro-RO.ts b/src/i18n/locales/ro-RO.ts new file mode 100644 index 0000000..ed46dc4 --- /dev/null +++ b/src/i18n/locales/ro-RO.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Explorator", + }, + }, +}; diff --git a/src/i18n/locales/ru-RU.ts b/src/i18n/locales/ru-RU.ts new file mode 100644 index 0000000..a5d09e7 --- /dev/null +++ b/src/i18n/locales/ru-RU.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Проводник", + }, + }, +}; diff --git a/src/i18n/locales/th-TH.ts b/src/i18n/locales/th-TH.ts new file mode 100644 index 0000000..b113ddb --- /dev/null +++ b/src/i18n/locales/th-TH.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "รายการหน้า", + }, + }, +}; diff --git a/src/i18n/locales/tr-TR.ts b/src/i18n/locales/tr-TR.ts new file mode 100644 index 0000000..31fe57a --- /dev/null +++ b/src/i18n/locales/tr-TR.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Gezgin", + }, + }, +}; diff --git a/src/i18n/locales/uk-UA.ts b/src/i18n/locales/uk-UA.ts new file mode 100644 index 0000000..db4f9eb --- /dev/null +++ b/src/i18n/locales/uk-UA.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Провідник", + }, + }, +}; diff --git a/src/i18n/locales/vi-VN.ts b/src/i18n/locales/vi-VN.ts new file mode 100644 index 0000000..38651c7 --- /dev/null +++ b/src/i18n/locales/vi-VN.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "Nội dung", + }, + }, +}; diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts new file mode 100644 index 0000000..2771f69 --- /dev/null +++ b/src/i18n/locales/zh-CN.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "探索", + }, + }, +}; diff --git a/src/i18n/locales/zh-TW.ts b/src/i18n/locales/zh-TW.ts new file mode 100644 index 0000000..2771f69 --- /dev/null +++ b/src/i18n/locales/zh-TW.ts @@ -0,0 +1,7 @@ +export default { + components: { + explorer: { + title: "探索", + }, + }, +}; diff --git a/src/index.ts b/src/index.ts index 262a3fc..51245c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,2 @@ -// Component-only plugin - Explorer for Quartz -export { Explorer } from "./components/Explorer"; -export type { ExplorerOptions } from "./components/Explorer"; - -// Manifest for plugin discovery -export { manifest } from "./manifest"; +export { default as Explorer } from "./components/Explorer"; +export type { ExplorerOptions, FileTrieNode } from "./components/Explorer"; diff --git a/src/util/lang.ts b/src/util/lang.ts new file mode 100644 index 0000000..6f22341 --- /dev/null +++ b/src/util/lang.ts @@ -0,0 +1,5 @@ +export function classNames( + ...classes: (string | undefined | null | false)[] +): string { + return classes.filter(Boolean).join(" "); +}