fumadocs-core 16.15.12 → 16.15.14

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.
@@ -12,6 +12,17 @@ const HeadingTags = /* @__PURE__ */ new Set([
12
12
  "h5",
13
13
  "h6"
14
14
  ]);
15
+ /**
16
+ * Read the step number `remarkSteps` assigns to a heading.
17
+ */
18
+ function getStep(properties) {
19
+ const raw = properties["data-fd-step"] ?? properties.dataFdStep;
20
+ if (typeof raw === "number") return raw;
21
+ if (typeof raw === "string" && raw.trim() !== "") {
22
+ const step = Number(raw);
23
+ if (Number.isFinite(step)) return step;
24
+ }
25
+ }
15
26
  function rehypeToc({ exportToc = true } = {}) {
16
27
  if (exportToc === true) exportToc = {
17
28
  as: "esm",
@@ -42,7 +53,7 @@ function rehypeToc({ exportToc = true } = {}) {
42
53
  title: element,
43
54
  depth: Number(element.tagName[1]),
44
55
  url: `#${id}`,
45
- _step: typeof element.properties["data-fd-step"] === "number" ? element.properties["data-fd-step"] : void 0
56
+ _step: getStep(element.properties)
46
57
  });
47
58
  if (isTocOnly && parent && typeof idx === "number") parent.children.splice(idx, 1);
48
59
  return "skip";
package/dist/toc.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { t as isEqualShallow } from "./is-equal-LdLqRs0o.js";
3
- import { createContext, use, useEffect, useEffectEvent, useMemo, useRef, useState } from "react";
3
+ import { createContext, use, useEffect, useEffectEvent, useMemo, useRef, useSyncExternalStore } from "react";
4
4
  import { jsx } from "react/jsx-runtime";
5
5
  import scrollIntoView from "scroll-into-view-if-needed";
6
6
  //#region src/utils/merge-refs.ts
@@ -24,11 +24,7 @@ function ScrollProvider({ containerRef, children }) {
24
24
  });
25
25
  }
26
26
  function AnchorProvider({ toc, single = false, children }) {
27
- const observer = useMemo(() => new Observer(), []);
28
- observer.single = single;
29
- useEffect(() => {
30
- observer.setItems(toc);
31
- }, [observer, toc]);
27
+ const observer = useMemo(() => new Observer(toc, single), [toc, single]);
32
28
  useEffect(() => {
33
29
  observer.watch({ threshold: .9 });
34
30
  return () => observer.unwatch();
@@ -38,48 +34,43 @@ function AnchorProvider({ toc, single = false, children }) {
38
34
  children
39
35
  });
40
36
  }
41
- function TOCItem({ ref, onActiveChange = () => null, autoScroll = true, ...props }) {
37
+ function TOCItem({ ref, onActiveChange, autoScroll = true, ...props }) {
42
38
  const id = props.href ? getItemId(props.href) : null;
43
39
  const containerRef = use(ScrollContext);
44
40
  const anchorRef = useRef(null);
45
- const isInitialRef = useRef(true);
41
+ const isMountedRef = useRef(false);
46
42
  const observer = useObserver();
47
- const [active, setActive] = useState(() => observer.items.some((item) => item.id === id && item.active));
48
- const onUpdate = useEffectEvent((items, initial) => {
49
- const itemData = id ? items.find((item) => item.id === id) : null;
50
- if (!itemData) return;
51
- if (itemData.active !== active) {
52
- setActive(itemData.active);
53
- onActiveChange(itemData.active);
54
- }
55
- const anchor = anchorRef.current;
56
- const container = containerRef?.current;
57
- if (!autoScroll || !anchor || !container) return;
58
- let lastActive;
59
- for (const item of items) {
60
- if (!item.active) continue;
61
- if (!lastActive || lastActive.t < item.t) lastActive = item;
62
- }
63
- if (lastActive?.id === id) scrollIntoView(anchor, {
64
- behavior: initial ? "instant" : "smooth",
65
- block: "center",
66
- inline: "center",
67
- scrollMode: "always",
68
- boundary: container
69
- });
70
- });
71
- useEffect(() => {
72
- if (autoScroll) onUpdate(observer.items, isInitialRef.current);
73
- isInitialRef.current = false;
74
- }, [observer, autoScroll]);
43
+ const active = useObserverValue((observer) => id !== null && observer.activeAnchors.includes(id));
44
+ const prevActiveRef = useRef(active);
45
+ if (prevActiveRef.current !== active) {
46
+ prevActiveRef.current = active;
47
+ onActiveChange?.(active);
48
+ }
75
49
  useEffect(() => {
76
- const listener = (items) => onUpdate(items, false);
77
- observer.listen(listener);
78
- return () => observer.unlisten(listener);
79
- }, [observer]);
50
+ if (id === null) return;
51
+ const callback = () => {
52
+ const anchor = anchorRef.current;
53
+ const container = containerRef?.current;
54
+ if (autoScroll && observer.activeAnchor === id && anchor && container) scrollIntoView(anchor, {
55
+ behavior: isMountedRef.current ? "smooth" : "instant",
56
+ block: "center",
57
+ inline: "center",
58
+ scrollMode: "always",
59
+ boundary: container
60
+ });
61
+ };
62
+ callback();
63
+ isMountedRef.current = true;
64
+ return observer.subscribe(callback);
65
+ }, [
66
+ autoScroll,
67
+ id,
68
+ observer,
69
+ containerRef
70
+ ]);
80
71
  return /* @__PURE__ */ jsx("a", {
81
72
  ref: mergeRefs(anchorRef, ref),
82
- "data-active": active,
73
+ "data-active": String(active),
83
74
  ...props
84
75
  });
85
76
  }
@@ -88,6 +79,12 @@ function useObserver() {
88
79
  if (!observer) throw new Error(`Component must be used under the <AnchorProvider /> component.`);
89
80
  return observer;
90
81
  }
82
+ /** subscribe to a value of the observer, it must be stable between updates */
83
+ function useObserverValue(get) {
84
+ const observer = useObserver();
85
+ const getSnapshot = () => get(observer);
86
+ return useSyncExternalStore(observer.subscribe, getSnapshot, getSnapshot);
87
+ }
91
88
  /** @returns static info object, useful for custom rendering logic */
92
89
  function useTOC() {
93
90
  const observer = useObserver();
@@ -102,75 +99,58 @@ function useTOC() {
102
99
  function useTOCListener(listener) {
103
100
  const observer = useObserver();
104
101
  const callback = useEffectEvent(listener);
105
- useEffect(() => {
106
- observer.listen(callback);
107
- return () => observer.unlisten(callback);
108
- }, [observer]);
102
+ useEffect(() => observer.subscribe(callback), [observer]);
109
103
  }
110
104
  function useTOCSelector(select, isEqual = isEqualShallow) {
111
- const observer = useObserver();
112
- const [value, setValue] = useState(() => select(observer.items));
113
- useTOCListener((items) => {
105
+ const cache = useRef(null);
106
+ return useObserverValue(({ items }) => {
107
+ const prev = cache.current;
108
+ if (prev && prev.items === items && prev.select === select) return prev.value;
114
109
  const next = select(items);
115
- if (!isEqual(value, next)) setValue(next);
110
+ const value = prev && isEqual(prev.value, next) ? prev.value : next;
111
+ cache.current = {
112
+ items,
113
+ select,
114
+ value
115
+ };
116
+ return value;
116
117
  });
117
- return value;
118
118
  }
119
119
  /**
120
120
  * The estimated active heading ID
121
121
  */
122
122
  function useActiveAnchor() {
123
- return useTOCSelector((items) => {
124
- let out;
125
- for (const item of items) {
126
- if (!item.active) continue;
127
- if (!out || item.t > out.t) out = item;
128
- }
129
- return out?.id;
130
- });
123
+ return useObserverValue((observer) => observer.activeAnchor);
131
124
  }
132
125
  /**
133
126
  * The id of visible anchors
134
127
  */
135
128
  function useActiveAnchors() {
136
- return useTOCSelector((items) => {
137
- const out = [];
138
- for (const item of items) if (item.active) out.push(item.id);
139
- return out;
140
- });
129
+ return useObserverValue((observer) => observer.activeAnchors);
141
130
  }
142
131
  function useItems() {
143
- return useTOCSelector((items) => items);
132
+ return useObserverValue((observer) => observer.items);
144
133
  }
145
134
  function getItemId(url) {
146
135
  if (url.startsWith("#")) return url.slice(1);
147
136
  return null;
148
137
  }
149
138
  var Observer = class {
150
- constructor() {
151
- this.items = [];
152
- this.single = false;
139
+ constructor(toc, single) {
140
+ this.single = single;
141
+ this.activeAnchors = [];
153
142
  this.observer = null;
154
143
  this.listeners = /* @__PURE__ */ new Set();
155
- }
156
- listen(listener) {
157
- this.listeners.add(listener);
158
- }
159
- unlisten(listener) {
160
- this.listeners.delete(listener);
161
- }
162
- setItems(newItems) {
163
- const observer = this.observer;
164
- if (observer) for (const item of this.items) {
165
- const element = document.getElementById(item.id);
166
- if (!element) continue;
167
- observer.unobserve(element);
168
- }
169
- const next = [];
170
- for (const item of newItems) {
144
+ this.subscribe = (listener) => {
145
+ this.listeners.add(listener);
146
+ return () => {
147
+ this.listeners.delete(listener);
148
+ };
149
+ };
150
+ this.items = [];
151
+ for (const item of toc) {
171
152
  const id = getItemId(item.url);
172
- if (!id) continue;
173
- next.push({
153
+ if (id) this.items.push({
174
154
  id,
175
155
  active: false,
176
156
  fallback: false,
@@ -178,13 +158,21 @@ var Observer = class {
178
158
  original: item
179
159
  });
180
160
  }
181
- this.update(next);
182
- this.observeItems();
161
+ }
162
+ listen(listener) {
163
+ this.listeners.add(listener);
164
+ }
165
+ unlisten(listener) {
166
+ this.listeners.delete(listener);
183
167
  }
184
168
  watch(options) {
185
169
  if (this.observer) return;
186
170
  this.observer = new IntersectionObserver(this.callback.bind(this), options);
187
- this.observeItems();
171
+ for (const item of this.items) {
172
+ const element = document.getElementById(item.id);
173
+ if (!element) continue;
174
+ this.observer.observe(element);
175
+ }
188
176
  }
189
177
  unwatch() {
190
178
  this.observer?.disconnect();
@@ -228,16 +216,17 @@ var Observer = class {
228
216
  }
229
217
  this.update(updated);
230
218
  }
231
- observeItems() {
232
- if (!this.observer) return;
233
- for (const item of this.items) {
234
- const element = document.getElementById(item.id);
235
- if (!element) continue;
236
- this.observer.observe(element);
237
- }
238
- }
239
219
  update(next) {
220
+ let latest;
221
+ const activeAnchors = [];
222
+ for (const item of next) {
223
+ if (!item.active) continue;
224
+ activeAnchors.push(item.id);
225
+ if (!latest || item.t > latest.t) latest = item;
226
+ }
240
227
  this.items = next;
228
+ this.activeAnchors = activeAnchors;
229
+ this.activeAnchor = latest?.id;
241
230
  for (const listener of this.listeners) listener(next);
242
231
  }
243
232
  };
@@ -1,3 +1,6 @@
1
1
  //#region src/utils/use-media-query.d.ts
2
+ /**
3
+ * @returns `null` on the server, during hydration, or when disabled.
4
+ */
2
5
  export declare function useMediaQuery(query: string, disabled?: boolean): boolean | null;
3
6
  //#endregion
@@ -1,20 +1,17 @@
1
- import { useEffect, useState } from "react";
1
+ import { useCallback, useSyncExternalStore } from "react";
2
2
  //#region src/utils/use-media-query.ts
3
+ const getServerSnapshot = () => null;
4
+ /**
5
+ * @returns `null` on the server, during hydration, or when disabled.
6
+ */
3
7
  function useMediaQuery(query, disabled = false) {
4
- const [isMatch, setMatch] = useState(null);
5
- useEffect(() => {
6
- if (disabled) return;
7
- const mediaQueryList = window.matchMedia(query);
8
- const handleChange = () => {
9
- setMatch(mediaQueryList.matches);
10
- };
11
- handleChange();
12
- mediaQueryList.addEventListener("change", handleChange);
13
- return () => {
14
- mediaQueryList.removeEventListener("change", handleChange);
15
- };
16
- }, [disabled, query]);
17
- return isMatch;
8
+ const subscribe = useCallback((onChange) => {
9
+ if (disabled) return () => {};
10
+ const list = window.matchMedia(query);
11
+ list.addEventListener("change", onChange);
12
+ return () => list.removeEventListener("change", onChange);
13
+ }, [query, disabled]);
14
+ return useSyncExternalStore(subscribe, () => disabled ? null : window.matchMedia(query).matches, getServerSnapshot);
18
15
  }
19
16
  //#endregion
20
17
  export { useMediaQuery };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fumadocs-core",
3
- "version": "16.15.12",
3
+ "version": "16.15.14",
4
4
  "description": "The React.js library for building a documentation website",
5
5
  "keywords": [
6
6
  "Docs",
@@ -151,12 +151,13 @@
151
151
  "next": "16.3.5",
152
152
  "path-to-regexp": "^8.4.2",
153
153
  "react-router": "^8.4.0",
154
+ "rehype-raw": "^7.0.0",
154
155
  "remark-directive": "^4.0.0",
155
156
  "remark-mdx": "^3.1.1",
156
157
  "tsconfig": "0.0.0",
157
158
  "tsdown": "0.23.0",
158
159
  "typescript": "^7.0.2",
159
- "waku": "1.0.0-rc.0",
160
+ "waku": "1.0.0-rc.1",
160
161
  "zod": "4.6.5"
161
162
  },
162
163
  "peerDependencies": {