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.
@@ -0,0 +1,102 @@
1
+ /** @jsxImportSource ilha */
2
+ import { Button, Icon, LinkButton, Popover } from "areia";
3
+ import { ExternalLink } from "lucide";
4
+ import { contentTree, type ContentTreeNode } from "imprensa/mdx";
5
+ import { NavFooterBar } from "./nav-footer-bar";
6
+ import { LogoButton } from "./search";
7
+ import type { ImprensaUiTree } from "./ilha-ui";
8
+ import { cx } from "./classes";
9
+ import { treeIndentClass } from "./tree-indent";
10
+
11
+ const ACTIVE_LINK_CLASS = "border-areia-primary text-areia-primary ring-1 ring-areia-primary/30";
12
+
13
+ function normalizePath(path: string) {
14
+ return path.replace(/\/$/, "") || "/";
15
+ }
16
+
17
+ function renderMobileTree(
18
+ nodes: ContentTreeNode[],
19
+ currentPath: string,
20
+ depth = 0,
21
+ ): ImprensaUiTree[] {
22
+ return nodes.flatMap((node) => {
23
+ const href = node.type === "link" ? node.link : node.path;
24
+ const active =
25
+ node.path && node.type !== "link" ? normalizePath(node.path) === currentPath : false;
26
+ const items: ImprensaUiTree[] = [];
27
+ if (href) {
28
+ items.push(
29
+ <Popover.Close>
30
+ <div class={treeIndentClass(depth)}>
31
+ <LinkButton
32
+ href={href}
33
+ external={node.external}
34
+ variant={active ? "outline" : "ghost"}
35
+ class={cx(
36
+ "w-full justify-start",
37
+ node.type === "link" && node.external && "justify-between",
38
+ active && ACTIVE_LINK_CLASS,
39
+ )}
40
+ >
41
+ <span class="min-w-0 truncate">{node.title}</span>
42
+ {node.type === "link" && node.external ? (
43
+ <Icon icon={ExternalLink} class="size-3.5 shrink-0" />
44
+ ) : null}
45
+ </LinkButton>
46
+ </div>
47
+ </Popover.Close>,
48
+ );
49
+ } else {
50
+ items.push(
51
+ <span
52
+ class={cx("mt-2 block text-sm font-medium text-areia-subtle", treeIndentClass(depth))}
53
+ >
54
+ {node.title}
55
+ </span>,
56
+ );
57
+ }
58
+ if (node.children.length > 0)
59
+ items.push(...renderMobileTree(node.children, currentPath, depth + 1));
60
+ return items;
61
+ });
62
+ }
63
+
64
+ export function MobileNavigationPopover(props: { currentPath: string }) {
65
+ const currentPath = normalizePath(props.currentPath);
66
+ return (
67
+ <Popover
68
+ side="bottom"
69
+ align="start"
70
+ contentClass="z-50"
71
+ trigger={
72
+ <Button shape="square" aria-label="Open navigation">
73
+ <svg
74
+ xmlns="http://www.w3.org/2000/svg"
75
+ class="size-5"
76
+ viewBox="0 0 24 24"
77
+ fill="none"
78
+ stroke="currentColor"
79
+ stroke-width="2"
80
+ stroke-linecap="round"
81
+ stroke-linejoin="round"
82
+ >
83
+ <line x1="3" y1="6" x2="21" y2="6" />
84
+ <line x1="3" y1="12" x2="21" y2="12" />
85
+ <line x1="3" y1="18" x2="21" y2="18" />
86
+ </svg>
87
+ </Button>
88
+ }
89
+ content={
90
+ <div class="flex w-64 max-h-[70dvh] flex-col gap-2 p-1">
91
+ <div class="flex shrink-0 items-center px-1 pb-1">
92
+ <LogoButton />
93
+ </div>
94
+ <nav class="min-h-0 flex-1 overflow-y-auto flex flex-col gap-1">
95
+ {renderMobileTree(contentTree, currentPath)}
96
+ </nav>
97
+ <NavFooterBar />
98
+ </div>
99
+ }
100
+ />
101
+ );
102
+ }
@@ -0,0 +1,34 @@
1
+ /** @jsxImportSource ilha */
2
+ import { LinkButton } from "areia";
3
+ import { socials } from "imprensa/config";
4
+ import { cx } from "./classes";
5
+ import { Icon } from "./icons";
6
+ import { ThemeToggle } from "./search";
7
+
8
+ export function NavFooterBar(props: { class?: string }) {
9
+ return (
10
+ <div
11
+ class={cx(
12
+ "flex shrink-0 items-center justify-between rounded-lg border border-areia-border bg-areia-surface-muted/60 p-1",
13
+ props.class,
14
+ )}
15
+ >
16
+ <div class="flex items-center gap-0">
17
+ {socials.map((s) => (
18
+ <LinkButton
19
+ href={s.url}
20
+ shape="square"
21
+ variant="ghost"
22
+ icon={<Icon icon={s.service} class="size-4" />}
23
+ external
24
+ aria-label={s.service}
25
+ class="shrink-0"
26
+ />
27
+ ))}
28
+ </div>
29
+ <div class="ml-auto">
30
+ <ThemeToggle />
31
+ </div>
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,83 @@
1
+ /** @jsxImportSource ilha */
2
+ import ilha from "ilha";
3
+ import { preview as previewConfig, shikiThemes } from "imprensa/config";
4
+ import type { ImprensaShikiHighlighter } from "../core/shiki-types";
5
+
6
+ const DEFAULT_IMPORTMAP = { imports: {} as Record<string, string> };
7
+
8
+ const DEFAULT_HEAD = ``;
9
+
10
+ function makeIframeDoc(code: string) {
11
+ const script = code
12
+ .replace(/^export default /m, "const __island = ")
13
+ .replace(/^export const (\w+)/m, "const $1");
14
+
15
+ const importmap = previewConfig.importmap
16
+ ? { imports: { ...DEFAULT_IMPORTMAP.imports, ...JSON.parse(previewConfig.importmap).imports } }
17
+ : DEFAULT_IMPORTMAP;
18
+
19
+ const head = previewConfig.head ?? DEFAULT_HEAD;
20
+
21
+ return `<!DOCTYPE html>
22
+ <html>
23
+ <head>
24
+ <meta charset="utf-8">
25
+ <script>const localStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {} };</script>
26
+ <script type="importmap">${JSON.stringify(importmap)}</script>
27
+ <script type="module" src="https://esm.sh/tsx"></script>
28
+ ${head}
29
+ </head>
30
+ <body>
31
+ <div id="root"></div>
32
+ <script type="text/babel" data-type="module">
33
+ ${script}
34
+ const __resolved = typeof __island !== 'undefined' ? __island : typeof App !== 'undefined' ? App : undefined;
35
+ if (__resolved?.mount) __resolved.mount(document.getElementById('root'));
36
+ </script>
37
+ </body>
38
+ </html>`;
39
+ }
40
+
41
+ export const Preview = ilha
42
+ .input<{ code64: string }>()
43
+ .onMount(({ host, input }) => {
44
+ const code = atob(input.code64);
45
+ const wrapper = host.querySelector<HTMLElement>(".preview-wrapper")!;
46
+
47
+ const pre = document.createElement("pre");
48
+ pre.className =
49
+ "rounded-lg bg-areia-surface-muted border border-areia-border p-4 overflow-x-auto text-sm";
50
+ pre.innerHTML = `<code>${code.replace(/&/g, "&amp;").replace(/</g, "&lt;")}</code>`;
51
+ wrapper.appendChild(pre);
52
+
53
+ const iframe = document.createElement("iframe");
54
+ iframe.className = "rounded-lg border border-areia-border w-full min-h-32";
55
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
56
+ iframe.srcdoc = makeIframeDoc(code);
57
+ wrapper.appendChild(iframe);
58
+
59
+ iframe.addEventListener("load", () => {
60
+ const syncDark = () => {
61
+ const dark = document.documentElement.classList.contains("dark");
62
+ iframe.contentDocument?.documentElement.classList.toggle("dark", dark);
63
+ };
64
+ syncDark();
65
+ new MutationObserver(syncDark).observe(document.documentElement, {
66
+ attributeFilter: ["class"],
67
+ });
68
+ });
69
+
70
+ void import("imprensa/shiki").then(async ({ shiki }) => {
71
+ const previewHighlighter = shiki as ImprensaShikiHighlighter;
72
+ await previewHighlighter.loadLanguage("tsx");
73
+ const div = document.createElement("div");
74
+ div.className =
75
+ "rounded-lg overflow-hidden border border-areia-border text-sm [&_pre]:!p-4 [&_pre]:!m-0 [&_pre]:overflow-x-auto";
76
+ div.innerHTML = previewHighlighter.codeToHtml(code, {
77
+ lang: "tsx",
78
+ themes: shikiThemes,
79
+ });
80
+ wrapper.replaceChild(div, pre);
81
+ });
82
+ })
83
+ .render(() => <div class="not-prose flex flex-col gap-4 preview-wrapper" />);
@@ -0,0 +1,47 @@
1
+ import MiniSearch from "minisearch";
2
+ import { searchDocuments } from "imprensa/mdx";
3
+
4
+ type SearchDocument = (typeof searchDocuments)[number];
5
+ export type SearchResult = Pick<SearchDocument, "id" | "title" | "path" | "text">;
6
+
7
+ const searchIndex = new MiniSearch<SearchDocument>({
8
+ fields: ["title", "text"],
9
+ storeFields: ["title", "path", "text"],
10
+ searchOptions: { boost: { title: 3 }, fuzzy: 0.2, prefix: true },
11
+ });
12
+ searchIndex.addAll(searchDocuments);
13
+
14
+ function getQueryTerms(query: string) {
15
+ return query.trim().toLowerCase().split(/\s+/).filter(Boolean);
16
+ }
17
+
18
+ function getTextMatchIndex(text: string, query: string) {
19
+ const normalizedText = text.toLowerCase();
20
+ return getQueryTerms(query)
21
+ .map((term) => normalizedText.indexOf(term))
22
+ .filter((index) => index >= 0)
23
+ .sort((a, b) => a - b)[0];
24
+ }
25
+
26
+ export function getMatchedExcerpt(text: string, query: string) {
27
+ const firstMatch = getTextMatchIndex(text, query);
28
+ if (firstMatch === undefined) return undefined;
29
+ const start = Math.max(0, firstMatch - 60);
30
+ const end = Math.min(text.length, firstMatch + 140);
31
+ const excerpt = text.slice(start, end).trim();
32
+ return `${start > 0 ? "…" : ""}${excerpt}${end < text.length ? "…" : ""}`;
33
+ }
34
+
35
+ export function getSearchResults(query: string): SearchResult[] {
36
+ const trimmedQuery = query.trim();
37
+ if (!trimmedQuery) return [];
38
+ return searchIndex
39
+ .search(trimmedQuery)
40
+ .slice(0, 8)
41
+ .map((result) => ({
42
+ id: result.id,
43
+ title: result.title,
44
+ path: result.path,
45
+ text: result.text,
46
+ }));
47
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Areia Dialog portals to document.body; Ilha does not morph that subtree.
3
+ * Syncs command-list + keyboard nav until Areia supports portal={false} on body hosts.
4
+ */
5
+ import { Command as CommandPrimitive } from "@areia/slots";
6
+ import { getMatchedExcerpt, getSearchResults, type SearchResult } from "./search-core";
7
+
8
+ function escapeHtml(text: string) {
9
+ return text
10
+ .replace(/&/g, "&amp;")
11
+ .replace(/</g, "&lt;")
12
+ .replace(/>/g, "&gt;")
13
+ .replace(/"/g, "&quot;");
14
+ }
15
+
16
+ function highlightPlain(text: string, query: string) {
17
+ const terms = query
18
+ .trim()
19
+ .toLowerCase()
20
+ .split(/\s+/)
21
+ .filter((t) => t.length > 1);
22
+ if (terms.length === 0) return escapeHtml(text);
23
+ const pattern = new RegExp(
24
+ `(${terms.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`,
25
+ "gi",
26
+ );
27
+ return text
28
+ .split(pattern)
29
+ .map((part) =>
30
+ terms.includes(part.toLowerCase())
31
+ ? `<mark class="rounded bg-areia-primary px-0.5 text-areia-primary-foreground">${escapeHtml(part)}</mark>`
32
+ : escapeHtml(part),
33
+ )
34
+ .join("");
35
+ }
36
+
37
+ export function findOpenSearchCommandRoot(): HTMLElement | null {
38
+ const portal = document.querySelector('[data-slot="dialog-portal"][data-state="open"]');
39
+ if (portal) {
40
+ const inPortal = portal.querySelector<HTMLElement>(
41
+ '[data-imprensa-search-dialog][data-slot="command"]',
42
+ );
43
+ if (inPortal) return inPortal;
44
+ }
45
+ return document.querySelector<HTMLElement>('[data-imprensa-search-dialog][data-slot="command"]');
46
+ }
47
+
48
+ function findListIn(root: HTMLElement | null): HTMLElement | null {
49
+ if (!root) return null;
50
+ return root.querySelector('[data-slot="command-list"]');
51
+ }
52
+
53
+ function findInputIn(root: HTMLElement | null): HTMLInputElement | null {
54
+ if (!root) return null;
55
+ return root.querySelector("[data-search-input]");
56
+ }
57
+
58
+ export function syncPortaledSearchList(
59
+ query: string,
60
+ results: SearchResult[],
61
+ root?: HTMLElement | null,
62
+ ) {
63
+ const commandRoot = root ?? findOpenSearchCommandRoot();
64
+ const list = findListIn(commandRoot);
65
+ if (!list) return false;
66
+
67
+ const trimmed = query.trim();
68
+ if (!trimmed) {
69
+ list.classList.add("hidden");
70
+ list.innerHTML = "";
71
+ return true;
72
+ }
73
+
74
+ list.classList.remove("hidden");
75
+
76
+ if (results.length === 0) {
77
+ list.innerHTML = `<div data-slot="command-group">
78
+ <div data-slot="command-group-heading" class="px-3 py-1 text-xs font-medium text-areia-foreground/60">Pages</div>
79
+ <div data-slot="command-empty" class="px-3 py-6 text-center text-sm text-areia-foreground/60">No results found.</div>
80
+ </div>`;
81
+ return true;
82
+ }
83
+
84
+ const items = results
85
+ .map((result) => {
86
+ const excerpt = getMatchedExcerpt(result.text, query);
87
+ const titleHtml = highlightPlain(result.title, query);
88
+ const excerptHtml = excerpt
89
+ ? `<div class="mt-1 line-clamp-2 text-xs text-areia-foreground/60">${highlightPlain(excerpt, query)}</div>`
90
+ : "";
91
+ return `<div data-slot="command-item" data-value="${escapeHtml(result.path)}" data-keywords="${escapeHtml(`${result.title}\n${result.text}`)}" class="cursor-pointer rounded-lg px-3 py-2 text-sm data-selected:bg-areia-control-hover">
92
+ <div class="font-medium">${titleHtml}</div>
93
+ ${excerptHtml}
94
+ </div>`;
95
+ })
96
+ .join("");
97
+
98
+ list.innerHTML = `<div data-slot="command-group">
99
+ <div data-slot="command-group-heading" class="px-3 py-1 text-xs font-medium text-areia-foreground/60">Pages</div>
100
+ ${items}
101
+ </div>`;
102
+ return true;
103
+ }
104
+
105
+ export type PortaledSearchBridgeOptions = {
106
+ isOpen: () => boolean;
107
+ getQuery: () => string;
108
+ setQuery: (value: string) => void;
109
+ onClose: () => void;
110
+ onNavigate: (path: string) => void;
111
+ };
112
+
113
+ function syncInputFromSaved(root: HTMLElement, saved: string) {
114
+ const input = findInputIn(root);
115
+ if (input && input.value !== saved) input.value = saved;
116
+ }
117
+
118
+ function searchDialogOpenInDom(): boolean {
119
+ return findOpenSearchCommandRoot() !== null;
120
+ }
121
+
122
+ export function attachPortaledSearchBridge(options: PortaledSearchBridgeOptions) {
123
+ const { isOpen, getQuery, setQuery, onClose, onNavigate } = options;
124
+ let commandDestroy = () => {};
125
+ let commandRoot: HTMLElement | null = null;
126
+
127
+ const repaint = (query: string, root?: HTMLElement | null) => {
128
+ syncPortaledSearchList(query, getSearchResults(query), root ?? findOpenSearchCommandRoot());
129
+ };
130
+
131
+ const ensureCommand = (root: HTMLElement) => {
132
+ if (root === commandRoot) return;
133
+ commandDestroy();
134
+ commandRoot = root;
135
+ const saved = getQuery();
136
+ syncInputFromSaved(root, saved);
137
+ const controller = CommandPrimitive.createCommand(root, {
138
+ label: "Search documentation",
139
+ loop: true,
140
+ shouldFilter: false,
141
+ onSearchChange: (value: string) => {
142
+ setQuery(value);
143
+ repaint(value, root);
144
+ },
145
+ onSelect: (value: string) => {
146
+ onClose();
147
+ onNavigate(value);
148
+ },
149
+ });
150
+ commandDestroy = () => controller.destroy();
151
+ repaint(saved, root);
152
+ findInputIn(root)?.focus();
153
+ };
154
+
155
+ const active = () => isOpen() || searchDialogOpenInDom();
156
+
157
+ const handleQueryFromInput = (input: HTMLInputElement) => {
158
+ if (!input.closest("[data-imprensa-search-dialog]")) return;
159
+ const root = input.closest<HTMLElement>('[data-slot="command"]');
160
+ if (!root) return;
161
+ const query = input.value;
162
+ setQuery(query);
163
+ repaint(query, root);
164
+ ensureCommand(root);
165
+ };
166
+
167
+ const onInput = (event: Event) => {
168
+ if (!active()) return;
169
+ const target = event.target;
170
+ if (!(target instanceof HTMLInputElement) || !target.matches("[data-search-input]")) return;
171
+ handleQueryFromInput(target);
172
+ };
173
+
174
+ const onClick = (event: MouseEvent) => {
175
+ if (!active()) return;
176
+ const item = (event.target as Element | null)?.closest<HTMLElement>(
177
+ '[data-slot="command-item"]',
178
+ );
179
+ if (!item?.closest("[data-imprensa-search-dialog]")) return;
180
+ const path = item.getAttribute("data-value");
181
+ if (!path) return;
182
+ event.preventDefault();
183
+ onClose();
184
+ onNavigate(path);
185
+ };
186
+
187
+ document.addEventListener("input", onInput, true);
188
+ document.addEventListener("click", onClick, true);
189
+
190
+ const tick = () => {
191
+ if (!active()) {
192
+ if (commandRoot) {
193
+ commandDestroy();
194
+ commandDestroy = () => {};
195
+ commandRoot = null;
196
+ }
197
+ return;
198
+ }
199
+ const root = findOpenSearchCommandRoot();
200
+ if (!root) return;
201
+ const saved = getQuery();
202
+ syncInputFromSaved(root, saved);
203
+ ensureCommand(root);
204
+ repaint(saved, root);
205
+ };
206
+
207
+ const interval = window.setInterval(tick, 50);
208
+ tick();
209
+
210
+ return () => {
211
+ window.clearInterval(interval);
212
+ document.removeEventListener("input", onInput, true);
213
+ document.removeEventListener("click", onClick, true);
214
+ commandDestroy();
215
+ commandRoot = null;
216
+ };
217
+ }
218
+
219
+ export function onSearchPortalMounted(container: HTMLElement, getSavedQuery: () => string) {
220
+ const root = container.querySelector<HTMLElement>(
221
+ '[data-imprensa-search-dialog][data-slot="command"]',
222
+ );
223
+ if (!root) return;
224
+ const input = findInputIn(root);
225
+ const query = getSavedQuery();
226
+ if (input) input.value = query;
227
+ syncPortaledSearchList(query, getSearchResults(query), root);
228
+ input?.focus();
229
+ if (query.length > 0) {
230
+ requestAnimationFrame(() => {
231
+ input?.setSelectionRange(query.length, query.length);
232
+ });
233
+ }
234
+ }
@@ -0,0 +1,34 @@
1
+ import { createStore } from "@ilha/store";
2
+
3
+ export type ImprensaSearchState = {
4
+ open: boolean;
5
+ query: string;
6
+ };
7
+
8
+ export const searchStore = createStore<ImprensaSearchState>({
9
+ open: false,
10
+ query: "",
11
+ });
12
+
13
+ /** Hoisted for `bind:open` / portaled bridge — same alien-signals graph as islands. */
14
+ export const searchOpen = searchStore.bind((s) => s.open);
15
+ export const searchQuery = searchStore.bind((s) => s.query);
16
+
17
+ export function closeSearch() {
18
+ searchOpen(false);
19
+ }
20
+
21
+ export function openSearch() {
22
+ searchOpen(true);
23
+ }
24
+
25
+ export function toggleSearch() {
26
+ if (searchOpen()) closeSearch();
27
+ else openSearch();
28
+ }
29
+
30
+ /** Legacy alias for store bind accessors (optional app code). */
31
+ export type SearchBindAccessors = {
32
+ dialogOpen: typeof searchOpen;
33
+ query: typeof searchQuery;
34
+ };
@@ -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
+ }