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.
@@ -0,0 +1,140 @@
1
+ /** @jsxImportSource ilha */
2
+ import { Button, Icon, LinkButton } from "areia";
3
+ import ilha from "ilha";
4
+ import { Command, Monitor, Moon, Search, Sun } from "lucide";
5
+ import { applyThemeToHtml, getStoredTheme, setStoredTheme } from "imprensa/runtime";
6
+
7
+ export { getSearchResults, type SearchResult } from "./search-core";
8
+ export {
9
+ closeSearch,
10
+ closeSearch as closeSearchDialog,
11
+ openSearch,
12
+ searchOpen,
13
+ searchQuery,
14
+ searchStore,
15
+ toggleSearch,
16
+ type ImprensaSearchState,
17
+ type SearchBindAccessors,
18
+ } from "./search-store";
19
+
20
+ export function LogoButton() {
21
+ return (
22
+ <LinkButton href="/" class="font-semibold" icon={<img src="/logo.svg" class="size-6" />}>
23
+ Imprensa
24
+ </LinkButton>
25
+ );
26
+ }
27
+
28
+ export const ThemeToggle = ilha
29
+ .state("mode", "system" as "light" | "dark" | "system")
30
+ .onMount(({ state }) => {
31
+ const mode = getStoredTheme();
32
+ state.mode(mode);
33
+
34
+ const applySystem = () =>
35
+ applyThemeToHtml(window.matchMedia("(prefers-color-scheme: dark)").matches);
36
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
37
+ const handler = () => {
38
+ if (state.mode() === "system") applySystem();
39
+ };
40
+ mq.addEventListener("change", handler);
41
+
42
+ if (mode === "system") applySystem();
43
+ else applyThemeToHtml(mode === "dark");
44
+
45
+ return () => mq.removeEventListener("change", handler);
46
+ })
47
+ .on("button@click", ({ state }) => {
48
+ const next = state.mode() === "light" ? "dark" : state.mode() === "dark" ? "system" : "light";
49
+ state.mode(next);
50
+ setStoredTheme(next);
51
+ if (next === "system")
52
+ applyThemeToHtml(window.matchMedia("(prefers-color-scheme: dark)").matches);
53
+ else applyThemeToHtml(next === "dark");
54
+ })
55
+ .render(({ state }) => (
56
+ <Button
57
+ shape="square"
58
+ aria-label={
59
+ state.mode() === "light"
60
+ ? "Switch to dark theme"
61
+ : state.mode() === "dark"
62
+ ? "Switch to system theme"
63
+ : "Switch to light theme"
64
+ }
65
+ icon={
66
+ <Icon icon={state.mode() === "light" ? Sun : state.mode() === "dark" ? Moon : Monitor} />
67
+ }
68
+ />
69
+ ));
70
+
71
+ /**
72
+ * @deprecated Replaced by `GlobalSearch` (body-mounted in `createImprensa().init()`).
73
+ * Kept so the vite plugin island registry stays stable.
74
+ */
75
+ export const SearchOverlay = ilha.render(() => <></>);
76
+
77
+ export function SearchTriggerButton(props: { class?: string }) {
78
+ return (
79
+ <Button
80
+ type="button"
81
+ data-search-trigger
82
+ aria-label="Search documentation"
83
+ icon={<Icon icon={Search} />}
84
+ class={props.class}
85
+ />
86
+ );
87
+ }
88
+
89
+ export function SearchSidebarTrigger() {
90
+ return (
91
+ <div class="relative inline-flex w-full min-w-0">
92
+ <Button
93
+ type="button"
94
+ data-search-trigger
95
+ aria-label="Search documentation"
96
+ icon={<Icon icon={Search} />}
97
+ class="w-10 justify-center sm:w-full sm:justify-start"
98
+ >
99
+ <span class="hidden sm:inline">Search</span>
100
+ <kbd class="ml-auto hidden items-center border border-areia-border py-px px-1 rounded-full md:flex">
101
+ <Icon icon={Command} class="size-3" />
102
+ <span>K</span>
103
+ </kbd>
104
+ </Button>
105
+ </div>
106
+ );
107
+ }
108
+
109
+ export function SearchMobileTriggerButton() {
110
+ return (
111
+ <Button
112
+ type="button"
113
+ shape="square"
114
+ data-search-trigger
115
+ aria-label="Search documentation"
116
+ icon={<Icon icon={Search} />}
117
+ class="shrink-0"
118
+ />
119
+ );
120
+ }
121
+
122
+ export function SearchNavbarTrigger() {
123
+ return (
124
+ <div class="relative inline-flex min-w-0">
125
+ <Button
126
+ type="button"
127
+ data-search-trigger
128
+ aria-label="Search documentation"
129
+ icon={<Icon icon={Search} />}
130
+ class="w-10 shrink-0 justify-center sm:w-auto sm:min-w-[7.5rem] sm:justify-start sm:gap-2 sm:px-3"
131
+ >
132
+ <span class="hidden sm:inline">Search</span>
133
+ <kbd class="ml-auto hidden items-center border border-areia-border py-px px-1 rounded-full md:inline-flex">
134
+ <Icon icon={Command} class="size-3" />
135
+ <span>K</span>
136
+ </kbd>
137
+ </Button>
138
+ </div>
139
+ );
140
+ }
@@ -0,0 +1,53 @@
1
+ export const SIDEBAR_STORAGE_KEY = "imprensa:sidebar-layout";
2
+ export const DEFAULT_SIDEBAR_LAYOUT: [number, number] = [20, 80];
3
+
4
+ export function readSidebarLayout(): [number, number] {
5
+ if (typeof localStorage === "undefined") return DEFAULT_SIDEBAR_LAYOUT;
6
+ try {
7
+ const layout = JSON.parse(localStorage.getItem(SIDEBAR_STORAGE_KEY) ?? "null");
8
+ if (
9
+ Array.isArray(layout) &&
10
+ layout.length === 2 &&
11
+ layout.every((size) => typeof size === "number")
12
+ ) {
13
+ return [layout[0], layout[1]];
14
+ }
15
+ } catch {
16
+ // ignore corrupt storage
17
+ }
18
+ return DEFAULT_SIDEBAR_LAYOUT;
19
+ }
20
+
21
+ export function writeSidebarLayout(layout: number[]) {
22
+ try {
23
+ localStorage.setItem(SIDEBAR_STORAGE_KEY, JSON.stringify(layout));
24
+ } catch {
25
+ // ignore quota / private mode
26
+ }
27
+ }
28
+
29
+ /** Injected into index.html so the saved split applies before first paint. */
30
+ export const SIDEBAR_LAYOUT_BOOT_SCRIPT = `<script>
31
+ (function () {
32
+ try {
33
+ var raw = localStorage.getItem(${JSON.stringify(SIDEBAR_STORAGE_KEY)});
34
+ if (!raw) return;
35
+ var layout = JSON.parse(raw);
36
+ if (!Array.isArray(layout) || layout.length !== 2) return;
37
+ var a = layout[0], b = layout[1];
38
+ if (typeof a !== "number" || typeof b !== "number") return;
39
+ var root = document.documentElement;
40
+ root.dataset.imprensaSidebarLayout = "1";
41
+ root.style.setProperty("--imprensa-sidebar-pct", String(a));
42
+ root.style.setProperty("--imprensa-content-pct", String(b));
43
+ } catch (e) {}
44
+ })();
45
+ </script>
46
+ <style id="imprensa-sidebar-layout-boot">
47
+ html[data-imprensa-sidebar-layout] [data-slot="resizable"] > [data-slot="resizable-panel"]:first-child {
48
+ flex-grow: var(--imprensa-sidebar-pct) !important;
49
+ }
50
+ html[data-imprensa-sidebar-layout] [data-slot="resizable"] > [data-slot="resizable-panel"]:last-child {
51
+ flex-grow: var(--imprensa-content-pct) !important;
52
+ }
53
+ </style>`;
@@ -0,0 +1,76 @@
1
+ /** @jsxImportSource ilha */
2
+ import { useRoute } from "@ilha/router";
3
+ import { Collapsible, Icon, LinkButton } from "areia";
4
+ import ilha from "ilha";
5
+ import { ExternalLink } from "lucide";
6
+ import { contentTree, type ContentTreeNode } from "imprensa/mdx";
7
+ import { LogoButton, SearchSidebarTrigger } from "./search";
8
+ import { NavFooterBar } from "./nav-footer-bar";
9
+ import type { ImprensaUiTree } from "./ilha-ui";
10
+ import { cx } from "./classes";
11
+ import { treeIndentClass } from "./tree-indent";
12
+
13
+ const ACTIVE_LINK_CLASS =
14
+ "border-areia-primary text-areia-primary ring-1 ring-inset ring-areia-primary/30";
15
+
16
+ function renderTree(nodes: ContentTreeNode[], currentPath: string, depth = 0): ImprensaUiTree[] {
17
+ return nodes.map((node) => {
18
+ const href = node.type === "link" ? node.link : node.path;
19
+ const active =
20
+ node.path && node.type !== "link"
21
+ ? (node.path.replace(/\/$/, "") || "/") === currentPath
22
+ : false;
23
+ const link = href ? (
24
+ <LinkButton
25
+ href={href}
26
+ external={node.external}
27
+ variant={active ? "outline" : "ghost"}
28
+ class={cx(
29
+ "w-full justify-start",
30
+ node.type === "link" && node.external && "justify-between",
31
+ active && ACTIVE_LINK_CLASS,
32
+ )}
33
+ >
34
+ <span class="min-w-0 truncate">{node.title}</span>
35
+ {node.type === "link" && node.external ? (
36
+ <Icon icon={ExternalLink} class="size-3.5 shrink-0" />
37
+ ) : null}
38
+ </LinkButton>
39
+ ) : (
40
+ <span class="text-sm font-medium text-areia-subtle">{node.title}</span>
41
+ );
42
+ return (
43
+ <div class={cx("mt-px flex flex-col gap-1", treeIndentClass(depth))}>
44
+ {node.children.length > 0 ? (
45
+ <Collapsible defaultOpen>
46
+ <Collapsible.Trigger class="w-full rounded-md px-2 py-1 text-left hover:bg-areia-control-hover">
47
+ {link}
48
+ </Collapsible.Trigger>
49
+ <Collapsible.Panel class="mt-1 flex flex-col p-px">
50
+ {renderTree(node.children, currentPath, node.path ? depth + 1 : depth)}
51
+ </Collapsible.Panel>
52
+ </Collapsible>
53
+ ) : (
54
+ link
55
+ )}
56
+ </div>
57
+ );
58
+ });
59
+ }
60
+
61
+ export const Sidebar = ilha.render(() => {
62
+ const { path } = useRoute();
63
+ const currentPath = path().replace(/\/$/, "") || "/";
64
+ return (
65
+ <div class="flex h-full min-h-0 flex-col gap-2 p-2">
66
+ <div class="flex items-center">
67
+ <LogoButton />
68
+ </div>
69
+ <SearchSidebarTrigger />
70
+ <nav class="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto overscroll-y-contain px-0.5">
71
+ {renderTree(contentTree, currentPath)}
72
+ </nav>
73
+ <NavFooterBar class="mt-auto" />
74
+ </div>
75
+ );
76
+ });
@@ -0,0 +1,38 @@
1
+ /** @jsxImportSource ilha */
2
+ import ilha from "ilha";
3
+ import { shikiThemes } from "imprensa/config";
4
+ import type { ImprensaShikiHighlighter } from "../core/shiki-types";
5
+
6
+ const WRAPPER_CLASS =
7
+ "max-w-full overflow-x-auto rounded-xl border border-areia-border text-xs leading-relaxed [&_pre]:min-w-max [&_pre]:p-4 [&_pre]:text-xs [&_pre]:leading-relaxed [&_pre]:!m-0";
8
+
9
+ function escapeHtml(code: string) {
10
+ return code.replace(/&/g, "&amp;").replace(/</g, "&lt;");
11
+ }
12
+
13
+ export const Snippet = ilha
14
+ .input<{ code: string; lang: string }>()
15
+ .onMount(({ host, input }) => {
16
+ const mount = host.querySelector<HTMLElement>("[data-imprensa-snippet]")!;
17
+ if (mount.querySelector(".shiki")) return;
18
+
19
+ const { code, lang } = input;
20
+ mount.replaceChildren();
21
+
22
+ const pre = document.createElement("pre");
23
+ pre.className =
24
+ "rounded-lg bg-areia-surface-muted border border-areia-border p-4 overflow-x-auto text-xs leading-relaxed";
25
+ pre.innerHTML = `<code>${escapeHtml(code)}</code>`;
26
+ mount.appendChild(pre);
27
+
28
+ void import("imprensa/shiki").then(async ({ shiki }) => {
29
+ const h = shiki as ImprensaShikiHighlighter;
30
+ await h.loadLanguage(lang);
31
+ const div = document.createElement("div");
32
+ div.innerHTML = h.codeToHtml(code, { lang, themes: shikiThemes });
33
+ const highlighted = div.firstElementChild;
34
+ if (!highlighted) return;
35
+ mount.replaceChildren(highlighted);
36
+ });
37
+ })
38
+ .render(() => <div class={WRAPPER_CLASS} data-imprensa-snippet />);
@@ -0,0 +1,14 @@
1
+ /** Sidebar / mobile nav tree depth → Tailwind class from `imprensa/default.css`. */
2
+ const TREE_INDENT = [
3
+ "imprensa-tree-indent-0",
4
+ "imprensa-tree-indent-1",
5
+ "imprensa-tree-indent-2",
6
+ "imprensa-tree-indent-3",
7
+ "imprensa-tree-indent-4",
8
+ "imprensa-tree-indent-5",
9
+ "imprensa-tree-indent-6",
10
+ ] as const;
11
+
12
+ export function treeIndentClass(depth: number): string {
13
+ return TREE_INDENT[Math.min(Math.max(depth, 0), TREE_INDENT.length - 1)] ?? TREE_INDENT[6];
14
+ }