@dreamlake/dockit 0.2.30 → 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/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/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/package.json +8 -8
- package/templates/set-version.mjs +0 -0
- package/templates/version-branch.sh +0 -0
|
@@ -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/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,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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dreamlake/dockit",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "The DreamLake documentation shell
|
|
3
|
+
"version": "0.2.31",
|
|
4
|
+
"description": "The DreamLake documentation shell — Vike + React + Tailwind v4 + MDX docs framework with sidebar, TOC, search, and theming",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
@@ -38,11 +38,6 @@
|
|
|
38
38
|
},
|
|
39
39
|
"./styles.css": "./dist/styles.css"
|
|
40
40
|
},
|
|
41
|
-
"scripts": {
|
|
42
|
-
"build": "vite build",
|
|
43
|
-
"typecheck": "tsc --noEmit",
|
|
44
|
-
"test": "node --test tests/*.test.mjs"
|
|
45
|
-
},
|
|
46
41
|
"peerDependencies": {
|
|
47
42
|
"@mdx-js/react": ">=3.0.0",
|
|
48
43
|
"react": ">=19.0.0",
|
|
@@ -85,5 +80,10 @@
|
|
|
85
80
|
"vike-react": "^0.5.0",
|
|
86
81
|
"vite": "^7.0.0",
|
|
87
82
|
"vite-plugin-dts": "^4.3.0"
|
|
83
|
+
},
|
|
84
|
+
"scripts": {
|
|
85
|
+
"build": "vite build",
|
|
86
|
+
"typecheck": "tsc --noEmit",
|
|
87
|
+
"test": "node --test tests/*.test.mjs"
|
|
88
88
|
}
|
|
89
|
-
}
|
|
89
|
+
}
|
|
File without changes
|
|
File without changes
|