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 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
@@ -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.2",
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";
@@ -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
+ });