@dreamlake/dockit 0.2.30 → 0.2.32

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.
@@ -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
- navigator.clipboard.writeText(el.innerText).then(
32
- () => {
33
- setCopied(true);
34
- setTimeout(() => setCopied(false), 1400);
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
- if (!examples.length) return null;
11
- return /* @__PURE__ */ jsx("div", { "data-code-tabs": true, children: /* @__PURE__ */ jsx(CodeTabsContext.Provider, { value: true, children: /* @__PURE__ */ jsx(
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: examples.map((example, index) => ({
17
- value: String(example.key ?? index),
18
- label: example.props.label,
19
- children: example.props.children
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
  }
@@ -12,5 +12,10 @@ export interface PreviewProps {
12
12
  defaultTab?: 'preview' | 'source' | 'data';
13
13
  /** Force a height for the preview pane. */
14
14
  height?: number | string;
15
+ /** `split` shows the source and the live render side by side (source on
16
+ * the left), stacking source above preview when the frame is narrower
17
+ * than 560px. The preview pane is then a size container, so a demo's
18
+ * container queries follow the pane's width. Default `tabs`. */
19
+ layout?: 'tabs' | 'split';
15
20
  }
16
- export declare function Preview({ children, source, dataSource, filename, defaultTab, height, }: PreviewProps): import("react").JSX.Element;
21
+ export declare function Preview({ children, source, dataSource, filename, defaultTab, height, layout, }: PreviewProps): import("react").JSX.Element;
@@ -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();
@@ -160,9 +161,12 @@ function Preview({
160
161
  dataSource,
161
162
  filename,
162
163
  defaultTab = "preview",
163
- height
164
+ height,
165
+ layout = "tabs"
164
166
  }) {
165
- const [tab, setTab] = useState(defaultTab);
167
+ const split = layout === "split";
168
+ const [selectedTab, setTab] = useState(defaultTab);
169
+ const tab = split ? "source" : selectedTab;
166
170
  const [copied, setCopied] = useState(false);
167
171
  const [fullscreen, setFullscreen] = useState(false);
168
172
  const [showLines, setShowLines] = useLineNumbers();
@@ -175,21 +179,19 @@ function Preview({
175
179
  window.addEventListener("keydown", onKey);
176
180
  return () => window.removeEventListener("keydown", onKey);
177
181
  }, [fullscreen]);
178
- function copy() {
179
- navigator.clipboard.writeText(visibleSource).then(
180
- () => {
181
- setCopied(true);
182
- setTimeout(() => setCopied(false), 1400);
183
- },
184
- () => {
185
- }
186
- );
182
+ function copy(button) {
183
+ void copyWithGlyphBurst(button, visibleSource, { color: "var(--color-doc-template-ink)" }).then((ok) => {
184
+ if (!ok) return;
185
+ setCopied(true);
186
+ setTimeout(() => setCopied(false), 1400);
187
+ });
187
188
  }
188
189
  return /* @__PURE__ */ jsxs(
189
190
  "div",
190
191
  {
191
192
  "data-line-numbers": showLines ? "on" : "off",
192
- className: "border border-doc-template-faint bg-doc-template-panel",
193
+ ...split ? { "data-preview-layout": "split" } : null,
194
+ className: "dockit-preview border border-doc-template-faint bg-doc-template-panel",
193
195
  style: fullscreen ? {
194
196
  // Full-viewport overlay. `--color-doc-template-bg` is the page
195
197
  // surface token so the backdrop matches the site in both light
@@ -215,7 +217,7 @@ function Preview({
215
217
  background: "color-mix(in srgb, var(--color-doc-template-ink) 3%, transparent)"
216
218
  },
217
219
  children: [
218
- /* @__PURE__ */ jsx("div", { style: { display: "inline-flex", gap: 2 }, role: "tablist", children: ["preview", "source", ...dataSource ? ["data"] : []].map((t) => {
220
+ split ? /* @__PURE__ */ jsx("span", { style: { ...TAB, color: "var(--color-doc-template-muted)", cursor: "default", display: "inline-flex", alignItems: "center" }, children: "Source · Preview" }) : /* @__PURE__ */ jsx("div", { style: { display: "inline-flex", gap: 2 }, role: "tablist", children: ["preview", "source", ...dataSource ? ["data"] : []].map((t) => {
219
221
  const active = t === tab;
220
222
  return /* @__PURE__ */ jsx(
221
223
  "button",
@@ -264,7 +266,7 @@ function Preview({
264
266
  "button",
265
267
  {
266
268
  type: "button",
267
- onClick: copy,
269
+ onClick: (e) => copy(e.currentTarget),
268
270
  "aria-label": "Copy source",
269
271
  style: headerBtnStyle(copied),
270
272
  children: copied ? "Copied" : "Copy"
@@ -287,7 +289,31 @@ function Preview({
287
289
  ]
288
290
  }
289
291
  ),
290
- tab === "preview" ? /* @__PURE__ */ jsxs(
292
+ split ? /* @__PURE__ */ jsxs(
293
+ "div",
294
+ {
295
+ className: "dockit-preview-split",
296
+ style: fullscreen ? { flex: 1, minHeight: 0, overflow: "auto" } : void 0,
297
+ children: [
298
+ /* @__PURE__ */ jsx(
299
+ "div",
300
+ {
301
+ className: "dockit-preview-split-source",
302
+ style: { background: "var(--color-doc-template-code)", minWidth: 0 },
303
+ children: /* @__PURE__ */ jsx(HighlightedSource, { code: source, filename })
304
+ }
305
+ ),
306
+ /* @__PURE__ */ jsx(
307
+ "div",
308
+ {
309
+ className: "dockit-preview-split-render",
310
+ style: { padding: 24, minHeight: 80, minWidth: 0, ...height ? { height } : null },
311
+ children
312
+ }
313
+ )
314
+ ]
315
+ }
316
+ ) : tab === "preview" ? /* @__PURE__ */ jsxs(
291
317
  "div",
292
318
  {
293
319
  ...fullscreen ? { "data-preview-fill": "" } : null,
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
- 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
- }
51
+ const stored = readDocStorage(fullKey);
52
+ setValue(stored === void 0 ? defaultRef.current : stored);
22
53
  };
23
54
  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
- };
55
+ return subscribeDocStorage(fullKey, () => read());
37
56
  }, [fullKey]);
38
57
  const set = useCallback(
39
58
  (next) => {
40
59
  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
- );
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
- useLocalStorage
69
+ readDocStorage,
70
+ subscribeDocStorage,
71
+ useLocalStorage,
72
+ writeDocStorage
58
73
  };
package/dist/styles.css CHANGED
@@ -468,3 +468,15 @@
468
468
  /* Touch has no hover-away state: leave room so actions never mask code. */
469
469
  [data-code-block]:has(> .dockit-code-controls) > div:last-child { padding-top: 30px; }
470
470
  }
471
+
472
+ /* Preview layout="split": source left, live render right; stacked when the
473
+ frame is narrower than 560px. The render pane is a size container so a
474
+ demo's container queries follow the pane, not the page. */
475
+ .dockit-preview[data-preview-layout='split'] { container: dockit-preview / inline-size; }
476
+ .dockit-preview-split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
477
+ .dockit-preview-split-source { border-right: 1px solid var(--color-doc-template-faint); overflow-x: auto; }
478
+ .dockit-preview-split-render { container-type: inline-size; }
479
+ @container dockit-preview (max-width: 560px) {
480
+ .dockit-preview-split { grid-template-columns: minmax(0, 1fr); }
481
+ .dockit-preview-split-source { border-right: 0; border-bottom: 1px solid var(--color-doc-template-faint); }
482
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dreamlake/dockit",
3
- "version": "0.2.30",
4
- "description": "The DreamLake documentation shell \u2014 Vike + React + Tailwind v4 + MDX docs framework with sidebar, TOC, search, and theming",
3
+ "version": "0.2.32",
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