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 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
- @source "./src/**/*.{ts,tsx}";
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
- return id === MDX_RUNTIME_CONFIG || id.endsWith("/imprensa/src/docs/mdx/runtime-config.ts") || id === MDX_SOURCE || id.endsWith("/imprensa/src/docs/mdx.ts");
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
- return code.replace(MDX_CONFIG_MARKER, `export const contentDir = ${JSON.stringify(normalizeContentDir(contentDir))};
601
- export const imprensaRepo = ${JSON.stringify(repo)};
602
- export const imprensaRepoBranch = ${JSON.stringify(repoBranch)};
603
- export const imprensaRepoPath = ${JSON.stringify(repoPath)};
604
- export const mdxRawSources = ${JSON.stringify(collectRawMdxSources(process.cwd(), contentDir))};
605
- export const headDefaults = ${JSON.stringify(headDefaults ?? null)};`);
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.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,3 @@
1
+ export function cx(...classes: Array<string | false | null | undefined>) {
2
+ return classes.filter(Boolean).join(" ");
3
+ }
@@ -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,2 @@
1
+ export { DocArticle, DocToolbar } from "./doc-toolbar";
2
+ export { DocPager, getAdjacentDocs, type DocNavItem } from "./doc-pager";
@@ -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, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
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";