imprensa 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/default.css +3 -1
- package/dist/index.mjs +35 -7
- package/package.json +2 -1
- package/src/components/classes.ts +3 -0
- package/src/components/doc-pager.tsx +97 -0
- package/src/components/doc-toolbar.tsx +143 -0
- package/src/components/doc.tsx +2 -0
- package/src/components/global-search.tsx +141 -0
- package/src/components/icons.tsx +45 -0
- package/src/components/ilha-ui.ts +6 -0
- package/src/components/index.tsx +13 -0
- package/src/components/layout.tsx +110 -0
- package/src/components/mobile-nav.tsx +102 -0
- package/src/components/nav-footer-bar.tsx +34 -0
- package/src/components/preview.tsx +83 -0
- package/src/components/search-core.tsx +47 -0
- package/src/components/search-portal-sync.ts +234 -0
- package/src/components/search-store.ts +34 -0
- package/src/components/search.tsx +140 -0
- package/src/components/sidebar-layout.ts +53 -0
- package/src/components/sidebar.tsx +76 -0
- package/src/components/snippet.tsx +38 -0
- package/src/components/tree-indent.ts +14 -0
|
@@ -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, "&").replace(/</g, "<");
|
|
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
|
+
}
|