imprensa 0.1.2 → 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 +12 -0
- 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
|
@@ -493,6 +493,16 @@ function isAppPageFile(file, root) {
|
|
|
493
493
|
const relative = path.relative(path.join(root, "src/pages"), file).replace(/\\/g, "/");
|
|
494
494
|
return !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
495
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
|
+
}
|
|
496
506
|
function createImprensaVitePlugins(options = {}) {
|
|
497
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;
|
|
498
508
|
const { rehypePlugins, remarkPlugins, ...restMdxOptions } = mdxOptions;
|
|
@@ -601,6 +611,8 @@ export const shikiThemes = ${JSON.stringify(shikiThemes)};`;
|
|
|
601
611
|
return IMPRENSA_VIRTUAL_RUNTIME.replace("__SHIKI_THEMES__", JSON.stringify(shikiThemes));
|
|
602
612
|
},
|
|
603
613
|
transform(code, id) {
|
|
614
|
+
const cssPatch = patchImprensaDefaultCss(code, id);
|
|
615
|
+
if (cssPatch) return cssPatch;
|
|
604
616
|
if (/\.mdx?$/.test(id) && code.startsWith("---")) {
|
|
605
617
|
const end = code.indexOf("\n---", 3);
|
|
606
618
|
if (end !== -1) return {
|
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";
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/** @jsxImportSource ilha */
|
|
2
|
+
import { defineLayout, routeHash } from "@ilha/router";
|
|
3
|
+
import { Resizable, Toaster } from "areia";
|
|
4
|
+
import ilha from "ilha";
|
|
5
|
+
|
|
6
|
+
import { Sidebar } from "./sidebar";
|
|
7
|
+
import { DEFAULT_SIDEBAR_LAYOUT, readSidebarLayout, writeSidebarLayout } from "./sidebar-layout";
|
|
8
|
+
|
|
9
|
+
const initialSidebarLayout =
|
|
10
|
+
typeof window !== "undefined" ? readSidebarLayout() : DEFAULT_SIDEBAR_LAYOUT;
|
|
11
|
+
|
|
12
|
+
function layoutsNearlyEqual(a: number[], b: [number, number]) {
|
|
13
|
+
return a.length === 2 && Math.abs(a[0] - b[0]) < 0.05 && Math.abs(a[1] - b[1]) < 0.05;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function mountSidebarLayoutPersistence(host: Element) {
|
|
17
|
+
let resizable: HTMLElement | null = null;
|
|
18
|
+
let persist = false;
|
|
19
|
+
const stored = readSidebarLayout();
|
|
20
|
+
|
|
21
|
+
const onLayoutChange = (event: Event) => {
|
|
22
|
+
if (!persist) return;
|
|
23
|
+
const { layout } = (event as CustomEvent<{ layout: number[] }>).detail;
|
|
24
|
+
if (!Array.isArray(layout) || layout.length !== 2) return;
|
|
25
|
+
writeSidebarLayout(layout);
|
|
26
|
+
document.documentElement.dataset.imprensaSidebarLayout = "1";
|
|
27
|
+
document.documentElement.style.setProperty("--imprensa-sidebar-pct", String(layout[0]));
|
|
28
|
+
document.documentElement.style.setProperty("--imprensa-content-pct", String(layout[1]));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const connect = () => {
|
|
32
|
+
resizable = host.querySelector<HTMLElement>('[data-slot="resizable"]');
|
|
33
|
+
if (!resizable) return;
|
|
34
|
+
|
|
35
|
+
resizable.addEventListener("resizable:change", onLayoutChange);
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
document.documentElement.dataset.imprensaSidebarLayout !== "1" &&
|
|
39
|
+
!layoutsNearlyEqual(stored, DEFAULT_SIDEBAR_LAYOUT)
|
|
40
|
+
) {
|
|
41
|
+
resizable.dispatchEvent(
|
|
42
|
+
new CustomEvent("resizable:set", { detail: { layout: [...stored] }, bubbles: true }),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
persist = true;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
queueMicrotask(connect);
|
|
49
|
+
|
|
50
|
+
return () => {
|
|
51
|
+
resizable?.removeEventListener("resizable:change", onLayoutChange);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const DOCS_LAYOUT_CLASS = "imprensa-docs-layout";
|
|
56
|
+
|
|
57
|
+
function mountDocsViewportLock() {
|
|
58
|
+
document.documentElement.classList.add(DOCS_LAYOUT_CLASS);
|
|
59
|
+
return () => {
|
|
60
|
+
document.documentElement.classList.remove(DOCS_LAYOUT_CLASS);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const RootLayout = defineLayout((children) =>
|
|
65
|
+
ilha.render(() => (
|
|
66
|
+
<div class="imprensa-root bg-areia-background text-areia-default">
|
|
67
|
+
<Toaster richColors closeButton />
|
|
68
|
+
<main class="imprensa-root-main">{children}</main>
|
|
69
|
+
</div>
|
|
70
|
+
)),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
export const ContentLayout = defineLayout((children) => {
|
|
74
|
+
return ilha
|
|
75
|
+
.effect(() => {
|
|
76
|
+
const hash = routeHash();
|
|
77
|
+
if (hash)
|
|
78
|
+
requestAnimationFrame(() => document.getElementById(hash.slice(1))?.scrollIntoView());
|
|
79
|
+
})
|
|
80
|
+
.onMount(({ host }) => {
|
|
81
|
+
const unlockViewport = mountDocsViewportLock();
|
|
82
|
+
const disconnectLayout = mountSidebarLayoutPersistence(host);
|
|
83
|
+
return () => {
|
|
84
|
+
disconnectLayout();
|
|
85
|
+
unlockViewport();
|
|
86
|
+
};
|
|
87
|
+
})
|
|
88
|
+
.render(() => (
|
|
89
|
+
<div class="imprensa-docs-shell flex h-dvh w-full overflow-hidden bg-areia-background text-areia-default">
|
|
90
|
+
<Resizable direction="horizontal" class="h-full min-h-0 w-full">
|
|
91
|
+
<Resizable.Panel
|
|
92
|
+
defaultSize={initialSidebarLayout[0]}
|
|
93
|
+
minSize={15}
|
|
94
|
+
maxSize={35}
|
|
95
|
+
class="imprensa-docs-sidebar-panel max-md:!hidden"
|
|
96
|
+
>
|
|
97
|
+
<Sidebar />
|
|
98
|
+
</Resizable.Panel>
|
|
99
|
+
<Resizable.Handle class="max-md:!hidden" />
|
|
100
|
+
<Resizable.Panel
|
|
101
|
+
defaultSize={initialSidebarLayout[1]}
|
|
102
|
+
minSize={50}
|
|
103
|
+
class="imprensa-docs-main-panel"
|
|
104
|
+
>
|
|
105
|
+
<div class="imprensa-docs-main-scroll flex w-full flex-col p-4">{children}</div>
|
|
106
|
+
</Resizable.Panel>
|
|
107
|
+
</Resizable>
|
|
108
|
+
</div>
|
|
109
|
+
));
|
|
110
|
+
});
|