@dreamlake/dockit 0.1.1

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.
Files changed (66) hide show
  1. package/dist/components/Callout.d.ts +11 -0
  2. package/dist/components/Callout.js +101 -0
  3. package/dist/components/ClientOnly.d.ts +4 -0
  4. package/dist/components/ClientOnly.js +11 -0
  5. package/dist/components/CodeBlock.d.ts +13 -0
  6. package/dist/components/CodeBlock.js +155 -0
  7. package/dist/components/DocFooter.d.ts +1 -0
  8. package/dist/components/DocFooter.js +63 -0
  9. package/dist/components/Preview.d.ts +16 -0
  10. package/dist/components/Preview.js +220 -0
  11. package/dist/components/SearchPalette.d.ts +8 -0
  12. package/dist/components/SearchPalette.js +678 -0
  13. package/dist/components/SearchResize.d.ts +11 -0
  14. package/dist/components/SearchResize.js +150 -0
  15. package/dist/components/Sidebar.d.ts +1 -0
  16. package/dist/components/Sidebar.js +134 -0
  17. package/dist/components/StatusTable.d.ts +19 -0
  18. package/dist/components/StatusTable.js +251 -0
  19. package/dist/components/TOC.d.ts +1 -0
  20. package/dist/components/TOC.js +674 -0
  21. package/dist/components/TabStrip.d.ts +8 -0
  22. package/dist/components/TabStrip.js +105 -0
  23. package/dist/components/ThemeProvider.d.ts +11 -0
  24. package/dist/components/ThemeProvider.js +40 -0
  25. package/dist/components/ThemeToggle.d.ts +2 -0
  26. package/dist/components/ThemeToggle.js +109 -0
  27. package/dist/components/Topbar.d.ts +9 -0
  28. package/dist/components/Topbar.js +818 -0
  29. package/dist/components/mdx-components.d.ts +30 -0
  30. package/dist/components/mdx-components.js +346 -0
  31. package/dist/index.d.ts +61 -0
  32. package/dist/index.js +81 -0
  33. package/dist/lib/navigation.d.ts +80 -0
  34. package/dist/lib/navigation.js +57 -0
  35. package/dist/lib/pagefind-search.d.ts +21 -0
  36. package/dist/lib/pagefind-search.js +201 -0
  37. package/dist/lib/search-index.d.ts +43 -0
  38. package/dist/lib/search-index.js +112 -0
  39. package/dist/lib/tabs.d.ts +23 -0
  40. package/dist/lib/tabs.js +30 -0
  41. package/dist/lib/use-hidden-toggle.d.ts +15 -0
  42. package/dist/lib/use-hidden-toggle.js +23 -0
  43. package/dist/lib/use-line-numbers.d.ts +9 -0
  44. package/dist/lib/use-line-numbers.js +7 -0
  45. package/dist/lib/use-local-storage.d.ts +15 -0
  46. package/dist/lib/use-local-storage.js +58 -0
  47. package/dist/lib/use-media-query.d.ts +5 -0
  48. package/dist/lib/use-media-query.js +16 -0
  49. package/dist/renderer/Layout.d.ts +5 -0
  50. package/dist/renderer/Layout.js +170 -0
  51. package/dist/renderer/onRenderClient.d.ts +1 -0
  52. package/dist/renderer/onRenderClient.js +23 -0
  53. package/dist/renderer/onRenderHtml.d.ts +5 -0
  54. package/dist/renderer/onRenderHtml.js +44 -0
  55. package/dist/site-config.d.ts +61 -0
  56. package/dist/site-config.js +21 -0
  57. package/dist/styles.css +310 -0
  58. package/dist/vike.d.ts +12 -0
  59. package/dist/vike.js +8 -0
  60. package/dist/vite.d.ts +2 -0
  61. package/dist/vite.js +80 -0
  62. package/package.json +82 -0
  63. package/templates/netlify.example.toml +33 -0
  64. package/templates/set-version.mjs +70 -0
  65. package/templates/version-branch.sh +16 -0
  66. package/templates/versions.example.json +15 -0
@@ -0,0 +1,201 @@
1
+ import { searchIndex, search } from "./search-index.js";
2
+ import { pages } from "./navigation.js";
3
+ let pagefind = null;
4
+ let initPromise = null;
5
+ async function getPagefind() {
6
+ if (pagefind) return pagefind;
7
+ if (initPromise) return initPromise;
8
+ initPromise = (async () => {
9
+ try {
10
+ const path = `/${["pagefind", "pagefind.js"].join("/")}`;
11
+ const pf = await import(
12
+ /* @vite-ignore */
13
+ path
14
+ );
15
+ await pf.options({ excerptLength: 200 });
16
+ pagefind = pf;
17
+ return pf;
18
+ } catch {
19
+ return null;
20
+ }
21
+ })();
22
+ return initPromise;
23
+ }
24
+ const STOP_WORDS = /* @__PURE__ */ new Set([
25
+ "a",
26
+ "an",
27
+ "and",
28
+ "are",
29
+ "as",
30
+ "at",
31
+ "be",
32
+ "but",
33
+ "by",
34
+ "for",
35
+ "if",
36
+ "in",
37
+ "into",
38
+ "is",
39
+ "it",
40
+ "no",
41
+ "not",
42
+ "of",
43
+ "on",
44
+ "or",
45
+ "such",
46
+ "that",
47
+ "the",
48
+ "their",
49
+ "then",
50
+ "there",
51
+ "these",
52
+ "they",
53
+ "this",
54
+ "to",
55
+ "was",
56
+ "will",
57
+ "with",
58
+ "how",
59
+ "what",
60
+ "when",
61
+ "where",
62
+ "which",
63
+ "who",
64
+ "do",
65
+ "does",
66
+ "can",
67
+ "could",
68
+ "should",
69
+ "would",
70
+ "has",
71
+ "have",
72
+ "had",
73
+ "my",
74
+ "your",
75
+ "its",
76
+ "our",
77
+ "from",
78
+ "about"
79
+ ]);
80
+ function extractMarked(html) {
81
+ const out = [];
82
+ const re = /<mark>([^<]*)<\/mark>/gi;
83
+ let m;
84
+ while ((m = re.exec(html)) !== null) out.push(m[1].toLowerCase());
85
+ return out;
86
+ }
87
+ function hasNonStopWordMatch(excerpts) {
88
+ for (const html of excerpts) {
89
+ for (const term of extractMarked(html)) {
90
+ const words = term.split(/\s+/);
91
+ if (words.some((w) => w && !STOP_WORDS.has(w))) return true;
92
+ }
93
+ }
94
+ return false;
95
+ }
96
+ function countOccurrences(text, phrase) {
97
+ let count = 0;
98
+ let from = 0;
99
+ while (true) {
100
+ const idx = text.indexOf(phrase, from);
101
+ if (idx < 0) break;
102
+ count++;
103
+ from = idx + phrase.length;
104
+ }
105
+ return count;
106
+ }
107
+ function normalizeUrl(url) {
108
+ return url.replace(/\/index\.html$/, "").replace(/\.html$/, "") || "/";
109
+ }
110
+ function defaultResults() {
111
+ return searchIndex.slice(0, 12).map((entry) => ({
112
+ path: entry.path,
113
+ title: entry.title,
114
+ section: entry.section,
115
+ description: entry.description ?? "",
116
+ snippet: entry.description ?? "",
117
+ excerptHtml: "",
118
+ subResults: [],
119
+ score: 0
120
+ }));
121
+ }
122
+ function getHiddenPaths() {
123
+ return new Set(pages.filter((p) => p.hidden).map((p) => p.path));
124
+ }
125
+ async function searchPages(query, limit = 30, showHidden = false) {
126
+ const q = query.trim();
127
+ if (!q) return defaultResults();
128
+ const hiddenPaths = getHiddenPaths();
129
+ const pf = await getPagefind();
130
+ if (!pf) {
131
+ return search(q, limit).filter((h) => showHidden || !hiddenPaths.has(h.entry.path)).map((h) => ({
132
+ path: h.entry.path,
133
+ title: h.entry.title,
134
+ section: h.entry.section,
135
+ description: h.entry.description ?? "",
136
+ snippet: h.snippet,
137
+ excerptHtml: "",
138
+ subResults: [],
139
+ score: h.score
140
+ }));
141
+ }
142
+ const results = await pf.search(q);
143
+ const loaded = await Promise.all(
144
+ results.results.slice(0, limit).map((r) => r.data())
145
+ );
146
+ const ql = q.toLowerCase();
147
+ const queryWords = ql.split(/\s+/).filter(Boolean);
148
+ const hasMultipleWords = queryWords.length > 1;
149
+ const hasContentWords = queryWords.some((w) => !STOP_WORDS.has(w));
150
+ const mapped = loaded.map((r, i) => {
151
+ const path = normalizeUrl(r.url);
152
+ const entry = searchIndex.find((e) => e.path === path);
153
+ const subResults = (r.sub_results ?? []).map((sr) => ({
154
+ title: sr.title ?? "",
155
+ url: normalizeUrl(sr.url),
156
+ excerptHtml: sr.excerpt ?? ""
157
+ }));
158
+ let score = results.results.length - i;
159
+ if (hasContentWords) {
160
+ const allExcerpts = [
161
+ r.excerpt ?? "",
162
+ ...(r.sub_results ?? []).map((sr) => sr.excerpt ?? "")
163
+ ];
164
+ if (!hasNonStopWordMatch(allExcerpts)) {
165
+ score = -1;
166
+ }
167
+ }
168
+ const titleLc = (r.meta?.title ?? entry?.title ?? "").toLowerCase();
169
+ if (titleLc === ql) score += 500;
170
+ else if (titleLc.startsWith(ql)) score += 300;
171
+ else if (titleLc.includes(ql)) score += 200;
172
+ const sectionLc = (entry?.section ?? "").toLowerCase();
173
+ if (sectionLc.includes(ql)) score += 50;
174
+ if (hasMultipleWords) {
175
+ const allText = [
176
+ r.excerpt ?? "",
177
+ ...(r.sub_results ?? []).map((sr) => sr.excerpt ?? "")
178
+ ].join(" ").replace(/<[^>]+>/g, "").toLowerCase();
179
+ const phraseCount = countOccurrences(allText, ql);
180
+ if (phraseCount > 0) {
181
+ score += 50 + Math.min(phraseCount, 5) * 10;
182
+ }
183
+ }
184
+ return {
185
+ path,
186
+ title: r.meta?.title ?? entry?.title ?? "",
187
+ section: entry?.section ?? "",
188
+ description: entry?.description ?? "",
189
+ snippet: r.excerpt?.replace(/<[^>]+>/g, "").trim() ?? "",
190
+ excerptHtml: r.excerpt ?? "",
191
+ subResults,
192
+ score
193
+ };
194
+ });
195
+ mapped.sort((a, b) => b.score - a.score);
196
+ return mapped.filter((r) => r.score >= 0 && (showHidden || !hiddenPaths.has(r.path)));
197
+ }
198
+ export {
199
+ defaultResults,
200
+ searchPages
201
+ };
@@ -0,0 +1,43 @@
1
+ import { PageMeta } from './navigation';
2
+ export interface SearchEntry extends PageMeta {
3
+ /** Heading text (h2/h3) for higher-weight ranking. */
4
+ headings: string[];
5
+ /** Plain-text body, lowercased, with markdown / JSX / code stripped. */
6
+ body: string;
7
+ }
8
+ /**
9
+ * Module-level singletons, populated by `initSearchIndex()` from the
10
+ * consuming site. Mutated in place — never reassigned — so importing
11
+ * components always see current data.
12
+ */
13
+ /** Raw (frontmatter- and import-stripped) markdown per page path — feeds
14
+ * the search palette's preview pane. */
15
+ export declare const rawMarkdown: Map<string, string>;
16
+ /** The dev-time fallback search index (pagefind takes over on built sites). */
17
+ export declare const searchIndex: SearchEntry[];
18
+ /**
19
+ * Populate the search singletons from the site's raw page glob:
20
+ *
21
+ * initSearchIndex(
22
+ * import.meta.glob('./pages/** /+Page.mdx', { eager: true, query: '?raw', import: 'default' }),
23
+ * )
24
+ *
25
+ * Call AFTER `initNavigation()` — entries join against `pages`.
26
+ */
27
+ export declare function initSearchIndex(rawSources: Record<string, string>): void;
28
+ export interface SearchHit {
29
+ entry: SearchEntry;
30
+ score: number;
31
+ /** Up to ~140 chars of context around the first body match, for the preview pane. */
32
+ snippet: string;
33
+ }
34
+ /**
35
+ * Rank weights — highest signal first:
36
+ * title exact / startsWith / contains
37
+ * heading contains
38
+ * description contains
39
+ * section contains
40
+ * body contains (count matters)
41
+ * subsequence fuzzy (last resort)
42
+ */
43
+ export declare function search(query: string, limit?: number): SearchHit[];
@@ -0,0 +1,112 @@
1
+ import { pages } from "./navigation.js";
2
+ function pathFromFile(filePath) {
3
+ const dir = filePath.replace(/^.*?\bpages\//, "").replace(/\/\+Page\.mdx$/, "");
4
+ return dir === "index" ? "/" : `/${dir}`;
5
+ }
6
+ function stripToText(raw) {
7
+ let s = raw;
8
+ s = s.replace(/^---[\s\S]*?\n---\s*\n?/, "");
9
+ s = s.replace(/^[ \t]*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?\s*$/gm, "");
10
+ s = s.replace(/```[\s\S]*?```/g, " ");
11
+ s = s.replace(/`([^`]*)`/g, "$1");
12
+ const headings = [];
13
+ s = s.replace(/^[ \t]*#{1,6}[ \t]+(.+?)[ \t]*#*[ \t]*$/gm, (_m, text) => {
14
+ headings.push(text.trim());
15
+ return " ";
16
+ });
17
+ s = s.replace(/<[^>]+>/g, " ");
18
+ s = s.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
19
+ s = s.replace(/\[([^\]]+)\]\[[^\]]*\]/g, "$1");
20
+ s = s.replace(/[*_~]+/g, " ");
21
+ s = s.replace(/\|/g, " ");
22
+ s = s.replace(/\s+/g, " ").trim().toLowerCase();
23
+ return { headings, body: s };
24
+ }
25
+ function stripPreamble(raw) {
26
+ let s = raw;
27
+ s = s.replace(/^---[\s\S]*?\n---\s*\n?/, "");
28
+ s = s.replace(/^[ \t]*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?\s*$/gm, "");
29
+ return s.trim();
30
+ }
31
+ const rawMarkdown = /* @__PURE__ */ new Map();
32
+ const searchIndex = [];
33
+ function initSearchIndex(rawSources) {
34
+ const bodyMap = /* @__PURE__ */ new Map();
35
+ rawMarkdown.clear();
36
+ for (const [filePath, raw] of Object.entries(rawSources)) {
37
+ const path = pathFromFile(filePath);
38
+ bodyMap.set(path, stripToText(raw));
39
+ rawMarkdown.set(path, stripPreamble(raw));
40
+ }
41
+ searchIndex.length = 0;
42
+ for (const p of pages) {
43
+ const extracted = bodyMap.get(p.path) ?? { headings: [], body: "" };
44
+ searchIndex.push({ ...p, headings: extracted.headings, body: extracted.body });
45
+ }
46
+ }
47
+ function search(query, limit = 30) {
48
+ const q = query.trim().toLowerCase();
49
+ if (!q) return [];
50
+ const hits = [];
51
+ for (const entry of searchIndex) {
52
+ const title = entry.title.toLowerCase();
53
+ const section = entry.section.toLowerCase();
54
+ const description = (entry.description ?? "").toLowerCase();
55
+ let score = 0;
56
+ let snippet = "";
57
+ if (title === q) score += 30;
58
+ else if (title.startsWith(q)) score += 18;
59
+ else if (title.includes(q)) score += 10;
60
+ for (const h of entry.headings) {
61
+ if (h.toLowerCase().includes(q)) {
62
+ score += 6;
63
+ if (!snippet) snippet = h;
64
+ }
65
+ }
66
+ if (description.includes(q)) {
67
+ score += 5;
68
+ if (!snippet) snippet = entry.description ?? "";
69
+ }
70
+ if (section.includes(q)) score += 3;
71
+ if (entry.body) {
72
+ const idx = entry.body.indexOf(q);
73
+ if (idx >= 0) {
74
+ let count = 0;
75
+ let from = 0;
76
+ while (true) {
77
+ const k = entry.body.indexOf(q, from);
78
+ if (k < 0) break;
79
+ count++;
80
+ from = k + q.length;
81
+ }
82
+ score += 2 + Math.min(count, 5);
83
+ if (!snippet) {
84
+ const start = Math.max(0, idx - 40);
85
+ const end = Math.min(entry.body.length, idx + q.length + 80);
86
+ snippet = (start > 0 ? "…" : "") + entry.body.slice(start, end) + (end < entry.body.length ? "…" : "");
87
+ }
88
+ }
89
+ }
90
+ if (score === 0) {
91
+ let i = 0;
92
+ for (const ch of title) {
93
+ if (ch === q[i]) i++;
94
+ if (i === q.length) {
95
+ score = 1;
96
+ break;
97
+ }
98
+ }
99
+ }
100
+ if (score > 0) {
101
+ hits.push({ entry, score, snippet: snippet || entry.description || "" });
102
+ }
103
+ }
104
+ hits.sort((a, b) => b.score - a.score);
105
+ return hits.slice(0, limit);
106
+ }
107
+ export {
108
+ initSearchIndex,
109
+ rawMarkdown,
110
+ search,
111
+ searchIndex
112
+ };
@@ -0,0 +1,23 @@
1
+ export interface TabDef {
2
+ id: string;
3
+ label: string;
4
+ /** Roman numeral prefix — small, muted, for the editorial chapter feel. */
5
+ numeral: string;
6
+ /** Default landing path when the user clicks this tab from elsewhere. */
7
+ landing: string;
8
+ /** URL prefix that scopes pages to this tab. A page belongs to a tab
9
+ * iff its path starts with this prefix (or equals `/` for the first tab). */
10
+ urlPrefix: string;
11
+ }
12
+ /** Module-level singleton, populated by `initTabs()`. Mutated in place —
13
+ * never reassigned — so importing components keep a live reference. */
14
+ export declare const TABS: TabDef[];
15
+ /** Populate the tab singleton from the consuming site. */
16
+ export declare function initTabs(tabs: TabDef[]): void;
17
+ /** Derive active tab from a page URL. The site root `/` resolves to the
18
+ * first tab. Unknown prefixes also fall back to the first tab so orphan
19
+ * pages never leave the sidebar empty. With no tabs registered, always
20
+ * returns '' (every page then belongs to the same, tabless view). */
21
+ export declare function tabForUrl(url: string | undefined): string;
22
+ /** True if a page URL belongs to the given tab. */
23
+ export declare function urlInTab(url: string, tabId: string): boolean;
@@ -0,0 +1,30 @@
1
+ const TABS = [];
2
+ function initTabs(tabs) {
3
+ TABS.length = 0;
4
+ TABS.push(...tabs);
5
+ }
6
+ function firstSegment(url) {
7
+ const path = url.split(/[?#]/)[0];
8
+ const trimmed = path.replace(/^\/+/, "").replace(/\/+$/, "");
9
+ if (!trimmed) return "";
10
+ return trimmed.split("/")[0];
11
+ }
12
+ function tabForUrl(url) {
13
+ if (TABS.length === 0) return "";
14
+ if (!url) return TABS[0].id;
15
+ const seg = firstSegment(url);
16
+ if (!seg) return TABS[0].id;
17
+ for (const tab of TABS) {
18
+ if (tab.urlPrefix === `/${seg}`) return tab.id;
19
+ }
20
+ return TABS[0].id;
21
+ }
22
+ function urlInTab(url, tabId) {
23
+ return tabForUrl(url) === tabId;
24
+ }
25
+ export {
26
+ TABS,
27
+ initTabs,
28
+ tabForUrl,
29
+ urlInTab
30
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Global "show hidden pages" toggle.
3
+ *
4
+ * Pages with `hidden: true` in frontmatter are filtered out of the
5
+ * sidebar by default. Press the chord below to reveal them; press
6
+ * again to hide. State is persisted to localStorage as `doc:show-hidden`
7
+ * so it survives reloads.
8
+ *
9
+ * Keybinding: **Cmd+Shift+D** (macOS) / **Ctrl+Shift+D** (Linux/Win).
10
+ * `Cmd+D` alone is browser bookmark — adding Shift dodges that.
11
+ *
12
+ * The hook also exposes the setter so a UI control (e.g. a topbar
13
+ * indicator) can toggle the state from a click as well.
14
+ */
15
+ export declare function useHiddenToggle(): [boolean, (next: boolean) => void];
@@ -0,0 +1,23 @@
1
+ import { useEffect } from "react";
2
+ import { useLocalStorage } from "./use-local-storage.js";
3
+ function useHiddenToggle() {
4
+ const [show, setShow] = useLocalStorage("show-hidden", false);
5
+ useEffect(() => {
6
+ const onKey = (e) => {
7
+ const t = e.target;
8
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) {
9
+ return;
10
+ }
11
+ const isToggle = (e.metaKey || e.ctrlKey) && e.shiftKey && (e.key === "D" || e.key === "d");
12
+ if (!isToggle) return;
13
+ e.preventDefault();
14
+ setShow(!show);
15
+ };
16
+ window.addEventListener("keydown", onKey);
17
+ return () => window.removeEventListener("keydown", onKey);
18
+ }, [show, setShow]);
19
+ return [show, setShow];
20
+ }
21
+ export {
22
+ useHiddenToggle
23
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Site-wide "show line numbers" toggle. Every CodeBlock / Preview
3
+ * reads and writes through this hook, so flipping the switch on one
4
+ * block flips them all — both within a single tab (via the
5
+ * `useLocalStorage` custom-event broadcast) and across tabs (via the
6
+ * native `storage` event). Persistence + cross-tab sync are provided
7
+ * by `useLocalStorage`; this file just pins the key + default.
8
+ */
9
+ export declare function useLineNumbers(): [boolean, (v: boolean) => void];
@@ -0,0 +1,7 @@
1
+ import { useLocalStorage } from "./use-local-storage.js";
2
+ function useLineNumbers() {
3
+ return useLocalStorage("line-numbers", false);
4
+ }
5
+ export {
6
+ useLineNumbers
7
+ };
@@ -0,0 +1,15 @@
1
+ export declare const DOC_PREFIX = "doc:";
2
+ export declare const docKey: (k: string) => string;
3
+ /**
4
+ * SSR-safe localStorage-backed state. Returns the in-memory React
5
+ * value and a setter that mirrors the value into localStorage and
6
+ * broadcasts to other consumers in the same tab.
7
+ *
8
+ * - On server render, the initial value is `defaultValue` (no
9
+ * localStorage access). The hook reconciles on mount.
10
+ * - Cross-tab sync works via the native `storage` event.
11
+ * - Same-tab sync (e.g., toggling a setting on one CodeBlock updates
12
+ * every other CodeBlock on the page) works via a custom event the
13
+ * setter dispatches on every write.
14
+ */
15
+ export declare function useLocalStorage<T>(key: string, defaultValue: T): [T, (next: T) => void];
@@ -0,0 +1,58 @@
1
+ import { useState, useRef, useEffect, useCallback } from "react";
2
+ const DOC_PREFIX = "doc:";
3
+ const docKey = (k) => k.startsWith(DOC_PREFIX) ? k : DOC_PREFIX + k;
4
+ const SAME_TAB_EVENT = "doc:storage";
5
+ function useLocalStorage(key, defaultValue) {
6
+ const fullKey = docKey(key);
7
+ const [value, setValue] = useState(defaultValue);
8
+ const defaultRef = useRef(defaultValue);
9
+ defaultRef.current = defaultValue;
10
+ useEffect(() => {
11
+ if (typeof window === "undefined") return;
12
+ const read = () => {
13
+ try {
14
+ const raw = localStorage.getItem(fullKey);
15
+ if (raw === null) {
16
+ setValue(defaultRef.current);
17
+ } else {
18
+ setValue(JSON.parse(raw));
19
+ }
20
+ } catch (err) {
21
+ }
22
+ };
23
+ read();
24
+ const onStorage = (e) => {
25
+ if (e.key === fullKey) read();
26
+ };
27
+ const onSameTab = (e) => {
28
+ const detail = e.detail;
29
+ if (detail?.key === fullKey) read();
30
+ };
31
+ window.addEventListener("storage", onStorage);
32
+ window.addEventListener(SAME_TAB_EVENT, onSameTab);
33
+ return () => {
34
+ window.removeEventListener("storage", onStorage);
35
+ window.removeEventListener(SAME_TAB_EVENT, onSameTab);
36
+ };
37
+ }, [fullKey]);
38
+ const set = useCallback(
39
+ (next) => {
40
+ setValue(next);
41
+ if (typeof window === "undefined") return;
42
+ try {
43
+ localStorage.setItem(fullKey, JSON.stringify(next));
44
+ } catch {
45
+ }
46
+ window.dispatchEvent(
47
+ new CustomEvent(SAME_TAB_EVENT, { detail: { key: fullKey } })
48
+ );
49
+ },
50
+ [fullKey]
51
+ );
52
+ return [value, set];
53
+ }
54
+ export {
55
+ DOC_PREFIX,
56
+ docKey,
57
+ useLocalStorage
58
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * SSR-safe matchMedia hook. Starts `false` on the server so the initial
3
+ * HTML matches the mobile-first CSS layer, then re-evaluates on mount.
4
+ */
5
+ export declare function useMediaQuery(query: string): boolean;
@@ -0,0 +1,16 @@
1
+ import { useState, useEffect } from "react";
2
+ function useMediaQuery(query) {
3
+ const [matches, setMatches] = useState(false);
4
+ useEffect(() => {
5
+ if (typeof window === "undefined") return;
6
+ const mql = window.matchMedia(query);
7
+ setMatches(mql.matches);
8
+ const onChange = (e) => setMatches(e.matches);
9
+ mql.addEventListener("change", onChange);
10
+ return () => mql.removeEventListener("change", onChange);
11
+ }, [query]);
12
+ return matches;
13
+ }
14
+ export {
15
+ useMediaQuery
16
+ };
@@ -0,0 +1,5 @@
1
+ import { ReactNode } from 'react';
2
+ export declare const useMerge: () => boolean;
3
+ export declare function Layout({ children }: {
4
+ children: ReactNode;
5
+ }): import("react").JSX.Element;