@terrafirmagreg-team/ui 0.0.0-stage → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/package.json +93 -3
  2. package/src/components/blocks/button/icon-button.tsx +49 -0
  3. package/src/components/blocks/canvas/chevron-edge.tsx +242 -0
  4. package/src/components/blocks/canvas/flow-canvas.tsx +181 -0
  5. package/src/components/blocks/canvas/flow-toolbar.tsx +33 -0
  6. package/src/components/blocks/canvas/flow-viewport.tsx +244 -0
  7. package/src/components/blocks/fit-box.tsx +26 -0
  8. package/src/components/blocks/git/github-info.tsx +78 -0
  9. package/src/components/blocks/lang/content.tsx +52 -0
  10. package/src/components/blocks/lang/flag.tsx +25 -0
  11. package/src/components/blocks/lang/link.tsx +23 -0
  12. package/src/components/blocks/lang/redirect.tsx +48 -0
  13. package/src/components/blocks/lang/switcher.tsx +54 -0
  14. package/src/components/blocks/lang/untranslated-callout.tsx +29 -0
  15. package/src/components/blocks/lang/untranslated-chip.tsx +53 -0
  16. package/src/components/blocks/link/link-preview.tsx +257 -0
  17. package/src/components/blocks/list/row.tsx +92 -0
  18. package/src/components/blocks/media/glb/model.tsx +244 -0
  19. package/src/components/blocks/media/glb/scene.tsx +373 -0
  20. package/src/components/blocks/media/glb/viewer.tsx +52 -0
  21. package/src/components/blocks/media/image.tsx +136 -0
  22. package/src/components/blocks/media/slideshow.tsx +178 -0
  23. package/src/components/blocks/nav/breadcrumb.tsx +51 -0
  24. package/src/components/blocks/nav/copy-menu.tsx +37 -0
  25. package/src/components/blocks/nav/link.tsx +86 -0
  26. package/src/components/blocks/nav/site-menu.tsx +78 -0
  27. package/src/components/blocks/nav/site-title.tsx +61 -0
  28. package/src/components/blocks/nav/switcher.tsx +206 -0
  29. package/src/components/blocks/page/empty-state.tsx +32 -0
  30. package/src/components/blocks/page/heading.tsx +54 -0
  31. package/src/components/blocks/page/pager.tsx +220 -0
  32. package/src/components/blocks/readability/fields.tsx +250 -0
  33. package/src/components/blocks/readability/menu.tsx +247 -0
  34. package/src/components/blocks/readability/track.tsx +165 -0
  35. package/src/components/blocks/search/command.tsx +80 -0
  36. package/src/components/blocks/search/dialog.tsx +724 -0
  37. package/src/components/blocks/search/provider.tsx +118 -0
  38. package/src/components/blocks/sidebar.tsx +406 -0
  39. package/src/components/blocks/sprite.tsx +231 -0
  40. package/src/components/blocks/text/format-text.tsx +110 -0
  41. package/src/components/blocks/theme/toggle.tsx +75 -0
  42. package/src/components/blocks/tooltip/hint.tsx +28 -0
  43. package/src/components/blocks/tooltip/lazy.tsx +41 -0
  44. package/src/components/blocks/viewer/controls.tsx +284 -0
  45. package/src/components/blocks/viewer/float-bar.tsx +51 -0
  46. package/src/components/layouts/layout.tsx +94 -0
  47. package/src/components/layouts/left-sidebar/content.tsx +63 -0
  48. package/src/components/layouts/left-sidebar/context.tsx +55 -0
  49. package/src/components/layouts/left-sidebar/expand.tsx +23 -0
  50. package/src/components/layouts/left-sidebar/footer.tsx +111 -0
  51. package/src/components/layouts/left-sidebar/index.ts +29 -0
  52. package/src/components/layouts/left-sidebar/items.tsx +103 -0
  53. package/src/components/layouts/left-sidebar/search.tsx +75 -0
  54. package/src/components/layouts/left-sidebar/sidebar.tsx +189 -0
  55. package/src/components/layouts/left-sidebar/tree.tsx +157 -0
  56. package/src/components/layouts/right-sidebar.tsx +96 -0
  57. package/src/components/layouts/tree-context.tsx +42 -0
  58. package/src/components/mdx/accordion.tsx +185 -0
  59. package/src/components/mdx/banner.tsx +157 -0
  60. package/src/components/mdx/callout.tsx +97 -0
  61. package/src/components/mdx/card.tsx +56 -0
  62. package/src/components/mdx/codeblock.tsx +250 -0
  63. package/src/components/mdx/collapsible.tsx +33 -0
  64. package/src/components/mdx/files.tsx +63 -0
  65. package/src/components/mdx/glow-header.tsx +54 -0
  66. package/src/components/mdx/gradient-text.tsx +28 -0
  67. package/src/components/mdx/icon.tsx +35 -0
  68. package/src/components/mdx/image-zoom.tsx +315 -0
  69. package/src/components/mdx/math.tsx +30 -0
  70. package/src/components/mdx/mermaid.tsx +23 -0
  71. package/src/components/mdx/multiblock.tsx +19 -0
  72. package/src/components/mdx/preview.tsx +101 -0
  73. package/src/components/mdx/steps.tsx +10 -0
  74. package/src/components/mdx/table.tsx +14 -0
  75. package/src/components/mdx/tabs.tsx +354 -0
  76. package/src/components/mdx/type-table.tsx +171 -0
  77. package/src/components/provider.tsx +139 -0
  78. package/src/components/ui/accordion.tsx +71 -0
  79. package/src/components/ui/alert-dialog.tsx +187 -0
  80. package/src/components/ui/alert.tsx +75 -0
  81. package/src/components/ui/aspect-ratio.tsx +22 -0
  82. package/src/components/ui/attachment.tsx +207 -0
  83. package/src/components/ui/avatar.tsx +108 -0
  84. package/src/components/ui/badge.tsx +51 -0
  85. package/src/components/ui/breadcrumb.tsx +124 -0
  86. package/src/components/ui/bubble.tsx +127 -0
  87. package/src/components/ui/button-group.tsx +87 -0
  88. package/src/components/ui/button.tsx +57 -0
  89. package/src/components/ui/calendar.tsx +221 -0
  90. package/src/components/ui/card.tsx +102 -0
  91. package/src/components/ui/carousel.tsx +242 -0
  92. package/src/components/ui/chart.tsx +371 -0
  93. package/src/components/ui/checkbox.tsx +28 -0
  94. package/src/components/ui/collapsible.tsx +21 -0
  95. package/src/components/ui/combobox.tsx +297 -0
  96. package/src/components/ui/command.tsx +198 -0
  97. package/src/components/ui/context-menu.tsx +270 -0
  98. package/src/components/ui/dialog.tsx +160 -0
  99. package/src/components/ui/direction.tsx +6 -0
  100. package/src/components/ui/drawer.tsx +227 -0
  101. package/src/components/ui/dropdown-menu.tsx +269 -0
  102. package/src/components/ui/empty.tsx +103 -0
  103. package/src/components/ui/field.tsx +238 -0
  104. package/src/components/ui/hover-card.tsx +50 -0
  105. package/src/components/ui/input-group.tsx +158 -0
  106. package/src/components/ui/input-otp.tsx +86 -0
  107. package/src/components/ui/input.tsx +19 -0
  108. package/src/components/ui/item.tsx +201 -0
  109. package/src/components/ui/kbd.tsx +26 -0
  110. package/src/components/ui/label.tsx +19 -0
  111. package/src/components/ui/marker.tsx +70 -0
  112. package/src/components/ui/menubar.tsx +280 -0
  113. package/src/components/ui/message-scroller.tsx +131 -0
  114. package/src/components/ui/message.tsx +91 -0
  115. package/src/components/ui/native-select.tsx +60 -0
  116. package/src/components/ui/navigation-menu.tsx +167 -0
  117. package/src/components/ui/pagination.tsx +130 -0
  118. package/src/components/ui/popover.tsx +91 -0
  119. package/src/components/ui/progress.tsx +82 -0
  120. package/src/components/ui/questionnaire.tsx +324 -0
  121. package/src/components/ui/radio-group.tsx +37 -0
  122. package/src/components/ui/resizable.tsx +49 -0
  123. package/src/components/ui/scroll-area.tsx +54 -0
  124. package/src/components/ui/select.tsx +200 -0
  125. package/src/components/ui/separator.tsx +24 -0
  126. package/src/components/ui/sheet.tsx +138 -0
  127. package/src/components/ui/sidebar.tsx +730 -0
  128. package/src/components/ui/skeleton.tsx +13 -0
  129. package/src/components/ui/slider.tsx +51 -0
  130. package/src/components/ui/spinner.tsx +10 -0
  131. package/src/components/ui/switch.tsx +31 -0
  132. package/src/components/ui/table.tsx +115 -0
  133. package/src/components/ui/tabs.tsx +81 -0
  134. package/src/components/ui/textarea.tsx +17 -0
  135. package/src/components/ui/toast.tsx +234 -0
  136. package/src/components/ui/toggle-group.tsx +89 -0
  137. package/src/components/ui/toggle.tsx +44 -0
  138. package/src/components/ui/tooltip.tsx +65 -0
  139. package/src/hooks/canvas/use-flow-hover.ts +19 -0
  140. package/src/hooks/canvas/use-flow-viewport-persist.ts +34 -0
  141. package/src/hooks/page/use-fit-pages.ts +92 -0
  142. package/src/hooks/page/use-location-search.ts +31 -0
  143. package/src/hooks/page/use-page-preview.ts +41 -0
  144. package/src/hooks/page/use-paged-list.ts +21 -0
  145. package/src/hooks/readability/use-readability.ts +87 -0
  146. package/src/hooks/search/use-dialog.ts +42 -0
  147. package/src/hooks/search/use-highlight-query.ts +58 -0
  148. package/src/hooks/search/use-provider.ts +26 -0
  149. package/src/hooks/use-async-value.ts +60 -0
  150. package/src/hooks/use-close-on-scroll.ts +24 -0
  151. package/src/hooks/use-copy-button.ts +47 -0
  152. package/src/hooks/use-cycle-index.ts +76 -0
  153. package/src/hooks/use-document-title.ts +10 -0
  154. package/src/hooks/use-element-size.ts +36 -0
  155. package/src/hooks/use-external-meta.ts +133 -0
  156. package/src/hooks/use-hot-key.ts +34 -0
  157. package/src/hooks/use-hover-follow.ts +55 -0
  158. package/src/hooks/use-in-view.ts +31 -0
  159. package/src/hooks/use-lang.ts +9 -0
  160. package/src/hooks/use-last-value.ts +16 -0
  161. package/src/hooks/use-layout.ts +19 -0
  162. package/src/hooks/use-marquee.ts +52 -0
  163. package/src/hooks/use-media-query.ts +15 -0
  164. package/src/hooks/use-mobile.ts +20 -0
  165. package/src/hooks/use-modifier-keys.ts +32 -0
  166. package/src/hooks/use-on-change.ts +36 -0
  167. package/src/hooks/use-pane-keys.ts +29 -0
  168. package/src/hooks/use-sidebar.ts +64 -0
  169. package/src/hooks/use-swipe.ts +27 -0
  170. package/src/hooks/use-tree.ts +100 -0
  171. package/src/hooks/viewer/use-in-float-bar.ts +7 -0
  172. package/src/hooks/viewer/use-viewer-prefs.ts +78 -0
  173. package/src/lib/canvas/edge.ts +202 -0
  174. package/src/lib/canvas/geometry.ts +54 -0
  175. package/src/lib/canvas/state.ts +48 -0
  176. package/src/lib/canvas/viewport.ts +38 -0
  177. package/src/lib/canvas/xyflow.ts +2 -0
  178. package/src/lib/config.ts +106 -0
  179. package/src/lib/mdx/callout.ts +21 -0
  180. package/src/lib/mdx/gradient.ts +16 -0
  181. package/src/lib/mdx/mermaid.ts +15 -0
  182. package/src/lib/media/crop-opaque.ts +77 -0
  183. package/src/lib/media/nearby-indexes.ts +7 -0
  184. package/src/lib/media/trim-transparent.ts +118 -0
  185. package/src/lib/readability/config.ts +11 -0
  186. package/src/lib/readability/spotlight.ts +187 -0
  187. package/src/lib/readability/store.ts +147 -0
  188. package/src/lib/tree.ts +90 -0
  189. package/src/lib/utils/cn.ts +2 -0
  190. package/src/lib/utils/format.ts +22 -0
  191. package/src/lib/utils/highlight-style.ts +10 -0
  192. package/src/lib/utils/history.ts +10 -0
  193. package/src/lib/utils/merge-refs.ts +14 -0
  194. package/src/lib/utils/nav-active.ts +59 -0
  195. package/src/lib/utils/stored-value.ts +137 -0
  196. package/src/lib/utils/urls.ts +18 -0
  197. package/src/locales/en_us.json +110 -0
  198. package/src/locales/pl_pl.json +33 -0
  199. package/src/locales/ru_ru.json +110 -0
  200. package/src/locales/uk_ua.json +33 -0
  201. package/src/locales/zh_cn.json +15 -0
  202. package/src/locales/zh_tw.json +15 -0
  203. package/src/locales.ts +6 -0
  204. package/src/types/canvas.ts +54 -0
  205. package/src/types/glb.ts +26 -0
  206. package/src/types/mdx-directives.ts +106 -0
  207. package/src/types/nav.ts +96 -0
  208. package/src/types/preview.ts +15 -0
  209. package/src/types/readability.ts +29 -0
  210. package/src/types/search.ts +57 -0
  211. package/src/types/tree.ts +19 -0
  212. package/src/types/viewer.ts +54 -0
  213. package/styles/content/base.css +198 -0
  214. package/styles/content/default-colors.css +51 -0
  215. package/styles/content/shiki.css +107 -0
  216. package/styles/content/typography.mjs +3637 -0
  217. package/styles/content.css +5 -0
  218. package/styles/image-zoom.css +83 -0
  219. package/styles/layout.css +28 -0
  220. package/styles/left-sidebar.css +140 -0
  221. package/styles/readability.css +50 -0
  222. package/styles/right-sidebar.css +6 -0
  223. package/styles/workspace.css +26 -0
  224. package/styles.css +430 -0
  225. package/README.md +0 -3
@@ -0,0 +1,76 @@
1
+ 'use client';
2
+
3
+ import {
4
+ useCallback,
5
+ useState,
6
+ useSyncExternalStore,
7
+ } from 'react';
8
+
9
+ const INTERVAL_MS = 4000;
10
+ const TICK_MS = 250;
11
+
12
+ const listeners = new Set<() => void>();
13
+ let timer: number | undefined;
14
+ let ticks = 0;
15
+
16
+ function subscribeTick(listener: () => void) {
17
+ listeners.add(listener);
18
+ timer ??= window.setInterval(() => {
19
+ ticks += 1;
20
+ for (const listener of listeners) listener();
21
+ }, TICK_MS);
22
+ return () => {
23
+ listeners.delete(listener);
24
+ if (!listeners.size && timer !== undefined) {
25
+ window.clearInterval(timer);
26
+ timer = undefined;
27
+ }
28
+ };
29
+ }
30
+
31
+ const readTickServer = () => 0;
32
+
33
+ export function useCycleIndex(
34
+ length: number,
35
+ {
36
+ enabled = true,
37
+ paused = false,
38
+ intervalMs = INTERVAL_MS,
39
+ }: { enabled?: boolean; paused?: boolean; intervalMs?: number } = {},
40
+ ) {
41
+ const running = enabled && !paused && length > 1;
42
+ const interval = intervalMs > 0 ? intervalMs : INTERVAL_MS;
43
+ const span = Math.max(1, Math.round(interval / TICK_MS));
44
+ const [offset, setOffset] = useState(0);
45
+
46
+ const subscribe = useCallback(
47
+ (listener: () => void) =>
48
+ running ? subscribeTick(listener) : () => {},
49
+ [running],
50
+ );
51
+ const readStep = useCallback(
52
+ () => Math.floor(ticks / span),
53
+ [span],
54
+ );
55
+ const base = useSyncExternalStore(
56
+ subscribe,
57
+ readStep,
58
+ readTickServer,
59
+ );
60
+ const index =
61
+ length > 0
62
+ ? (((base + offset) % length) + length) % length
63
+ : 0;
64
+
65
+ const setIndex = useCallback(
66
+ (next: number | ((current: number) => number)) => {
67
+ const size = Math.max(length, 1);
68
+ const target = typeof next === 'function' ? next(index) : next;
69
+ setOffset((((target - base) % size) + size) % size);
70
+ },
71
+ [index, base, length],
72
+ );
73
+
74
+ return [index, setIndex] as const;
75
+ }
76
+
@@ -0,0 +1,10 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+
5
+ export function useDocumentTitle(title: string): void {
6
+ useEffect(() => {
7
+ document.title = title;
8
+ }, [title]);
9
+ }
10
+
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState, type RefObject } from 'react';
4
+
5
+ export function useElementSize(ref: RefObject<HTMLElement | null>) {
6
+ const [size, setSize] = useState({ w: 0, h: 0 });
7
+ const observed = useRef<{ el: Element; ro: ResizeObserver } | null>(null);
8
+
9
+ useEffect(() => {
10
+ const el = ref.current;
11
+ if (!el || observed.current?.el === el) return;
12
+ observed.current?.ro.disconnect();
13
+ const apply = (width: number, height: number) =>
14
+ setSize((prev) =>
15
+ prev.w === width && prev.h === height ? prev : { w: width, h: height },
16
+ );
17
+ const ro = new ResizeObserver((entries) => {
18
+ const rect = entries[0]?.contentRect;
19
+ if (rect) apply(rect.width, rect.height);
20
+ });
21
+ ro.observe(el);
22
+ observed.current = { el, ro };
23
+ apply(el.clientWidth, el.clientHeight);
24
+ });
25
+
26
+ useEffect(
27
+ () => () => {
28
+ observed.current?.ro.disconnect();
29
+ observed.current = null;
30
+ },
31
+ [],
32
+ );
33
+
34
+ return size;
35
+ }
36
+
@@ -0,0 +1,133 @@
1
+ 'use client';
2
+ import { useEffect, useState } from 'react';
3
+ import {
4
+ DEFAULT_EXTERNAL_META_PROVIDERS,
5
+ uiConfig,
6
+ type ExternalMetaProvider,
7
+ } from '@terrafirmagreg-team/ui/lib/config';
8
+ import { createAsyncCache } from '@terrafirmagreg-team/data/lib/async-cache';
9
+
10
+ export interface ExternalMeta {
11
+ title: string;
12
+ description?: string;
13
+ host: string;
14
+ }
15
+
16
+ const metaCache = createAsyncCache((href: string) =>
17
+ resolveMeta(new URL(href), AbortSignal.timeout(6000)),
18
+ );
19
+
20
+ function pick(data: unknown, path: string): unknown {
21
+ return path
22
+ .split('.')
23
+ .reduce<unknown>(
24
+ (acc, key) =>
25
+ acc && typeof acc === 'object'
26
+ ? (acc as Record<string, unknown>)[key]
27
+ : undefined,
28
+ data,
29
+ );
30
+ }
31
+
32
+ function pickText(data: unknown, path?: string): string | undefined {
33
+ const value = path ? pick(data, path) : undefined;
34
+ const text = typeof value === 'string' ? value.trim() : '';
35
+ return text || undefined;
36
+ }
37
+
38
+ function buildUrl(
39
+ template: string,
40
+ url: URL,
41
+ match: RegExpMatchArray | null,
42
+ ): string {
43
+ return template.replace(/\{(\w+)\}/g, (_, key: string) => {
44
+ if (key === 'href') return encodeURIComponent(url.href);
45
+ if (key === 'host') return url.hostname;
46
+ const group = match?.[Number(key)];
47
+ return group !== undefined ? encodeURIComponent(group) : '';
48
+ });
49
+ }
50
+
51
+ async function fetchMeta(
52
+ provider: ExternalMetaProvider,
53
+ url: URL,
54
+ match: RegExpMatchArray | null,
55
+ signal: AbortSignal,
56
+ ): Promise<ExternalMeta> {
57
+ const res = await fetch(buildUrl(provider.url, url, match), { signal });
58
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
59
+ const host = url.hostname;
60
+ if (provider.kind === 'html') {
61
+ const doc = new DOMParser().parseFromString(await res.text(), 'text/html');
62
+ return {
63
+ title:
64
+ doc
65
+ .querySelector("meta[property='og:title']")
66
+ ?.getAttribute('content')
67
+ ?.trim() ||
68
+ doc.title ||
69
+ host,
70
+ description:
71
+ doc
72
+ .querySelector(
73
+ "meta[property='og:description'], meta[name='description']",
74
+ )
75
+ ?.getAttribute('content') || undefined,
76
+ host,
77
+ };
78
+ }
79
+ const data = await res.json();
80
+ let title = pickText(data, provider.title) || host;
81
+ for (const [pattern, to] of provider.replace ?? []) {
82
+ title = title.replace(pattern, to);
83
+ }
84
+ return { title, description: pickText(data, provider.description), host };
85
+ }
86
+
87
+ async function resolveMeta(url: URL, signal: AbortSignal): Promise<ExternalMeta> {
88
+ const host = url.hostname;
89
+ for (const provider of uiConfig().externalMetaProviders ??
90
+ DEFAULT_EXTERNAL_META_PROVIDERS) {
91
+ if (!provider.host.test(host)) continue;
92
+ const match = provider.path ? url.pathname.match(provider.path) : null;
93
+ if (provider.path && !match) continue;
94
+ try {
95
+ return await fetchMeta(provider, url, match, signal);
96
+ } catch {
97
+
98
+ }
99
+ }
100
+ return { title: host, host };
101
+ }
102
+
103
+ export function loadExternalMeta(href: string): Promise<ExternalMeta> {
104
+ let url: URL;
105
+ try {
106
+ url = new URL(href);
107
+ } catch {
108
+ return Promise.resolve({ title: href, host: '' });
109
+ }
110
+ if (!/^https?:$/i.test(url.protocol)) {
111
+ return Promise.resolve({ title: href, host: url.hostname });
112
+ }
113
+ return metaCache(href);
114
+ }
115
+
116
+ export function useExternalMeta(
117
+ href: string,
118
+ active: boolean,
119
+ ): ExternalMeta | null {
120
+ const [meta, setMeta] = useState<ExternalMeta | null>(null);
121
+ useEffect(() => {
122
+ if (!active) return;
123
+ let mounted = true;
124
+ void loadExternalMeta(href).then((m) => {
125
+ if (mounted) setMeta(m);
126
+ });
127
+ return () => {
128
+ mounted = false;
129
+ };
130
+ }, [href, active]);
131
+ return meta;
132
+ }
133
+
@@ -0,0 +1,34 @@
1
+ import { useEffect, useEffectEvent } from 'react';
2
+
3
+ export function useHotKey(
4
+ onKeyDown: (e: KeyboardEvent) => void,
5
+ {
6
+ enabled = true,
7
+ ignoreTyping = false,
8
+ }: {
9
+ enabled?: boolean;
10
+
11
+ ignoreTyping?: boolean;
12
+ } = {},
13
+ ) {
14
+ const listener = useEffectEvent((e: KeyboardEvent) => {
15
+ if (e.defaultPrevented || e.isComposing || e.keyCode === 229) return;
16
+ if (ignoreTyping && isTypingTarget(e.target)) return;
17
+ onKeyDown(e);
18
+ });
19
+
20
+ useEffect(() => {
21
+ if (!enabled) return;
22
+ window.addEventListener('keydown', listener);
23
+ return () => window.removeEventListener('keydown', listener);
24
+ }, [enabled]);
25
+ }
26
+
27
+ function isTypingTarget(target: EventTarget | null): boolean {
28
+ if (!(target instanceof HTMLElement)) return false;
29
+ if (target.isContentEditable) return true;
30
+ if (['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)) return true;
31
+
32
+ return target.closest('[role="dialog"]') !== null;
33
+ }
34
+
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+ import { useCallback, type RefCallback } from 'react';
3
+
4
+ export function useHoverFollow(): RefCallback<HTMLElement> {
5
+ return useCallback<RefCallback<HTMLElement>>((area) => {
6
+ const block = area?.firstElementChild;
7
+ if (!area || !(block instanceof HTMLElement)) return;
8
+ let current: Element | null = null;
9
+ const show = (item: Element | null) => {
10
+ if (item === current) return;
11
+ block.style.transitionProperty = current ? '' : 'opacity';
12
+ current = item;
13
+ if (!item) {
14
+ block.style.opacity = '0';
15
+ return;
16
+ }
17
+ const rect = item.getBoundingClientRect();
18
+ const origin = area.getBoundingClientRect();
19
+ block.style.translate = `${rect.left - origin.left}px ${rect.top - origin.top}px`;
20
+ block.style.width = `${rect.width}px`;
21
+ block.style.height = `${rect.height}px`;
22
+ block.style.opacity = item.matches('[data-active=true]') ? '0' : '1';
23
+ };
24
+ const itemOf = (target: unknown) =>
25
+ target instanceof Element ? target.closest('a[href], button') : null;
26
+ const controller = new AbortController();
27
+ const { signal } = controller;
28
+ area.addEventListener(
29
+ 'pointerover',
30
+ (e) => {
31
+ const item = itemOf(e.target);
32
+ if (item && e.pointerType === 'mouse') show(item);
33
+ },
34
+ { signal },
35
+ );
36
+ area.addEventListener('pointerleave', () => show(null), { signal });
37
+ area.addEventListener(
38
+ 'focusin',
39
+ (e) => {
40
+ if (e.target instanceof Element && e.target.matches(':focus-visible'))
41
+ show(itemOf(e.target));
42
+ },
43
+ { signal },
44
+ );
45
+ area.addEventListener(
46
+ 'focusout',
47
+ () => {
48
+ if (!area.matches(':hover')) show(null);
49
+ },
50
+ { signal },
51
+ );
52
+ return () => controller.abort();
53
+ }, []);
54
+ }
55
+
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, type RefObject } from 'react';
4
+
5
+ export function useInView<T extends Element>(
6
+ ref: RefObject<T | null>,
7
+ rootMargin = '240px',
8
+ ): boolean {
9
+ const [inView, setInView] = useState(false);
10
+ useEffect(() => {
11
+ const el = ref.current;
12
+ if (!el || inView) return;
13
+ if (typeof IntersectionObserver === 'undefined') {
14
+ const id = requestAnimationFrame(() => setInView(true));
15
+ return () => cancelAnimationFrame(id);
16
+ }
17
+ const io = new IntersectionObserver(
18
+ ([entry]) => {
19
+ if (entry?.isIntersecting) {
20
+ setInView(true);
21
+ io.disconnect();
22
+ }
23
+ },
24
+ { rootMargin },
25
+ );
26
+ io.observe(el);
27
+ return () => io.disconnect();
28
+ }, [ref, rootMargin, inView]);
29
+ return inView;
30
+ }
31
+
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+ import { createContext, useContext } from 'react';
3
+
4
+ export const UntranslatedContext = createContext(false);
5
+
6
+ export function useUntranslated(): boolean {
7
+ return useContext(UntranslatedContext);
8
+ }
9
+
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ export function useLastValue<T>(
6
+ value: T | undefined,
7
+ key?: string,
8
+ ): T | undefined {
9
+ const [held, setHeld] = useState<{ key?: string; value: T } | undefined>();
10
+ const stale = held === undefined || held.key !== key;
11
+ if (value !== undefined && (stale || !Object.is(held?.value, value))) {
12
+ setHeld({ key, value });
13
+ }
14
+ return value ?? (stale ? undefined : held?.value);
15
+ }
16
+
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+ import {
3
+ createContext,
4
+ use,
5
+ type ComponentProps,
6
+ type ComponentType,
7
+ } from 'react';
8
+
9
+ export const LayoutContext = createContext<{
10
+ navTitle: ComponentType<ComponentProps<'a'>>;
11
+ } | null>(null);
12
+
13
+ export function useLayout() {
14
+ const context = use(LayoutContext);
15
+ if (!context)
16
+ throw new Error('Missing LayoutContext, expected under <Layout />.');
17
+ return context;
18
+ }
19
+
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+ import { useEffect, useRef, type RefObject } from 'react';
3
+
4
+ export function useMarquee<T extends HTMLElement>(): RefObject<T | null> {
5
+ const ref = useRef<T>(null);
6
+
7
+ useEffect(() => {
8
+ const outer = ref.current;
9
+ const inner = outer?.firstElementChild;
10
+ if (!outer || !(inner instanceof HTMLElement)) return;
11
+ const target =
12
+ outer.closest(
13
+ 'a, button, [role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"], [role="option"]',
14
+ ) ?? outer;
15
+
16
+ const measure = () => {
17
+ const overflow = inner.scrollWidth - outer.clientWidth;
18
+ outer.classList.toggle('marquee', overflow > 1);
19
+ if (overflow > 1) {
20
+ inner.style.setProperty('--marquee-x', `${-overflow}px`);
21
+ inner.style.setProperty(
22
+ '--marquee-duration',
23
+ `${Math.max(1.5, overflow / 30)}s`,
24
+ );
25
+ }
26
+ };
27
+ const start = () => {
28
+ measure();
29
+ outer.classList.add('marquee-run');
30
+ };
31
+ const stop = () => outer.classList.remove('marquee-run');
32
+
33
+ measure();
34
+ const observer = new ResizeObserver(measure);
35
+ observer.observe(outer);
36
+ observer.observe(inner);
37
+ target.addEventListener('pointerenter', start);
38
+ target.addEventListener('pointerleave', stop);
39
+ target.addEventListener('focusin', start);
40
+ target.addEventListener('focusout', stop);
41
+ return () => {
42
+ observer.disconnect();
43
+ target.removeEventListener('pointerenter', start);
44
+ target.removeEventListener('pointerleave', stop);
45
+ target.removeEventListener('focusin', start);
46
+ target.removeEventListener('focusout', stop);
47
+ };
48
+ }, []);
49
+
50
+ return ref;
51
+ }
52
+
@@ -0,0 +1,15 @@
1
+ 'use client';
2
+ import { useSyncExternalStore } from 'react';
3
+
4
+ export function useMediaQuery(query: string): boolean {
5
+ return useSyncExternalStore(
6
+ (onChange) => {
7
+ const mql = window.matchMedia(query);
8
+ mql.addEventListener('change', onChange);
9
+ return () => mql.removeEventListener('change', onChange);
10
+ },
11
+ () => window.matchMedia(query).matches,
12
+ () => false,
13
+ );
14
+ }
15
+
@@ -0,0 +1,20 @@
1
+ import * as React from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener("change", onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener("change", onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
20
+
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ export type ModifierKeys = {
6
+ shift: boolean;
7
+ ctrl: boolean;
8
+ alt: boolean;
9
+ };
10
+
11
+ const IDLE: ModifierKeys = { shift: false, ctrl: false, alt: false };
12
+
13
+ export function useModifierKeys(active = true): ModifierKeys {
14
+ const [keys, setKeys] = useState<ModifierKeys>(IDLE);
15
+ useEffect(() => {
16
+ if (!active) return;
17
+ const update = (e: KeyboardEvent) => {
18
+ setKeys({ shift: e.shiftKey, ctrl: e.ctrlKey, alt: e.altKey });
19
+ };
20
+ const reset = () => setKeys(IDLE);
21
+ window.addEventListener('keydown', update);
22
+ window.addEventListener('keyup', update);
23
+ window.addEventListener('blur', reset);
24
+ return () => {
25
+ window.removeEventListener('keydown', update);
26
+ window.removeEventListener('keyup', update);
27
+ window.removeEventListener('blur', reset);
28
+ };
29
+ }, [active]);
30
+ return keys;
31
+ }
32
+
@@ -0,0 +1,36 @@
1
+ import { useState } from 'react';
2
+
3
+ function isEqualShallow(a: unknown, b: unknown): boolean {
4
+ if (Object.is(a, b)) return true;
5
+ if (
6
+ !a ||
7
+ !b ||
8
+ typeof a !== 'object' ||
9
+ typeof b !== 'object' ||
10
+ Array.isArray(a) !== Array.isArray(b)
11
+ )
12
+ return false;
13
+ const aKeys = Object.keys(a);
14
+ const bKeys = Object.keys(b);
15
+ if (aKeys.length !== bKeys.length) return false;
16
+ return aKeys.every((key) =>
17
+ Object.is(
18
+ (a as Record<string, unknown>)[key],
19
+ (b as Record<string, unknown>)[key],
20
+ ),
21
+ );
22
+ }
23
+
24
+ export function useOnChange<T>(
25
+ value: T,
26
+ onChange: (current: T, previous: T) => void,
27
+ isUpdated: (prev: T, current: T) => boolean = (a, b) =>
28
+ !isEqualShallow(a, b),
29
+ ) {
30
+ const [prev, setPrev] = useState(value);
31
+ if (isUpdated(prev, value)) {
32
+ onChange(value, prev);
33
+ setPrev(value);
34
+ }
35
+ }
36
+
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+
5
+ export function usePaneKeys<T extends string>(
6
+ picks: Partial<Record<'r' | 'u' | 't', T>>,
7
+ onSelect: (id: T) => void,
8
+ ) {
9
+ const { r, u, t } = picks;
10
+ useEffect(() => {
11
+ const map = { r, u, t };
12
+ const onKey = (event: KeyboardEvent) => {
13
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
14
+ const target = event.target as HTMLElement | null;
15
+ if (
16
+ target?.closest(
17
+ 'input, textarea, select, [contenteditable="true"], [role="textbox"]',
18
+ )
19
+ ) {
20
+ return;
21
+ }
22
+ const id = map[event.key.toLowerCase() as 'r' | 'u' | 't'];
23
+ if (id) onSelect(id);
24
+ };
25
+ window.addEventListener('keydown', onKey);
26
+ return () => window.removeEventListener('keydown', onKey);
27
+ }, [r, u, t, onSelect]);
28
+ }
29
+
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+ import {
3
+ createContext,
4
+ use,
5
+ useEffect,
6
+ type RefObject,
7
+ } from 'react';
8
+ import scrollIntoView from 'scroll-into-view-if-needed';
9
+
10
+ export interface SidebarContextValue {
11
+ open: boolean;
12
+ setOpen: (v: boolean) => void;
13
+ collapsed: boolean;
14
+ setCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
15
+
16
+ closeOnRedirect: RefObject<boolean>;
17
+ defaultOpenLevel: number;
18
+ prefetch?: boolean;
19
+ mode: 'drawer' | 'full';
20
+ }
21
+
22
+ export interface FolderContextValue {
23
+ open: boolean;
24
+ setOpen: (v: boolean) => void;
25
+ depth: number;
26
+ collapsible: boolean;
27
+ }
28
+
29
+ export const SidebarContext = createContext<SidebarContextValue | null>(null);
30
+ export const FolderContext = createContext<FolderContextValue | null>(null);
31
+
32
+ export function useSidebar(): SidebarContextValue {
33
+ const ctx = use(SidebarContext);
34
+ if (!ctx)
35
+ throw new Error(
36
+ 'Missing SidebarContext, make sure you have wrapped the component in <DocsLayout /> and the context is available.',
37
+ );
38
+ return ctx;
39
+ }
40
+
41
+ export function useFolder(): FolderContextValue | null {
42
+ return use(FolderContext);
43
+ }
44
+
45
+ export function useFolderDepth(): number {
46
+ return use(FolderContext)?.depth ?? 0;
47
+ }
48
+
49
+ export function useAutoScroll(
50
+ active: boolean,
51
+ ref: RefObject<HTMLElement | null>,
52
+ ): void {
53
+ const { mode } = useSidebar();
54
+ useEffect(() => {
55
+ if (active && ref.current)
56
+ scrollIntoView(ref.current, {
57
+ boundary: document.getElementById(
58
+ mode === 'drawer' ? 'sidebar-mobile' : 'sidebar',
59
+ ),
60
+ scrollMode: 'if-needed',
61
+ });
62
+ }, [active, mode, ref]);
63
+ }
64
+