@dreamlake/dockit 0.2.29 → 0.2.31
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/dist/components/CodeBlock.js +10 -11
- package/dist/components/CodeTabs.d.ts +8 -1
- package/dist/components/CodeTabs.js +21 -9
- package/dist/components/Preview.js +8 -10
- package/dist/components/TOC.js +6 -3
- package/dist/components/Topbar.js +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/lib/code-tabs-sync.d.ts +35 -0
- package/dist/lib/code-tabs-sync.js +63 -0
- package/dist/lib/dev-metadata-plugin.d.ts +3 -0
- package/dist/lib/dev-metadata-plugin.js +81 -0
- package/dist/lib/glyph-burst.d.ts +28 -0
- package/dist/lib/glyph-burst.js +111 -0
- package/dist/lib/use-local-storage.d.ts +14 -0
- package/dist/lib/use-local-storage.js +47 -32
- package/dist/vite.js +2 -0
- package/package.json +1 -1
|
@@ -2,6 +2,8 @@ import { jsxs, Fragment, jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useContext, useRef, useState } from "react";
|
|
3
3
|
import { CodeTabsContext } from "./code-tabs-context.js";
|
|
4
4
|
import { useLineNumbers } from "../lib/use-line-numbers.js";
|
|
5
|
+
import { copyWithGlyphBurst } from "../lib/glyph-burst.js";
|
|
6
|
+
const BURST = { color: "var(--color-doc-template-ink)" };
|
|
5
7
|
const headerBtnStyle = (active) => ({
|
|
6
8
|
display: "inline-flex",
|
|
7
9
|
alignItems: "center",
|
|
@@ -25,17 +27,14 @@ function CodeBlock({ children, filename, lang }) {
|
|
|
25
27
|
const [copied, setCopied] = useState(false);
|
|
26
28
|
const [wrap, setWrap] = useState(false);
|
|
27
29
|
const [showLines, setShowLines] = useLineNumbers();
|
|
28
|
-
function copy() {
|
|
30
|
+
function copy(button) {
|
|
29
31
|
const el = wrapRef.current?.querySelector("pre code");
|
|
30
32
|
if (!el) return;
|
|
31
|
-
|
|
32
|
-
()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
() => {
|
|
37
|
-
}
|
|
38
|
-
);
|
|
33
|
+
void copyWithGlyphBurst(button, el.innerText, BURST).then((ok) => {
|
|
34
|
+
if (!ok) return;
|
|
35
|
+
setCopied(true);
|
|
36
|
+
setTimeout(() => setCopied(false), 1400);
|
|
37
|
+
});
|
|
39
38
|
}
|
|
40
39
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
41
40
|
embedded && (filename || lang) && /* @__PURE__ */ jsx("span", { className: "dockit-code-tabs-label", title: filename || lang, children: filename || lang }),
|
|
@@ -115,7 +114,7 @@ function CodeBlock({ children, filename, lang }) {
|
|
|
115
114
|
children: "Wrap"
|
|
116
115
|
}
|
|
117
116
|
),
|
|
118
|
-
/* @__PURE__ */ jsx("button", { type: "button", onClick: copy, style: headerBtnStyle(copied), children: copied ? "Copied" : "Copy" })
|
|
117
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: (e) => copy(e.currentTarget), style: headerBtnStyle(copied), children: copied ? "Copied" : "Copy" })
|
|
119
118
|
]
|
|
120
119
|
}
|
|
121
120
|
)
|
|
@@ -181,7 +180,7 @@ function CodeBlock({ children, filename, lang }) {
|
|
|
181
180
|
children: "Wrap"
|
|
182
181
|
}
|
|
183
182
|
),
|
|
184
|
-
/* @__PURE__ */ jsx("button", { type: "button", onClick: copy, style: headerBtnStyle(copied), children: copied ? "Copied" : "Copy" })
|
|
183
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: (e) => copy(e.currentTarget), style: headerBtnStyle(copied), children: copied ? "Copied" : "Copy" })
|
|
185
184
|
]
|
|
186
185
|
}
|
|
187
186
|
),
|
|
@@ -8,6 +8,13 @@ export declare function CodeExample({ children }: CodeExampleProps): import("rea
|
|
|
8
8
|
export interface CodeTabsProps {
|
|
9
9
|
children: ReactNode;
|
|
10
10
|
'aria-label'?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Opt-in sync group. Every CodeTabs with the same `syncKey` shows the same
|
|
13
|
+
* tab label, on this page and others; the choice persists in localStorage
|
|
14
|
+
* (`doc:code-tabs:<syncKey>`) and follows other browser tabs. A group that
|
|
15
|
+
* lacks the chosen label keeps its own selection.
|
|
16
|
+
*/
|
|
17
|
+
syncKey?: string;
|
|
11
18
|
}
|
|
12
19
|
/** UIKit owns the frame and keyboard navigation; Dockit owns fenced examples. */
|
|
13
|
-
export declare function CodeTabs({ children, 'aria-label': label }: CodeTabsProps): import("react").JSX.Element | null;
|
|
20
|
+
export declare function CodeTabs({ children, syncKey, 'aria-label': label }: CodeTabsProps): import("react").JSX.Element | null;
|
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
import { jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Children, isValidElement } from "react";
|
|
2
|
+
import { Children, isValidElement, useState } from "react";
|
|
3
3
|
import { TabbedContainer } from "../vendor/@dreamlake/uikit/src/components/TabbedContainer/TabbedContainer.js";
|
|
4
4
|
import { CodeTabsContext } from "./code-tabs-context.js";
|
|
5
|
+
import { useCodeTabsChoice, resolveCodeTab } from "../lib/code-tabs-sync.js";
|
|
5
6
|
function CodeExample({ children }) {
|
|
6
7
|
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
7
8
|
}
|
|
8
|
-
function CodeTabs({ children, "aria-label": label = "Code examples" }) {
|
|
9
|
+
function CodeTabs({ children, syncKey, "aria-label": label = "Code examples" }) {
|
|
9
10
|
const examples = Children.toArray(children).filter(isValidElement);
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
const items = examples.map((example, index) => ({
|
|
12
|
+
value: String(example.key ?? index),
|
|
13
|
+
label: example.props.label,
|
|
14
|
+
children: example.props.children
|
|
15
|
+
}));
|
|
16
|
+
const [choice, choose] = useCodeTabsChoice(syncKey);
|
|
17
|
+
const [own, setOwn] = useState(void 0);
|
|
18
|
+
const value = resolveCodeTab(items, choice, own);
|
|
19
|
+
if (value !== own && choice !== null && value !== void 0) setOwn(value);
|
|
20
|
+
if (!items.length) return null;
|
|
21
|
+
return /* @__PURE__ */ jsx("div", { "data-code-tabs": true, "data-code-tabs-sync": syncKey, children: /* @__PURE__ */ jsx(CodeTabsContext.Provider, { value: true, children: /* @__PURE__ */ jsx(
|
|
12
22
|
TabbedContainer,
|
|
13
23
|
{
|
|
14
24
|
"aria-label": label,
|
|
15
25
|
className: "dockit-code-tabs",
|
|
16
|
-
items
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
items,
|
|
27
|
+
value,
|
|
28
|
+
onValueChange: (next) => {
|
|
29
|
+
setOwn(next);
|
|
30
|
+
const picked = items.find((item) => item.value === next);
|
|
31
|
+
if (picked) choose(picked.label);
|
|
32
|
+
}
|
|
21
33
|
}
|
|
22
34
|
) }) });
|
|
23
35
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from "react";
|
|
3
3
|
import { useLineNumbers } from "../lib/use-line-numbers.js";
|
|
4
|
+
import { copyWithGlyphBurst } from "../lib/glyph-burst.js";
|
|
4
5
|
function langFromFilename(name) {
|
|
5
6
|
if (!name) return "tsx";
|
|
6
7
|
const ext = name.split(".").pop()?.toLowerCase();
|
|
@@ -175,15 +176,12 @@ function Preview({
|
|
|
175
176
|
window.addEventListener("keydown", onKey);
|
|
176
177
|
return () => window.removeEventListener("keydown", onKey);
|
|
177
178
|
}, [fullscreen]);
|
|
178
|
-
function copy() {
|
|
179
|
-
|
|
180
|
-
()
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
() => {
|
|
185
|
-
}
|
|
186
|
-
);
|
|
179
|
+
function copy(button) {
|
|
180
|
+
void copyWithGlyphBurst(button, visibleSource, { color: "var(--color-doc-template-ink)" }).then((ok) => {
|
|
181
|
+
if (!ok) return;
|
|
182
|
+
setCopied(true);
|
|
183
|
+
setTimeout(() => setCopied(false), 1400);
|
|
184
|
+
});
|
|
187
185
|
}
|
|
188
186
|
return /* @__PURE__ */ jsxs(
|
|
189
187
|
"div",
|
|
@@ -264,7 +262,7 @@ function Preview({
|
|
|
264
262
|
"button",
|
|
265
263
|
{
|
|
266
264
|
type: "button",
|
|
267
|
-
onClick: copy,
|
|
265
|
+
onClick: (e) => copy(e.currentTarget),
|
|
268
266
|
"aria-label": "Copy source",
|
|
269
267
|
style: headerBtnStyle(copied),
|
|
270
268
|
children: copied ? "Copied" : "Copy"
|
package/dist/components/TOC.js
CHANGED
|
@@ -3,6 +3,7 @@ import { useState, useRef, useEffect, useMemo } from "react";
|
|
|
3
3
|
import { usePageContext } from "vike-react/usePageContext";
|
|
4
4
|
import { normalizePath, findPage } from "../lib/navigation.js";
|
|
5
5
|
import { siteConfig } from "../site-config.js";
|
|
6
|
+
import { useHiddenToggle } from "../lib/use-hidden-toggle.js";
|
|
6
7
|
const TOC_KEY_PREFIX = "toc-collapsed:";
|
|
7
8
|
const X0 = 4;
|
|
8
9
|
const X_INDENT_H3 = 12;
|
|
@@ -52,19 +53,21 @@ function scrollToHeading(e, id) {
|
|
|
52
53
|
function TOC() {
|
|
53
54
|
const { urlPathname } = usePageContext();
|
|
54
55
|
const currentPath = normalizePath(urlPathname);
|
|
56
|
+
const [showDeveloperLinks] = useHiddenToggle();
|
|
55
57
|
const mdHref = (() => {
|
|
56
58
|
const meta = findPage(urlPathname);
|
|
57
59
|
if (!meta || meta.noindex) return null;
|
|
58
60
|
return currentPath === "/" ? "/index.md" : `${currentPath}.md`;
|
|
59
61
|
})();
|
|
60
62
|
const editHref = (() => {
|
|
63
|
+
if (!showDeveloperLinks || !siteConfig.docsRepoUrl) return null;
|
|
61
64
|
const meta = findPage(urlPathname);
|
|
62
65
|
if (!meta) return null;
|
|
63
66
|
const slug = currentPath === "/" ? "index" : currentPath.replace(/^\//, "");
|
|
64
67
|
const pagesPath = siteConfig.docsPagesPath ?? "docs/pages";
|
|
65
|
-
return `${siteConfig.docsRepoUrl}/edit/${siteConfig.docsBranch}/${pagesPath}/${slug}/+Page.mdx`;
|
|
68
|
+
return `${siteConfig.docsRepoUrl}/edit/${encodeURIComponent(siteConfig.docsBranch)}/${pagesPath}/${slug}/+Page.mdx`;
|
|
66
69
|
})();
|
|
67
|
-
const issueHref = `${siteConfig.docsRepoUrl}/issues/new
|
|
70
|
+
const issueHref = showDeveloperLinks && siteConfig.docsRepoUrl ? `${siteConfig.docsRepoUrl}/issues/new` : null;
|
|
68
71
|
const [headings, setHeadings] = useState([]);
|
|
69
72
|
const [endPos, setEndPos] = useState(null);
|
|
70
73
|
const [pathD, setPathD] = useState("");
|
|
@@ -643,7 +646,7 @@ function TOC() {
|
|
|
643
646
|
children: "Edit this page"
|
|
644
647
|
}
|
|
645
648
|
),
|
|
646
|
-
/* @__PURE__ */ jsx(
|
|
649
|
+
issueHref && /* @__PURE__ */ jsx(
|
|
647
650
|
"a",
|
|
648
651
|
{
|
|
649
652
|
href: issueHref,
|
|
@@ -38,6 +38,7 @@ const SEARCH_INSET = `max(${SEARCH_GUTTER}px, calc((100vw - ${SEARCH_MAX_W}px) /
|
|
|
38
38
|
function Topbar({ searchOpen, onOpenSearch, onCloseSearch, query, setQuery }) {
|
|
39
39
|
const { urlPathname } = usePageContext();
|
|
40
40
|
const current = findPage(urlPathname);
|
|
41
|
+
const [showDeveloperLinks] = useHiddenToggle();
|
|
41
42
|
const hasTabs = TABS.length > 0;
|
|
42
43
|
const activeTab = internalTabs().find((t) => t.id === tabForUrl(urlPathname));
|
|
43
44
|
const versionChips = activeTab?.versionChips ?? siteConfig.versionChips ?? [];
|
|
@@ -353,7 +354,7 @@ function Topbar({ searchOpen, onOpenSearch, onCloseSearch, query, setQuery }) {
|
|
|
353
354
|
},
|
|
354
355
|
chip.label
|
|
355
356
|
)),
|
|
356
|
-
siteConfig.gitHash && /* @__PURE__ */ jsxs(
|
|
357
|
+
showDeveloperLinks && siteConfig.gitHash && /* @__PURE__ */ jsxs(
|
|
357
358
|
"span",
|
|
358
359
|
{
|
|
359
360
|
className: "hidden md:inline-flex items-center text-doc-template-muted border border-doc-template-faint",
|
|
@@ -553,7 +554,7 @@ function Topbar({ searchOpen, onOpenSearch, onCloseSearch, query, setQuery }) {
|
|
|
553
554
|
className: "flex items-stretch gap-2.5 md:gap-3.5 pr-[14px] md:pr-[22px]",
|
|
554
555
|
style: { gridColumn: 3, gridRow: 1, justifySelf: "end", ...palFade(searchOpen) },
|
|
555
556
|
children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3.5", children: [
|
|
556
|
-
siteConfig.repoUrl && /* @__PURE__ */ jsx(
|
|
557
|
+
showDeveloperLinks && siteConfig.repoUrl && /* @__PURE__ */ jsx(
|
|
557
558
|
"a",
|
|
558
559
|
{
|
|
559
560
|
href: siteConfig.repoUrl,
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export { useHiddenToggle } from './lib/use-hidden-toggle';
|
|
|
26
26
|
export { useLineNumbers } from './lib/use-line-numbers';
|
|
27
27
|
export { useLocalStorage, DOC_PREFIX, docKey } from './lib/use-local-storage';
|
|
28
28
|
export { useMediaQuery } from './lib/use-media-query';
|
|
29
|
+
export { glyphBurst, copyWithGlyphBurst, burstGlyphs, type GlyphBurstOptions } from './lib/glyph-burst';
|
|
29
30
|
export { Callout } from './components/Callout';
|
|
30
31
|
export { Chip, type ChipProps, type ChipVariant } from './components/Chip';
|
|
31
32
|
export { ClientOnly } from './components/ClientOnly';
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { useHiddenToggle } from "./lib/use-hidden-toggle.js";
|
|
|
11
11
|
import { useLineNumbers } from "./lib/use-line-numbers.js";
|
|
12
12
|
import { DOC_PREFIX, docKey, useLocalStorage } from "./lib/use-local-storage.js";
|
|
13
13
|
import { useMediaQuery } from "./lib/use-media-query.js";
|
|
14
|
+
import { burstGlyphs, copyWithGlyphBurst, glyphBurst } from "./lib/glyph-burst.js";
|
|
14
15
|
import { Callout } from "./components/Callout.js";
|
|
15
16
|
import { Chip } from "./components/Chip.js";
|
|
16
17
|
import { ClientOnly } from "./components/ClientOnly.js";
|
|
@@ -66,11 +67,14 @@ export {
|
|
|
66
67
|
ThemeToggle,
|
|
67
68
|
Topbar,
|
|
68
69
|
VersionBadge,
|
|
70
|
+
burstGlyphs,
|
|
71
|
+
copyWithGlyphBurst,
|
|
69
72
|
defaultResults,
|
|
70
73
|
defineSiteConfig,
|
|
71
74
|
docKey,
|
|
72
75
|
findPage,
|
|
73
76
|
getAdjacentPages,
|
|
77
|
+
glyphBurst,
|
|
74
78
|
groupedPages,
|
|
75
79
|
groupedVisiblePages,
|
|
76
80
|
initDocs,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in sync groups for `<CodeTabs syncKey="…">` (Docusaurus `groupId`).
|
|
3
|
+
*
|
|
4
|
+
* Every CodeTabs sharing a `syncKey` shows the same tab *label*. The
|
|
5
|
+
* choice is persisted at `doc:code-tabs:<syncKey>` through the same
|
|
6
|
+
* `doc:` storage layer as `useLocalStorage`, so it survives navigation and
|
|
7
|
+
* reloads, follows other browser tabs via the native `storage` event, and
|
|
8
|
+
* follows other blocks on the page via the same-tab broadcast. An
|
|
9
|
+
* in-memory copy keeps in-page sync working when storage is blocked.
|
|
10
|
+
*/
|
|
11
|
+
export declare const codeTabsStorageKey: (syncKey: string) => string;
|
|
12
|
+
/** Current choice for a group (client only). */
|
|
13
|
+
export declare function readCodeTabsChoice(syncKey: string): string | null;
|
|
14
|
+
/** Choose a label for every block in the group, here and in other tabs. */
|
|
15
|
+
export declare function writeCodeTabsChoice(syncKey: string, label: string): void;
|
|
16
|
+
/** Notify when the group's choice changes from any source. */
|
|
17
|
+
export declare function subscribeCodeTabsChoice(syncKey: string, onChange: () => void): () => void;
|
|
18
|
+
export interface CodeTabItem {
|
|
19
|
+
value: string;
|
|
20
|
+
label: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Which tab a block shows: the group's choice when the block has that label;
|
|
24
|
+
* otherwise the block keeps its own last selection, then its first tab.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveCodeTab(items: readonly CodeTabItem[], choice: string | null, own: string | undefined): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* The group's chosen label, or `null` without a `syncKey`/choice.
|
|
29
|
+
*
|
|
30
|
+
* SSR and the hydration pass both see `null`, so markup always matches the
|
|
31
|
+
* server's (first-tab) HTML. A layout effect then re-renders before the
|
|
32
|
+
* browser paints the hydrated tree; client-side navigation reads the stored
|
|
33
|
+
* choice on first render, so no flash there.
|
|
34
|
+
*/
|
|
35
|
+
export declare function useCodeTabsChoice(syncKey: string | undefined): [string | null, (label: string) => void];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useCallback, useSyncExternalStore, useReducer, useLayoutEffect } from "react";
|
|
2
|
+
import { subscribeDocStorage, readDocStorage, writeDocStorage } from "./use-local-storage.js";
|
|
3
|
+
const codeTabsStorageKey = (syncKey) => `code-tabs:${syncKey}`;
|
|
4
|
+
const memory = /* @__PURE__ */ new Map();
|
|
5
|
+
const asLabel = (value) => typeof value === "string" ? value : null;
|
|
6
|
+
function readCodeTabsChoice(syncKey) {
|
|
7
|
+
let choice = memory.get(syncKey);
|
|
8
|
+
if (choice === void 0) {
|
|
9
|
+
choice = asLabel(readDocStorage(codeTabsStorageKey(syncKey)));
|
|
10
|
+
memory.set(syncKey, choice);
|
|
11
|
+
}
|
|
12
|
+
return choice;
|
|
13
|
+
}
|
|
14
|
+
function writeCodeTabsChoice(syncKey, label) {
|
|
15
|
+
memory.set(syncKey, label);
|
|
16
|
+
writeDocStorage(codeTabsStorageKey(syncKey), label);
|
|
17
|
+
}
|
|
18
|
+
function subscribeCodeTabsChoice(syncKey, onChange) {
|
|
19
|
+
return subscribeDocStorage(codeTabsStorageKey(syncKey), (change) => {
|
|
20
|
+
if (change) memory.set(syncKey, asLabel(change.value));
|
|
21
|
+
else memory.delete(syncKey);
|
|
22
|
+
onChange();
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function resolveCodeTab(items, choice, own) {
|
|
26
|
+
const chosen = choice === null ? void 0 : items.find((item) => item.label === choice);
|
|
27
|
+
if (chosen) return chosen.value;
|
|
28
|
+
return items.some((item) => item.value === own) ? own : items[0]?.value;
|
|
29
|
+
}
|
|
30
|
+
const noop = () => () => {
|
|
31
|
+
};
|
|
32
|
+
const serverSnapshot = () => null;
|
|
33
|
+
function useCodeTabsChoice(syncKey) {
|
|
34
|
+
const subscribe = useCallback(
|
|
35
|
+
(onChange) => syncKey ? subscribeCodeTabsChoice(syncKey, onChange) : () => {
|
|
36
|
+
},
|
|
37
|
+
[syncKey]
|
|
38
|
+
);
|
|
39
|
+
const choice = useSyncExternalStore(
|
|
40
|
+
syncKey ? subscribe : noop,
|
|
41
|
+
() => syncKey ? readCodeTabsChoice(syncKey) : null,
|
|
42
|
+
serverSnapshot
|
|
43
|
+
);
|
|
44
|
+
const [, rerender] = useReducer((n) => n + 1, 0);
|
|
45
|
+
useLayoutEffect(() => {
|
|
46
|
+
if (syncKey && readCodeTabsChoice(syncKey) !== choice) rerender();
|
|
47
|
+
}, [syncKey]);
|
|
48
|
+
const choose = useCallback(
|
|
49
|
+
(label) => {
|
|
50
|
+
if (syncKey) writeCodeTabsChoice(syncKey, label);
|
|
51
|
+
},
|
|
52
|
+
[syncKey]
|
|
53
|
+
);
|
|
54
|
+
return [choice, choose];
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
codeTabsStorageKey,
|
|
58
|
+
readCodeTabsChoice,
|
|
59
|
+
resolveCodeTab,
|
|
60
|
+
subscribeCodeTabsChoice,
|
|
61
|
+
useCodeTabsChoice,
|
|
62
|
+
writeCodeTabsChoice
|
|
63
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { transformWithEsbuild } from "vite";
|
|
2
|
+
const fields = /* @__PURE__ */ new Set(["repoUrl", "docsRepoUrl", "docsBranch", "docsPagesPath", "gitHash"]);
|
|
3
|
+
function staticString(node) {
|
|
4
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
5
|
+
if (node.type === "TemplateLiteral") {
|
|
6
|
+
const parts = node.quasis;
|
|
7
|
+
const expressions = node.expressions;
|
|
8
|
+
let value = parts[0].value.cooked;
|
|
9
|
+
for (let i = 0; i < expressions.length; i++) {
|
|
10
|
+
const part = staticString(expressions[i]);
|
|
11
|
+
if (part === null) return null;
|
|
12
|
+
value += part + parts[i + 1].value.cooked;
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
function devMetadataPlugin() {
|
|
19
|
+
let define = {};
|
|
20
|
+
return {
|
|
21
|
+
name: "dockit-dev-metadata",
|
|
22
|
+
enforce: "pre",
|
|
23
|
+
apply: "build",
|
|
24
|
+
configResolved(config) {
|
|
25
|
+
define = Object.fromEntries(Object.entries(config.define ?? {}).map(([key, value]) => [
|
|
26
|
+
key,
|
|
27
|
+
typeof value === "string" ? value : JSON.stringify(value)
|
|
28
|
+
]));
|
|
29
|
+
if (config.build.sourcemap) {
|
|
30
|
+
throw new Error("Dockit developer metadata requires build.sourcemap: false");
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
async transform(source, id) {
|
|
34
|
+
if (!/(?:^|\/)site\.config\.[cm]?[jt]s$/.test(id)) return null;
|
|
35
|
+
const compiled = await transformWithEsbuild(source, id, {
|
|
36
|
+
loader: "ts",
|
|
37
|
+
target: "esnext",
|
|
38
|
+
define,
|
|
39
|
+
sourcemap: false,
|
|
40
|
+
legalComments: "none"
|
|
41
|
+
});
|
|
42
|
+
const edits = [];
|
|
43
|
+
const visit = (node) => {
|
|
44
|
+
if (node.type === "Property") {
|
|
45
|
+
const key = node.key;
|
|
46
|
+
const name = key.type === "Identifier" && !node.computed ? key.name : key.value;
|
|
47
|
+
if (typeof name === "string" && fields.has(name)) {
|
|
48
|
+
const value = node.value;
|
|
49
|
+
const text = staticString(value);
|
|
50
|
+
if (text === null) {
|
|
51
|
+
this.error(`Dockit ${name} must be a string literal or a build-time define in ${id}`);
|
|
52
|
+
}
|
|
53
|
+
const bytes = Array.from({ length: text.length }, (_, i) => text.charCodeAt(i) ^ 90);
|
|
54
|
+
edits.push({
|
|
55
|
+
start: value.start,
|
|
56
|
+
end: value.end,
|
|
57
|
+
text: `[${bytes.join(",")}].map(n=>String.fromCharCode(n^90)).join("")`
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const value of Object.values(node)) {
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
for (const item of value) if (item?.type) visit(item);
|
|
65
|
+
} else if (value && typeof value === "object" && "type" in value) {
|
|
66
|
+
visit(value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
visit(this.parse(compiled.code));
|
|
71
|
+
let code = compiled.code;
|
|
72
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
73
|
+
code = code.slice(0, edit.start) + edit.text + code.slice(edit.end);
|
|
74
|
+
}
|
|
75
|
+
return { code, map: { mappings: "" } };
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
devMetadataPlugin
|
|
81
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glyph burst — the copy-button flourish. After a successful copy, a handful
|
|
3
|
+
* of the copied code's own punctuation (`{ } ( ) ; = …`) pops out of the
|
|
4
|
+
* button in its ink and mono face, arcs under gravity, and fades.
|
|
5
|
+
*
|
|
6
|
+
* Everything happens after the clipboard write resolves, inside one fixed,
|
|
7
|
+
* pointer-transparent overlay that removes itself; only transform and
|
|
8
|
+
* opacity animate, so nothing on the page reflows. Reduced motion, no
|
|
9
|
+
* matchMedia, or no Web Animations API → no burst, plain feedback only.
|
|
10
|
+
*
|
|
11
|
+
* Kept dependency-free on purpose; the DreamLake app ships a twin of this
|
|
12
|
+
* file (lib/notes/glyphBurst.ts) so both copy buttons feel the same.
|
|
13
|
+
*/
|
|
14
|
+
/** Up to `count` distinct symbols from `source`, padded with code punctuation. */
|
|
15
|
+
export declare function burstGlyphs(source: string, count?: number): string[];
|
|
16
|
+
export interface GlyphBurstOptions {
|
|
17
|
+
/** CSS font-family for the glyphs; defaults to the origin's computed font. */
|
|
18
|
+
font?: string;
|
|
19
|
+
/** CSS color for the glyphs; defaults to the origin's computed color. */
|
|
20
|
+
color?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Fire-and-forget. Never throws; does nothing under reduced motion. */
|
|
23
|
+
export declare function glyphBurst(origin: Element, source: string, options?: GlyphBurstOptions): void;
|
|
24
|
+
/**
|
|
25
|
+
* Write `text` to the clipboard, then (only on success) burst from `origin`.
|
|
26
|
+
* Resolves true when the copy happened; the burst never delays or fails it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function copyWithGlyphBurst(origin: Element, text: string, options?: GlyphBurstOptions): Promise<boolean>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const FALLBACK = "{}();=<>/*[]#:";
|
|
2
|
+
const SYMBOL = /[^\p{L}\p{N}\s_]/u;
|
|
3
|
+
const COUNT = 10;
|
|
4
|
+
function burstGlyphs(source, count = COUNT) {
|
|
5
|
+
const glyphs = [];
|
|
6
|
+
for (const ch of source) {
|
|
7
|
+
if (glyphs.length >= count) break;
|
|
8
|
+
if (SYMBOL.test(ch) && !glyphs.includes(ch)) glyphs.push(ch);
|
|
9
|
+
}
|
|
10
|
+
for (const ch of FALLBACK) {
|
|
11
|
+
if (glyphs.length >= count) break;
|
|
12
|
+
if (!glyphs.includes(ch)) glyphs.push(ch);
|
|
13
|
+
}
|
|
14
|
+
return glyphs;
|
|
15
|
+
}
|
|
16
|
+
function motionAllowed() {
|
|
17
|
+
if (typeof window === "undefined" || typeof document === "undefined")
|
|
18
|
+
return false;
|
|
19
|
+
if (typeof window.matchMedia !== "function") return false;
|
|
20
|
+
return !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
21
|
+
}
|
|
22
|
+
function glyphBurst(origin, source, options = {}) {
|
|
23
|
+
try {
|
|
24
|
+
if (!motionAllowed()) return;
|
|
25
|
+
const rect = origin.getBoundingClientRect();
|
|
26
|
+
if (!rect.width && !rect.height) return;
|
|
27
|
+
const computed = window.getComputedStyle(origin);
|
|
28
|
+
const font = options.font ?? computed.fontFamily;
|
|
29
|
+
const color = options.color ?? computed.color;
|
|
30
|
+
const x = rect.left + rect.width / 2;
|
|
31
|
+
const y = rect.top + rect.height / 2;
|
|
32
|
+
const layer = document.createElement("div");
|
|
33
|
+
layer.setAttribute("aria-hidden", "true");
|
|
34
|
+
layer.setAttribute("data-glyph-burst", "");
|
|
35
|
+
Object.assign(layer.style, {
|
|
36
|
+
position: "fixed",
|
|
37
|
+
inset: "0",
|
|
38
|
+
pointerEvents: "none",
|
|
39
|
+
overflow: "hidden",
|
|
40
|
+
contain: "strict",
|
|
41
|
+
zIndex: "2147483647"
|
|
42
|
+
});
|
|
43
|
+
const glyphs = burstGlyphs(source);
|
|
44
|
+
const animations = [];
|
|
45
|
+
glyphs.forEach((glyph, i) => {
|
|
46
|
+
const span = document.createElement("span");
|
|
47
|
+
span.textContent = glyph;
|
|
48
|
+
Object.assign(span.style, {
|
|
49
|
+
position: "absolute",
|
|
50
|
+
left: `${x}px`,
|
|
51
|
+
top: `${y}px`,
|
|
52
|
+
font: `500 12px/1 ${font}`,
|
|
53
|
+
color,
|
|
54
|
+
whiteSpace: "pre",
|
|
55
|
+
willChange: "transform, opacity"
|
|
56
|
+
});
|
|
57
|
+
layer.append(span);
|
|
58
|
+
if (typeof span.animate !== "function") return;
|
|
59
|
+
const angle = (-150 + 120 * (i + 0.2 + Math.random() * 0.6) / glyphs.length) * Math.PI / 180;
|
|
60
|
+
const speed = 170 + Math.random() * 60;
|
|
61
|
+
const vx = Math.cos(angle) * speed * 0.6;
|
|
62
|
+
const vy = Math.sin(angle) * speed;
|
|
63
|
+
const gravity = 440;
|
|
64
|
+
const spin = (Math.random() - 0.5) * 300;
|
|
65
|
+
const frames = [];
|
|
66
|
+
for (let step = 0; step <= 8; step++) {
|
|
67
|
+
const t = step / 8;
|
|
68
|
+
const dx = vx * t;
|
|
69
|
+
const dy = vy * t + gravity * t * t / 2;
|
|
70
|
+
frames.push({
|
|
71
|
+
offset: t,
|
|
72
|
+
opacity: t < 0.6 ? 0.9 : 0.9 * (1 - (t - 0.6) / 0.4),
|
|
73
|
+
transform: `translate(-50%, -50%) translate(${dx.toFixed(1)}px, ${dy.toFixed(1)}px) rotate(${(spin * t).toFixed(1)}deg) scale(${t === 0 ? 0.5 : 1})`
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
animations.push(
|
|
77
|
+
span.animate(frames, {
|
|
78
|
+
duration: 760 + Math.random() * 220,
|
|
79
|
+
delay: i * 12,
|
|
80
|
+
easing: "cubic-bezier(.3,.55,.55,1)",
|
|
81
|
+
fill: "both"
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
if (!animations.length) return;
|
|
86
|
+
document.body.append(layer);
|
|
87
|
+
let removed = false;
|
|
88
|
+
const remove = () => {
|
|
89
|
+
if (removed) return;
|
|
90
|
+
removed = true;
|
|
91
|
+
layer.remove();
|
|
92
|
+
};
|
|
93
|
+
Promise.all(animations.map((a) => a.finished)).then(remove, remove);
|
|
94
|
+
setTimeout(remove, 1600);
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function copyWithGlyphBurst(origin, text, options) {
|
|
99
|
+
try {
|
|
100
|
+
await navigator.clipboard.writeText(text);
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
glyphBurst(origin, text, options);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
burstGlyphs,
|
|
109
|
+
copyWithGlyphBurst,
|
|
110
|
+
glyphBurst
|
|
111
|
+
};
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
export declare const DOC_PREFIX = "doc:";
|
|
2
2
|
export declare const docKey: (k: string) => string;
|
|
3
|
+
/** Read and JSON-decode a `doc:` key. Missing, malformed, or blocked
|
|
4
|
+
* storage (privacy mode, sandboxed iframe, SSR) all return `undefined`. */
|
|
5
|
+
export declare function readDocStorage<T>(key: string): T | undefined;
|
|
6
|
+
/** JSON-encode a value into a `doc:` key and notify same-tab subscribers.
|
|
7
|
+
* The broadcast fires even when persistence fails, so live in-page sync
|
|
8
|
+
* survives disabled storage. */
|
|
9
|
+
export declare function writeDocStorage<T>(key: string, value: T): void;
|
|
10
|
+
/** Subscribe to changes of a `doc:` key from this tab (custom event) and
|
|
11
|
+
* other tabs (native `storage` event; `key === null` means storage was
|
|
12
|
+
* cleared). Same-tab writes pass `{ value }`, so subscribers stay in sync
|
|
13
|
+
* even when storage is blocked; cross-tab changes pass `null` (re-read). */
|
|
14
|
+
export declare function subscribeDocStorage(key: string, onChange: (sameTab: {
|
|
15
|
+
value: unknown;
|
|
16
|
+
} | null) => void): () => void;
|
|
3
17
|
/**
|
|
4
18
|
* SSR-safe localStorage-backed state. Returns the in-memory React
|
|
5
19
|
* value and a setter that mirrors the value into localStorage and
|
|
@@ -2,50 +2,62 @@ import { useState, useRef, useEffect, useCallback } from "react";
|
|
|
2
2
|
const DOC_PREFIX = "doc:";
|
|
3
3
|
const docKey = (k) => k.startsWith(DOC_PREFIX) ? k : DOC_PREFIX + k;
|
|
4
4
|
const SAME_TAB_EVENT = "doc:storage";
|
|
5
|
+
function readDocStorage(key) {
|
|
6
|
+
if (typeof window === "undefined") return void 0;
|
|
7
|
+
const fullKey = docKey(key);
|
|
8
|
+
try {
|
|
9
|
+
const raw = localStorage.getItem(fullKey);
|
|
10
|
+
return raw === null ? void 0 : JSON.parse(raw);
|
|
11
|
+
} catch (err) {
|
|
12
|
+
return void 0;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function writeDocStorage(key, value) {
|
|
16
|
+
if (typeof window === "undefined") return;
|
|
17
|
+
const fullKey = docKey(key);
|
|
18
|
+
try {
|
|
19
|
+
localStorage.setItem(fullKey, JSON.stringify(value));
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
window.dispatchEvent(
|
|
23
|
+
new CustomEvent(SAME_TAB_EVENT, { detail: { key: fullKey, value } })
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
function subscribeDocStorage(key, onChange) {
|
|
27
|
+
if (typeof window === "undefined") return () => {
|
|
28
|
+
};
|
|
29
|
+
const fullKey = docKey(key);
|
|
30
|
+
const onStorage = (e) => {
|
|
31
|
+
if (e.key === fullKey || e.key === null) onChange(null);
|
|
32
|
+
};
|
|
33
|
+
const onSameTab = (e) => {
|
|
34
|
+
const detail = e.detail;
|
|
35
|
+
if (detail?.key === fullKey) onChange({ value: detail.value });
|
|
36
|
+
};
|
|
37
|
+
window.addEventListener("storage", onStorage);
|
|
38
|
+
window.addEventListener(SAME_TAB_EVENT, onSameTab);
|
|
39
|
+
return () => {
|
|
40
|
+
window.removeEventListener("storage", onStorage);
|
|
41
|
+
window.removeEventListener(SAME_TAB_EVENT, onSameTab);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
5
44
|
function useLocalStorage(key, defaultValue) {
|
|
6
45
|
const fullKey = docKey(key);
|
|
7
46
|
const [value, setValue] = useState(defaultValue);
|
|
8
47
|
const defaultRef = useRef(defaultValue);
|
|
9
48
|
defaultRef.current = defaultValue;
|
|
10
49
|
useEffect(() => {
|
|
11
|
-
if (typeof window === "undefined") return;
|
|
12
50
|
const read = () => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (raw === null) {
|
|
16
|
-
setValue(defaultRef.current);
|
|
17
|
-
} else {
|
|
18
|
-
setValue(JSON.parse(raw));
|
|
19
|
-
}
|
|
20
|
-
} catch (err) {
|
|
21
|
-
}
|
|
51
|
+
const stored = readDocStorage(fullKey);
|
|
52
|
+
setValue(stored === void 0 ? defaultRef.current : stored);
|
|
22
53
|
};
|
|
23
54
|
read();
|
|
24
|
-
|
|
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
|
-
};
|
|
55
|
+
return subscribeDocStorage(fullKey, () => read());
|
|
37
56
|
}, [fullKey]);
|
|
38
57
|
const set = useCallback(
|
|
39
58
|
(next) => {
|
|
40
59
|
setValue(next);
|
|
41
|
-
|
|
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
|
-
);
|
|
60
|
+
writeDocStorage(fullKey, next);
|
|
49
61
|
},
|
|
50
62
|
[fullKey]
|
|
51
63
|
);
|
|
@@ -54,5 +66,8 @@ function useLocalStorage(key, defaultValue) {
|
|
|
54
66
|
export {
|
|
55
67
|
DOC_PREFIX,
|
|
56
68
|
docKey,
|
|
57
|
-
|
|
69
|
+
readDocStorage,
|
|
70
|
+
subscribeDocStorage,
|
|
71
|
+
useLocalStorage,
|
|
72
|
+
writeDocStorage
|
|
58
73
|
};
|
package/dist/vite.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mdxFrontmatterLoader } from "./lib/mdx-frontmatter-loader.js";
|
|
2
|
+
import { devMetadataPlugin } from "./lib/dev-metadata-plugin.js";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import react from "@vitejs/plugin-react";
|
|
4
5
|
import vike from "vike/plugin";
|
|
@@ -30,6 +31,7 @@ function mdxRawLoader() {
|
|
|
30
31
|
}
|
|
31
32
|
function dockit() {
|
|
32
33
|
return [
|
|
34
|
+
devMetadataPlugin(),
|
|
33
35
|
tailwindcss(),
|
|
34
36
|
mdxRawLoader(),
|
|
35
37
|
mdxFrontmatterLoader(),
|
package/package.json
CHANGED