imprensa 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/default.css +3 -1
- package/dist/index.mjs +35 -7
- package/package.json +2 -1
- package/src/components/classes.ts +3 -0
- package/src/components/doc-pager.tsx +97 -0
- package/src/components/doc-toolbar.tsx +143 -0
- package/src/components/doc.tsx +2 -0
- package/src/components/global-search.tsx +141 -0
- package/src/components/icons.tsx +45 -0
- package/src/components/ilha-ui.ts +6 -0
- package/src/components/index.tsx +13 -0
- package/src/components/layout.tsx +110 -0
- package/src/components/mobile-nav.tsx +102 -0
- package/src/components/nav-footer-bar.tsx +34 -0
- package/src/components/preview.tsx +83 -0
- package/src/components/search-core.tsx +47 -0
- package/src/components/search-portal-sync.ts +234 -0
- package/src/components/search-store.ts +34 -0
- package/src/components/search.tsx +140 -0
- package/src/components/sidebar-layout.ts +53 -0
- package/src/components/sidebar.tsx +76 -0
- package/src/components/snippet.tsx +38 -0
- package/src/components/tree-indent.ts +14 -0
package/README.md
CHANGED
|
@@ -32,15 +32,17 @@ See `templates/starter` in the [imprensa](https://github.com/ilhajs/imprensa) mo
|
|
|
32
32
|
|
|
33
33
|
## Exports
|
|
34
34
|
|
|
35
|
-
| Subpath | Role
|
|
36
|
-
| ---------------------- |
|
|
37
|
-
| `imprensa` | Vite plugin
|
|
38
|
-
| `imprensa/runtime` | `createImprensa`, theme, mount/hydrate
|
|
39
|
-
| `imprensa/prerender` | Static prerender entry
|
|
40
|
-
| `imprensa/mdx` | Routes, search index, MDX render helpers
|
|
41
|
-
| `imprensa/components` | Layout, sidebar, search triggers
|
|
42
|
-
| `imprensa/doc` | Doc toolbar, pager
|
|
43
|
-
| `imprensa/default.css` | Docs theme + Tailwind layers
|
|
35
|
+
| Subpath | Role |
|
|
36
|
+
| ---------------------- | -------------------------------------------------------- |
|
|
37
|
+
| `imprensa` | Vite plugin |
|
|
38
|
+
| `imprensa/runtime` | `createImprensa`, theme, mount/hydrate |
|
|
39
|
+
| `imprensa/prerender` | Static prerender entry |
|
|
40
|
+
| `imprensa/mdx` | Routes, search index, MDX render helpers |
|
|
41
|
+
| `imprensa/components` | Layout, sidebar, search triggers |
|
|
42
|
+
| `imprensa/doc` | Doc toolbar, pager |
|
|
43
|
+
| `imprensa/default.css` | Docs theme + Tailwind layers (import from `src/app.css`) |
|
|
44
|
+
|
|
45
|
+
Import `imprensa/default.css` in your app stylesheet and keep `imprensa()` in Vite plugins so Tailwind scans imprensa’s `dist/` and `src/components` for utility classes.
|
|
44
46
|
|
|
45
47
|
## Global search
|
|
46
48
|
|
package/default.css
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
@plugin "@tailwindcss/typography";
|
|
4
4
|
@plugin "areia";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/* Class names live in prebuilt dist chunks and optional source components (npm tarball). */
|
|
7
|
+
@source "./dist/**/*.{mjs,js}";
|
|
8
|
+
@source "./src/components/**/*.{ts,tsx}";
|
|
7
9
|
|
|
8
10
|
@layer base {
|
|
9
11
|
* {
|
package/dist/index.mjs
CHANGED
|
@@ -474,13 +474,35 @@ const CONFIG_STUB = fileURLToPath(new URL("./docs/config.mjs", import.meta.url))
|
|
|
474
474
|
const ICONS_ENTRY = fileURLToPath(new URL("./components/icons.mjs", import.meta.url));
|
|
475
475
|
const IMPRENSA_PRERENDER_ENTRY = path.resolve(fileURLToPath(new URL("./core/prerender-core.mjs", import.meta.url)));
|
|
476
476
|
const IMPRENSA_CLIENT_RUNTIME = path.resolve(fileURLToPath(new URL("./core/client-runtime.mjs", import.meta.url)));
|
|
477
|
+
/** Published bundle still references __IMPRENSA_* until the Vite plugin rewrites them. */
|
|
478
|
+
const MDX_DIST_BUNDLE = path.join(fileURLToPath(new URL("../../..", import.meta.url)), "dist/docs/mdx.mjs");
|
|
477
479
|
function isMdxConfigTarget(id) {
|
|
478
|
-
|
|
480
|
+
const normalized = id.split("?")[0] ?? id;
|
|
481
|
+
return normalized === MDX_RUNTIME_CONFIG || normalized.endsWith("/imprensa/src/docs/mdx/runtime-config.ts") || normalized === MDX_SOURCE || normalized.endsWith("/imprensa/src/docs/mdx.ts") || normalized === MDX_DIST_BUNDLE || normalized.endsWith("/imprensa/dist/docs/mdx.mjs");
|
|
482
|
+
}
|
|
483
|
+
function injectedMdxRuntimeConfig(options) {
|
|
484
|
+
const { contentDir, repo, repoBranch, repoPath, headDefaults } = options;
|
|
485
|
+
return `export const contentDir = ${JSON.stringify(normalizeContentDir(contentDir))};
|
|
486
|
+
export const imprensaRepo = ${JSON.stringify(repo)};
|
|
487
|
+
export const imprensaRepoBranch = ${JSON.stringify(repoBranch)};
|
|
488
|
+
export const imprensaRepoPath = ${JSON.stringify(repoPath)};
|
|
489
|
+
export const mdxRawSources = ${JSON.stringify(collectRawMdxSources(process.cwd(), contentDir))};
|
|
490
|
+
export const headDefaults = ${JSON.stringify(headDefaults ?? null)};`;
|
|
479
491
|
}
|
|
480
492
|
function isAppPageFile(file, root) {
|
|
481
493
|
const relative = path.relative(path.join(root, "src/pages"), file).replace(/\\/g, "/");
|
|
482
494
|
return !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
483
495
|
}
|
|
496
|
+
/** Tailwind v4 @source paths are relative to the stylesheet file (package root for npm consumers). */
|
|
497
|
+
function patchImprensaDefaultCss(code, id) {
|
|
498
|
+
const file = id.split("?")[0] ?? id;
|
|
499
|
+
if (!file.endsWith(`${path.sep}imprensa${path.sep}default.css`) && !file.endsWith("/imprensa/default.css")) return;
|
|
500
|
+
const pkgRoot = path.dirname(file);
|
|
501
|
+
const distGlob = path.join(pkgRoot, "dist", "**", "*.{mjs,js}").replace(/\\/g, "/");
|
|
502
|
+
if (code.includes(distGlob)) return code;
|
|
503
|
+
const injection = `@source "${distGlob}";\n@source "${path.join(pkgRoot, "src", "components", "**", "*.{ts,tsx}").replace(/\\/g, "/")}";\n`;
|
|
504
|
+
return code.replace(/@plugin "areia";\s*\n/, `@plugin "areia";\n${injection}`);
|
|
505
|
+
}
|
|
484
506
|
function createImprensaVitePlugins(options = {}) {
|
|
485
507
|
const { shiki, mdx: mdxOptions = {}, pages: pagesOptions = {}, contentDir = "src/pages/(content)", detectDeadLink = true, llms = true, repo = "", repoBranch = "main", repoPath = "", hostname, head: headDefaults, socials = [], preview = {} } = options;
|
|
486
508
|
const { rehypePlugins, remarkPlugins, ...restMdxOptions } = mdxOptions;
|
|
@@ -589,6 +611,8 @@ export const shikiThemes = ${JSON.stringify(shikiThemes)};`;
|
|
|
589
611
|
return IMPRENSA_VIRTUAL_RUNTIME.replace("__SHIKI_THEMES__", JSON.stringify(shikiThemes));
|
|
590
612
|
},
|
|
591
613
|
transform(code, id) {
|
|
614
|
+
const cssPatch = patchImprensaDefaultCss(code, id);
|
|
615
|
+
if (cssPatch) return cssPatch;
|
|
592
616
|
if (/\.mdx?$/.test(id) && code.startsWith("---")) {
|
|
593
617
|
const end = code.indexOf("\n---", 3);
|
|
594
618
|
if (end !== -1) return {
|
|
@@ -597,12 +621,15 @@ export const shikiThemes = ${JSON.stringify(shikiThemes)};`;
|
|
|
597
621
|
};
|
|
598
622
|
}
|
|
599
623
|
if (!isMdxConfigTarget(id)) return;
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
624
|
+
const injected = injectedMdxRuntimeConfig({
|
|
625
|
+
contentDir,
|
|
626
|
+
repo,
|
|
627
|
+
repoBranch,
|
|
628
|
+
repoPath,
|
|
629
|
+
headDefaults
|
|
630
|
+
});
|
|
631
|
+
if (code.includes(MDX_CONFIG_MARKER)) return code.replace(MDX_CONFIG_MARKER, injected);
|
|
632
|
+
if (/__IMPRENSA_CONTENT_DIR__/.test(code)) return code.replace(/\/\/#region src\/docs\/mdx\/runtime-config\.ts[\s\S]*?\/\/#endregion/, `//#region src/docs/mdx/runtime-config.ts\n${injected}\n//#endregion`);
|
|
606
633
|
},
|
|
607
634
|
handleHotUpdate(ctx) {
|
|
608
635
|
if (!isAppPageFile(ctx.file, ctx.server.config.root)) return;
|
|
@@ -621,6 +648,7 @@ export const headDefaults = ${JSON.stringify(headDefaults ?? null)};`);
|
|
|
621
648
|
sonner = createRequire(path.join(root, "package.json")).resolve("sonner");
|
|
622
649
|
} catch {}
|
|
623
650
|
return {
|
|
651
|
+
optimizeDeps: { exclude: ["imprensa/mdx"] },
|
|
624
652
|
server: { watch: { usePolling: true } },
|
|
625
653
|
build: { rolldownOptions: { output: { codeSplitting: { groups: [{
|
|
626
654
|
name: "imprensa-search",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imprensa",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Vite plugin and runtime for Ilha + Areia documentation sites (MDX, prerender, search)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"areia",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
|
+
"src/components",
|
|
31
32
|
"src/docs/frontmatter.ts",
|
|
32
33
|
"src/docs/mdx.ts",
|
|
33
34
|
"src/docs/mdx/",
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** @jsxImportSource ilha */
|
|
2
|
+
import { Icon, LayerCard } from "areia";
|
|
3
|
+
import { ChevronLeft, ChevronRight } from "lucide";
|
|
4
|
+
import { contentMeta, contentTree, searchDocuments, type ContentTreeNode } from "imprensa/mdx";
|
|
5
|
+
import { cx } from "./classes";
|
|
6
|
+
|
|
7
|
+
export type DocNavItem = {
|
|
8
|
+
title: string;
|
|
9
|
+
path: string;
|
|
10
|
+
excerpt: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const EXCERPT_MAX = 160;
|
|
14
|
+
|
|
15
|
+
function excerptForPath(path: string): string {
|
|
16
|
+
const normalized = normalizePath(path);
|
|
17
|
+
const meta = contentMeta[normalized];
|
|
18
|
+
if (meta?.description?.trim()) return meta.description.trim();
|
|
19
|
+
|
|
20
|
+
const doc = searchDocuments.find((d) => normalizePath(d.path) === normalized);
|
|
21
|
+
const text = doc?.text?.trim();
|
|
22
|
+
if (!text) return "";
|
|
23
|
+
|
|
24
|
+
if (text.length <= EXCERPT_MAX) return text;
|
|
25
|
+
const slice = text.slice(0, EXCERPT_MAX);
|
|
26
|
+
const lastSpace = slice.lastIndexOf(" ");
|
|
27
|
+
return `${(lastSpace > 80 ? slice.slice(0, lastSpace) : slice).trim()}…`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizePath(path: string) {
|
|
31
|
+
return path.replace(/\/$/, "") || "/";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function flattenContentTree(nodes: ContentTreeNode[]): { title: string; path: string }[] {
|
|
35
|
+
const pages: { title: string; path: string }[] = [];
|
|
36
|
+
|
|
37
|
+
for (const node of nodes) {
|
|
38
|
+
if (node.path && node.type !== "link") pages.push({ title: node.title, path: node.path });
|
|
39
|
+
if (node.children.length > 0) pages.push(...flattenContentTree(node.children));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return pages;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getAdjacentDocs(path: string): { prev?: DocNavItem; next?: DocNavItem } {
|
|
46
|
+
const pages = flattenContentTree(contentTree);
|
|
47
|
+
const index = pages.findIndex((page) => normalizePath(page.path) === normalizePath(path));
|
|
48
|
+
if (index === -1) return {};
|
|
49
|
+
|
|
50
|
+
const toNavItem = (page: { title: string; path: string }): DocNavItem => ({
|
|
51
|
+
...page,
|
|
52
|
+
excerpt: excerptForPath(page.path),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
prev: index > 0 ? toNavItem(pages[index - 1]) : undefined,
|
|
57
|
+
next: index < pages.length - 1 ? toNavItem(pages[index + 1]) : undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function DocNavCard(props: { item: DocNavItem; direction: "prev" | "next" }) {
|
|
62
|
+
const isPrev = props.direction === "prev";
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<a href={props.item.path} class="group block h-full no-underline">
|
|
66
|
+
<LayerCard class="h-full transition-colors group-hover:bg-areia-control-hover">
|
|
67
|
+
<LayerCard.Content class="flex flex-col gap-2 p-4">
|
|
68
|
+
<div
|
|
69
|
+
class={cx(
|
|
70
|
+
"flex items-center gap-1.5 text-sm font-medium text-areia-strong",
|
|
71
|
+
!isPrev && "justify-end text-right",
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
{isPrev ? <Icon icon={ChevronLeft} class="size-4 shrink-0" /> : null}
|
|
75
|
+
<span>{props.item.title}</span>
|
|
76
|
+
{!isPrev ? <Icon icon={ChevronRight} class="size-4 shrink-0" /> : null}
|
|
77
|
+
</div>
|
|
78
|
+
<p class={cx("line-clamp-2 text-sm text-areia-subtle", !isPrev && "text-right")}>
|
|
79
|
+
{props.item.excerpt}
|
|
80
|
+
</p>
|
|
81
|
+
</LayerCard.Content>
|
|
82
|
+
</LayerCard>
|
|
83
|
+
</a>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function DocPager(props: { path: string }) {
|
|
88
|
+
const { prev, next } = getAdjacentDocs(props.path);
|
|
89
|
+
if (!prev && !next) return null;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<nav class="not-prose grid gap-4 py-12 sm:grid-cols-2" aria-label="Documentation pagination">
|
|
93
|
+
{prev ? <DocNavCard item={prev} direction="prev" /> : <div aria-hidden="true" />}
|
|
94
|
+
{next ? <DocNavCard item={next} direction="next" /> : <div aria-hidden="true" />}
|
|
95
|
+
</nav>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** @jsxImportSource ilha */
|
|
2
|
+
import type { RawHtml } from "ilha";
|
|
3
|
+
import { useRoute } from "@ilha/router";
|
|
4
|
+
import { Button, Dropdown, Icon } from "areia";
|
|
5
|
+
import ilha from "ilha";
|
|
6
|
+
import { MobileNavigationPopover } from "./mobile-nav";
|
|
7
|
+
import { SearchMobileTriggerButton } from "./search";
|
|
8
|
+
import { ChevronDown, Copy, ExternalLink, FileText, GitBranch, MessageSquare } from "lucide";
|
|
9
|
+
import { toast } from "sonner";
|
|
10
|
+
import { articleClass, contentMeta, getDocLinks } from "imprensa/mdx";
|
|
11
|
+
import { DocPager } from "./doc-pager";
|
|
12
|
+
import { cx } from "./classes";
|
|
13
|
+
|
|
14
|
+
function absoluteUrl(path: string) {
|
|
15
|
+
if (typeof window === "undefined") return path;
|
|
16
|
+
return new URL(path, window.location.origin).href;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function aiPromptUrl(service: "chatgpt" | "claude", markdownUrl: string) {
|
|
20
|
+
const prompt = `Read ${markdownUrl}`;
|
|
21
|
+
const encoded = encodeURIComponent(prompt);
|
|
22
|
+
|
|
23
|
+
if (service === "chatgpt") return `https://chatgpt.com/?q=${encoded}`;
|
|
24
|
+
return `https://claude.ai/new?q=${encoded}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function copyMarkdownForRoute(route: string) {
|
|
28
|
+
const links = getDocLinks(route);
|
|
29
|
+
if (!links) return;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const text =
|
|
33
|
+
links.rawMarkdown ??
|
|
34
|
+
(await fetch(links.markdownUrl).then((response) => {
|
|
35
|
+
if (!response.ok) throw new Error("Failed to fetch markdown");
|
|
36
|
+
return response.text();
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
await navigator.clipboard.writeText(text);
|
|
40
|
+
toast.success("Markdown copied to clipboard");
|
|
41
|
+
} catch {
|
|
42
|
+
toast.error("Failed to copy markdown");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const DocToolbar = ilha.input<{ path: string }>().render(({ input }) => {
|
|
47
|
+
const links = getDocLinks(input.path);
|
|
48
|
+
if (!links) return <></>;
|
|
49
|
+
|
|
50
|
+
const markdownUrl = absoluteUrl(links.markdownUrl);
|
|
51
|
+
const items = [
|
|
52
|
+
{
|
|
53
|
+
label: "Copy Markdown",
|
|
54
|
+
value: "copy-markdown",
|
|
55
|
+
icon: <Icon icon={Copy} class="size-4" />,
|
|
56
|
+
},
|
|
57
|
+
...(links.githubUrl
|
|
58
|
+
? [
|
|
59
|
+
{
|
|
60
|
+
label: "Open in GitHub",
|
|
61
|
+
href: links.githubUrl,
|
|
62
|
+
external: true,
|
|
63
|
+
icon: <Icon icon={GitBranch} class="size-4" />,
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
: []),
|
|
67
|
+
{
|
|
68
|
+
label: "View as Markdown",
|
|
69
|
+
href: links.markdownUrl,
|
|
70
|
+
external: true,
|
|
71
|
+
icon: <Icon icon={FileText} class="size-4" />,
|
|
72
|
+
"data-no-intercept": true,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: "Open in ChatGPT",
|
|
76
|
+
href: aiPromptUrl("chatgpt", markdownUrl),
|
|
77
|
+
external: true,
|
|
78
|
+
icon: <Icon icon={MessageSquare} class="size-4" />,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
label: "Open in Claude",
|
|
82
|
+
href: aiPromptUrl("claude", markdownUrl),
|
|
83
|
+
external: true,
|
|
84
|
+
icon: <Icon icon={ExternalLink} class="size-4" />,
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const { path: routePath } = useRoute();
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
class="not-prose fixed inset-x-0 top-0 z-30 mb-6 flex w-auto flex-wrap items-center justify-between gap-2 border-b border-areia-border bg-areia-background/90 px-4 py-2 backdrop-blur md:static md:inset-auto md:w-full md:border-b-0 md:bg-transparent md:p-0 md:backdrop-blur-none"
|
|
93
|
+
data-doc-route={input.path}
|
|
94
|
+
>
|
|
95
|
+
<div class="flex shrink-0 items-center gap-2 md:hidden">
|
|
96
|
+
<MobileNavigationPopover currentPath={routePath()} />
|
|
97
|
+
<SearchMobileTriggerButton />
|
|
98
|
+
</div>
|
|
99
|
+
<div class="flex flex-wrap items-center justify-end gap-2 md:ml-auto">
|
|
100
|
+
<Dropdown
|
|
101
|
+
align="end"
|
|
102
|
+
side="bottom"
|
|
103
|
+
onSelect={(value) => {
|
|
104
|
+
if (value === "copy-markdown") void copyMarkdownForRoute(input.path);
|
|
105
|
+
}}
|
|
106
|
+
trigger={
|
|
107
|
+
<Button
|
|
108
|
+
variant="outline"
|
|
109
|
+
shape="square"
|
|
110
|
+
aria-label="Open"
|
|
111
|
+
class="shrink-0 md:!size-auto md:!h-9 md:min-w-[4.5rem] md:gap-1.5 md:rounded-lg md:px-3"
|
|
112
|
+
icon={<Icon icon={ChevronDown} class="size-4" />}
|
|
113
|
+
>
|
|
114
|
+
<span class="hidden md:inline">Open In</span>
|
|
115
|
+
</Button>
|
|
116
|
+
}
|
|
117
|
+
items={items}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export function DocArticle(props: { path: string; children: RawHtml | string; class?: string }) {
|
|
125
|
+
const meta = contentMeta[props.path.replace(/\/$/, "") || "/"];
|
|
126
|
+
const articleClasses =
|
|
127
|
+
props.class ?? (meta?.type === "custom" ? "w-full max-w-none" : articleClass);
|
|
128
|
+
|
|
129
|
+
const isCustom = meta?.type === "custom";
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div
|
|
133
|
+
class={cx(
|
|
134
|
+
"flex min-h-0 w-full flex-1 flex-col",
|
|
135
|
+
!isCustom && "mx-auto max-w-4xl pt-14 md:pt-0",
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
{isCustom ? null : <DocToolbar path={props.path} />}
|
|
139
|
+
<article class={cx(articleClasses, "flex-1")}>{props.children}</article>
|
|
140
|
+
{isCustom ? null : <DocPager path={props.path} />}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/** @jsxImportSource ilha */
|
|
2
|
+
import { Dialog, Icon } from "areia";
|
|
3
|
+
import ilha from "ilha";
|
|
4
|
+
import { Search, X } from "lucide";
|
|
5
|
+
import { closeSearch, openSearch, searchOpen, searchQuery, toggleSearch } from "./search-store";
|
|
6
|
+
import { attachPortaledSearchBridge, onSearchPortalMounted } from "./search-portal-sync";
|
|
7
|
+
|
|
8
|
+
const HOST_ID = "imprensa-global-search-host";
|
|
9
|
+
|
|
10
|
+
function syncSearchHostVisibility(open = searchOpen()) {
|
|
11
|
+
document.getElementById(HOST_ID)?.toggleAttribute("hidden", !open);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function mountSearchShortcuts() {
|
|
15
|
+
const onTriggerClick = (event: MouseEvent) => {
|
|
16
|
+
const target = event.target;
|
|
17
|
+
if (!(target instanceof Element) || !target.closest("[data-search-trigger]")) return;
|
|
18
|
+
event.preventDefault();
|
|
19
|
+
event.stopPropagation();
|
|
20
|
+
openSearch();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const onKeydown = (event: KeyboardEvent) => {
|
|
24
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
toggleSearch();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (event.key === "Escape" && searchOpen()) {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
closeSearch();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
document.addEventListener("click", onTriggerClick, true);
|
|
36
|
+
window.addEventListener("keydown", onKeydown);
|
|
37
|
+
return () => {
|
|
38
|
+
document.removeEventListener("click", onTriggerClick, true);
|
|
39
|
+
window.removeEventListener("keydown", onKeydown);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const GlobalSearch = ilha
|
|
44
|
+
.effect(() => {
|
|
45
|
+
const open = searchOpen();
|
|
46
|
+
syncSearchHostVisibility(open);
|
|
47
|
+
if (!open) return;
|
|
48
|
+
let tries = 0;
|
|
49
|
+
const restore = () => {
|
|
50
|
+
const portal = document.querySelector<HTMLElement>(
|
|
51
|
+
'[data-slot="dialog-portal"][data-state="open"]',
|
|
52
|
+
);
|
|
53
|
+
if (portal) {
|
|
54
|
+
onSearchPortalMounted(portal, searchQuery);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (++tries < 24) requestAnimationFrame(restore);
|
|
58
|
+
};
|
|
59
|
+
requestAnimationFrame(restore);
|
|
60
|
+
})
|
|
61
|
+
.onMount(() => {
|
|
62
|
+
const unlisten = mountSearchShortcuts();
|
|
63
|
+
const detachBridge = attachPortaledSearchBridge({
|
|
64
|
+
isOpen: searchOpen,
|
|
65
|
+
getQuery: searchQuery,
|
|
66
|
+
setQuery: (value) => searchQuery(value),
|
|
67
|
+
onClose: closeSearch,
|
|
68
|
+
onNavigate: (path) => window.location.assign(path),
|
|
69
|
+
});
|
|
70
|
+
return () => {
|
|
71
|
+
unlisten();
|
|
72
|
+
detachBridge();
|
|
73
|
+
};
|
|
74
|
+
})
|
|
75
|
+
.render(() => (
|
|
76
|
+
<Dialog
|
|
77
|
+
bind:open={searchOpen as never}
|
|
78
|
+
closeOnClickOutside
|
|
79
|
+
closeOnEscape
|
|
80
|
+
lockScroll
|
|
81
|
+
onOpenChange={(open: boolean) => {
|
|
82
|
+
if (!open) closeSearch();
|
|
83
|
+
}}
|
|
84
|
+
onPortalMounted={(container) => onSearchPortalMounted(container, searchQuery)}
|
|
85
|
+
>
|
|
86
|
+
<Dialog.Trigger as="button" type="button" class="sr-only" tabIndex={-1} aria-hidden="true">
|
|
87
|
+
Search
|
|
88
|
+
</Dialog.Trigger>
|
|
89
|
+
<Dialog.Portal>
|
|
90
|
+
<Dialog.Overlay class="backdrop-blur-sm" />
|
|
91
|
+
<Dialog.Content size="lg" class="grid gap-0 p-0 min-w-[min(100%,32rem)]">
|
|
92
|
+
<div data-imprensa-search-dialog data-slot="command" data-label="Search documentation">
|
|
93
|
+
<Dialog.Title class="sr-only">Search documentation</Dialog.Title>
|
|
94
|
+
<div
|
|
95
|
+
data-slot="command-input-wrapper"
|
|
96
|
+
class="flex h-12 items-center gap-2 border-b border-areia-border px-3"
|
|
97
|
+
>
|
|
98
|
+
<Icon icon={Search} class="size-4 shrink-0 text-areia-foreground/60" />
|
|
99
|
+
<input
|
|
100
|
+
data-search-input
|
|
101
|
+
data-slot="command-input"
|
|
102
|
+
type="search"
|
|
103
|
+
autocomplete="off"
|
|
104
|
+
spellcheck={false}
|
|
105
|
+
placeholder="Search documentation..."
|
|
106
|
+
class="h-full flex-1 bg-transparent text-sm leading-none outline-none"
|
|
107
|
+
/>
|
|
108
|
+
<Dialog.Close
|
|
109
|
+
as="button"
|
|
110
|
+
type="button"
|
|
111
|
+
class="flex size-7 items-center justify-center rounded-md hover:bg-areia-control-hover"
|
|
112
|
+
aria-label="Close search"
|
|
113
|
+
>
|
|
114
|
+
<Icon icon={X} class="size-4" />
|
|
115
|
+
</Dialog.Close>
|
|
116
|
+
</div>
|
|
117
|
+
<div data-slot="command-list" class="max-h-96 overflow-y-auto p-2 hidden" />
|
|
118
|
+
</div>
|
|
119
|
+
</Dialog.Content>
|
|
120
|
+
</Dialog.Portal>
|
|
121
|
+
</Dialog>
|
|
122
|
+
));
|
|
123
|
+
|
|
124
|
+
let globalSearchUnmount: (() => void) | undefined;
|
|
125
|
+
|
|
126
|
+
export function ensureGlobalSearchMounted() {
|
|
127
|
+
if (typeof document === "undefined" || globalSearchUnmount) return;
|
|
128
|
+
|
|
129
|
+
let host = document.getElementById(HOST_ID);
|
|
130
|
+
if (!host) {
|
|
131
|
+
host = document.createElement("div");
|
|
132
|
+
host.id = HOST_ID;
|
|
133
|
+
host.hidden = true;
|
|
134
|
+
document.body.appendChild(host);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
syncSearchHostVisibility(false);
|
|
138
|
+
|
|
139
|
+
const result = GlobalSearch.mount(host);
|
|
140
|
+
globalSearchUnmount = typeof result === "function" ? result : () => {};
|
|
141
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** @jsxImportSource ilha */
|
|
2
|
+
import type { RawHtml } from "ilha";
|
|
3
|
+
import { raw } from "ilha";
|
|
4
|
+
import type { ImprensaSocialService } from "../docs/socials";
|
|
5
|
+
|
|
6
|
+
export type { ImprensaSocialService } from "../docs/socials";
|
|
7
|
+
|
|
8
|
+
function escapeAttribute(value: string) {
|
|
9
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function svg(className: string | undefined, path: string) {
|
|
13
|
+
const classAttr = className ? ` class="${escapeAttribute(className)}"` : "";
|
|
14
|
+
return raw(
|
|
15
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"${classAttr}><path d="${path}"/></svg>`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PATHS = {
|
|
20
|
+
github:
|
|
21
|
+
"M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2z",
|
|
22
|
+
x: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.744l7.737-8.835L1.254 2.25H8.08l4.253 5.622 5.911-5.622zm-1.161 17.52h1.833L7.084 4.126H5.117z",
|
|
23
|
+
discord:
|
|
24
|
+
"M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z",
|
|
25
|
+
} as const satisfies Record<ImprensaSocialService, string>;
|
|
26
|
+
|
|
27
|
+
/** Social brand icon — `icon` matches `imprensa({ socials })` `service` values. */
|
|
28
|
+
export function Icon(props: { icon: ImprensaSocialService; class?: string }): RawHtml {
|
|
29
|
+
return svg(props.class, PATHS[props.icon]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @deprecated Use `<Icon icon="github" />` from `imprensa/icons`. */
|
|
33
|
+
export function GithubIcon(props: { class?: string }) {
|
|
34
|
+
return Icon({ icon: "github", class: props.class });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @deprecated Use `<Icon icon="x" />`. */
|
|
38
|
+
export function XIcon(props: { class?: string }) {
|
|
39
|
+
return Icon({ icon: "x", class: props.class });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @deprecated Use `<Icon icon="discord" />`. */
|
|
43
|
+
export function DiscordIcon(props: { class?: string }) {
|
|
44
|
+
return Icon({ icon: "discord", class: props.class });
|
|
45
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RawHtml } from "ilha";
|
|
2
|
+
|
|
3
|
+
/** JSX nodes returned from Ilha render helpers in imprensa components. */
|
|
4
|
+
export type ImprensaUiNode = RawHtml | string | number | boolean | null | undefined;
|
|
5
|
+
|
|
6
|
+
export type ImprensaUiTree = ImprensaUiNode | ImprensaUiTree[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
LogoButton,
|
|
3
|
+
ThemeToggle,
|
|
4
|
+
SearchOverlay,
|
|
5
|
+
SearchNavbarTrigger,
|
|
6
|
+
SearchSidebarTrigger,
|
|
7
|
+
} from "./search";
|
|
8
|
+
export { Sidebar } from "./sidebar";
|
|
9
|
+
export { ContentLayout, RootLayout } from "./layout";
|
|
10
|
+
export { Preview } from "./preview";
|
|
11
|
+
export { Snippet } from "./snippet";
|
|
12
|
+
export { DocArticle, DocToolbar } from "./doc-toolbar";
|
|
13
|
+
export { DocPager, getAdjacentDocs, type DocNavItem } from "./doc-pager";
|