@terrafirmagreg-team/ui 0.0.0-stage → 0.1.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.
- package/package.json +93 -3
- package/src/components/blocks/button/icon-button.tsx +48 -0
- package/src/components/blocks/canvas/chevron-edge.tsx +134 -0
- package/src/components/blocks/canvas/flow-canvas.tsx +177 -0
- package/src/components/blocks/canvas/flow-toolbar.tsx +32 -0
- package/src/components/blocks/canvas/flow-viewport.tsx +243 -0
- package/src/components/blocks/git/github-info.tsx +77 -0
- package/src/components/blocks/lang/content.tsx +22 -0
- package/src/components/blocks/lang/flag.tsx +24 -0
- package/src/components/blocks/lang/link.tsx +22 -0
- package/src/components/blocks/lang/redirect.tsx +47 -0
- package/src/components/blocks/lang/switcher.tsx +53 -0
- package/src/components/blocks/lang/untranslated-callout.tsx +28 -0
- package/src/components/blocks/link/link-preview.tsx +256 -0
- package/src/components/blocks/list/row.tsx +91 -0
- package/src/components/blocks/media/glb/model.tsx +243 -0
- package/src/components/blocks/media/glb/scene.tsx +372 -0
- package/src/components/blocks/media/glb/viewer.tsx +51 -0
- package/src/components/blocks/media/image.tsx +135 -0
- package/src/components/blocks/media/slideshow.tsx +177 -0
- package/src/components/blocks/nav/breadcrumb.tsx +50 -0
- package/src/components/blocks/nav/link.tsx +85 -0
- package/src/components/blocks/nav/site-menu.tsx +77 -0
- package/src/components/blocks/nav/site-title.tsx +60 -0
- package/src/components/blocks/nav/switcher.tsx +205 -0
- package/src/components/blocks/page/empty-state.tsx +31 -0
- package/src/components/blocks/page/heading.tsx +53 -0
- package/src/components/blocks/page/pager.tsx +112 -0
- package/src/components/blocks/readability/menu.tsx +431 -0
- package/src/components/blocks/readability/track.tsx +164 -0
- package/src/components/blocks/search/command.tsx +79 -0
- package/src/components/blocks/search/dialog.tsx +712 -0
- package/src/components/blocks/search/provider.tsx +117 -0
- package/src/components/blocks/sidebar.tsx +406 -0
- package/src/components/blocks/theme/toggle.tsx +71 -0
- package/src/components/blocks/tooltip/hint.tsx +27 -0
- package/src/components/blocks/viewer/controls.tsx +279 -0
- package/src/components/blocks/viewer/float-bar.tsx +50 -0
- package/src/components/layouts/layout.tsx +93 -0
- package/src/components/layouts/left-sidebar/content.tsx +62 -0
- package/src/components/layouts/left-sidebar/context.tsx +54 -0
- package/src/components/layouts/left-sidebar/expand.tsx +22 -0
- package/src/components/layouts/left-sidebar/footer.tsx +105 -0
- package/src/components/layouts/left-sidebar/index.ts +28 -0
- package/src/components/layouts/left-sidebar/items.tsx +102 -0
- package/src/components/layouts/left-sidebar/search.tsx +74 -0
- package/src/components/layouts/left-sidebar/sidebar.tsx +188 -0
- package/src/components/layouts/left-sidebar/tree.tsx +156 -0
- package/src/components/layouts/right-sidebar.tsx +95 -0
- package/src/components/layouts/tree-context.tsx +41 -0
- package/src/components/mdx/accordion.tsx +184 -0
- package/src/components/mdx/banner.tsx +154 -0
- package/src/components/mdx/callout.tsx +96 -0
- package/src/components/mdx/card.tsx +49 -0
- package/src/components/mdx/codeblock.tsx +249 -0
- package/src/components/mdx/collapsible.tsx +32 -0
- package/src/components/mdx/files.tsx +62 -0
- package/src/components/mdx/glow-header.tsx +40 -0
- package/src/components/mdx/gradient-text.tsx +27 -0
- package/src/components/mdx/image-zoom.tsx +314 -0
- package/src/components/mdx/mermaid.tsx +16 -0
- package/src/components/mdx/multiblock.tsx +18 -0
- package/src/components/mdx/preview.tsx +100 -0
- package/src/components/mdx/steps.tsx +9 -0
- package/src/components/mdx/table.tsx +13 -0
- package/src/components/mdx/tabs.tsx +353 -0
- package/src/components/mdx/type-table.tsx +170 -0
- package/src/components/ui/accordion.tsx +71 -0
- package/src/components/ui/alert-dialog.tsx +187 -0
- package/src/components/ui/alert.tsx +75 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/attachment.tsx +207 -0
- package/src/components/ui/avatar.tsx +108 -0
- package/src/components/ui/badge.tsx +51 -0
- package/src/components/ui/breadcrumb.tsx +124 -0
- package/src/components/ui/bubble.tsx +127 -0
- package/src/components/ui/button-group.tsx +87 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/calendar.tsx +221 -0
- package/src/components/ui/card.tsx +102 -0
- package/src/components/ui/carousel.tsx +242 -0
- package/src/components/ui/chart.tsx +371 -0
- package/src/components/ui/checkbox.tsx +28 -0
- package/src/components/ui/collapsible.tsx +21 -0
- package/src/components/ui/combobox.tsx +297 -0
- package/src/components/ui/command.tsx +198 -0
- package/src/components/ui/context-menu.tsx +270 -0
- package/src/components/ui/dialog.tsx +160 -0
- package/src/components/ui/direction.tsx +6 -0
- package/src/components/ui/drawer.tsx +227 -0
- package/src/components/ui/dropdown-menu.tsx +269 -0
- package/src/components/ui/empty.tsx +103 -0
- package/src/components/ui/field.tsx +238 -0
- package/src/components/ui/hover-card.tsx +50 -0
- package/src/components/ui/input-group.tsx +158 -0
- package/src/components/ui/input-otp.tsx +86 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/item.tsx +201 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/marker.tsx +70 -0
- package/src/components/ui/menubar.tsx +280 -0
- package/src/components/ui/message-scroller.tsx +131 -0
- package/src/components/ui/message.tsx +91 -0
- package/src/components/ui/native-select.tsx +60 -0
- package/src/components/ui/navigation-menu.tsx +167 -0
- package/src/components/ui/pagination.tsx +130 -0
- package/src/components/ui/popover.tsx +91 -0
- package/src/components/ui/progress.tsx +82 -0
- package/src/components/ui/questionnaire.tsx +324 -0
- package/src/components/ui/radio-group.tsx +37 -0
- package/src/components/ui/resizable.tsx +49 -0
- package/src/components/ui/scroll-area.tsx +54 -0
- package/src/components/ui/select.tsx +200 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +730 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +51 -0
- package/src/components/ui/spinner.tsx +10 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +115 -0
- package/src/components/ui/tabs.tsx +81 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toast.tsx +234 -0
- package/src/components/ui/toggle-group.tsx +89 -0
- package/src/components/ui/toggle.tsx +44 -0
- package/src/components/ui/tooltip.tsx +65 -0
- package/src/hooks/canvas/use-flow-hover.ts +18 -0
- package/src/hooks/canvas/use-flow-viewport-persist.ts +33 -0
- package/src/hooks/lang.ts +8 -0
- package/src/hooks/layout.ts +18 -0
- package/src/hooks/page/use-fit-pages.ts +91 -0
- package/src/hooks/page/use-location-search.ts +25 -0
- package/src/hooks/page/use-page-preview.ts +40 -0
- package/src/hooks/page/use-paged-list.ts +20 -0
- package/src/hooks/readability/provider.ts +86 -0
- package/src/hooks/search/dialog.ts +41 -0
- package/src/hooks/search/provider.ts +25 -0
- package/src/hooks/search/use-highlight-query.ts +51 -0
- package/src/hooks/sidebar.ts +63 -0
- package/src/hooks/use-async-value.ts +57 -0
- package/src/hooks/use-close-on-scroll.ts +23 -0
- package/src/hooks/use-copy-button.ts +46 -0
- package/src/hooks/use-cycle-index.ts +75 -0
- package/src/hooks/use-document-title.ts +9 -0
- package/src/hooks/use-element-size.ts +35 -0
- package/src/hooks/use-external-meta.ts +132 -0
- package/src/hooks/use-hot-key.ts +33 -0
- package/src/hooks/use-hover-follow.ts +54 -0
- package/src/hooks/use-in-view.ts +30 -0
- package/src/hooks/use-last-value.ts +15 -0
- package/src/hooks/use-marquee.ts +51 -0
- package/src/hooks/use-media-query.ts +14 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-modifier-keys.ts +31 -0
- package/src/hooks/use-on-change.ts +35 -0
- package/src/hooks/use-pane-keys.ts +28 -0
- package/src/hooks/use-swipe.ts +26 -0
- package/src/hooks/use-tree.ts +102 -0
- package/src/hooks/viewer/use-in-float-bar.ts +6 -0
- package/src/hooks/viewer/use-viewer-prefs.ts +77 -0
- package/src/lib/canvas/edge.ts +173 -0
- package/src/lib/canvas/geometry.ts +53 -0
- package/src/lib/canvas/state.ts +47 -0
- package/src/lib/canvas/viewport.ts +37 -0
- package/src/lib/canvas/xyflow.ts +1 -0
- package/src/lib/config.ts +81 -0
- package/src/lib/mdx/callout.ts +20 -0
- package/src/lib/mdx/gradient.ts +15 -0
- package/src/lib/mdx/mermaid.ts +14 -0
- package/src/lib/media/crop-opaque.ts +76 -0
- package/src/lib/media/nearby-indexes.ts +6 -0
- package/src/lib/media/trim-transparent.ts +117 -0
- package/src/lib/readability/config.ts +10 -0
- package/src/lib/readability/spotlight.ts +186 -0
- package/src/lib/readability/store.ts +146 -0
- package/src/lib/tree.ts +92 -0
- package/src/lib/utils/async-cache.ts +32 -0
- package/src/lib/utils/cn.ts +1 -0
- package/src/lib/utils/format.ts +22 -0
- package/src/lib/utils/history.ts +9 -0
- package/src/lib/utils/merge-refs.ts +13 -0
- package/src/lib/utils/nav-active.ts +58 -0
- package/src/lib/utils/stored-value.ts +136 -0
- package/src/lib/utils/urls.ts +17 -0
- package/src/locales/en_us.json +119 -0
- package/src/locales/pl_pl.json +35 -0
- package/src/locales/ru_ru.json +119 -0
- package/src/locales/uk_ua.json +35 -0
- package/src/locales/zh_cn.json +17 -0
- package/src/locales/zh_tw.json +17 -0
- package/src/locales.ts +5 -0
- package/src/provider.tsx +141 -0
- package/src/types/canvas.ts +49 -0
- package/src/types/glb.ts +25 -0
- package/src/types/nav.ts +95 -0
- package/src/types/preview.ts +9 -0
- package/src/types/readability.ts +28 -0
- package/src/types/search.ts +56 -0
- package/src/types/tree.ts +18 -0
- package/src/types/viewer.ts +53 -0
- package/styles/content/base.css +194 -0
- package/styles/content/default-colors.css +51 -0
- package/styles/content/shiki.css +108 -0
- package/styles/content/typography.mjs +3941 -0
- package/styles/content.css +7 -0
- package/styles/image-zoom.css +83 -0
- package/styles/layout.css +28 -0
- package/styles/left-sidebar.css +140 -0
- package/styles/readability.css +50 -0
- package/styles/right-sidebar.css +6 -0
- package/styles/workspace.css +26 -0
- package/styles.css +413 -0
- package/README.md +0 -3
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
|
|
4
|
+
import { cn } from "cn"
|
|
5
|
+
|
|
6
|
+
function TooltipProvider({
|
|
7
|
+
delay = 0,
|
|
8
|
+
...props
|
|
9
|
+
}: TooltipPrimitive.Provider.Props) {
|
|
10
|
+
return (
|
|
11
|
+
<TooltipPrimitive.Provider
|
|
12
|
+
data-slot="tooltip-provider"
|
|
13
|
+
delay={delay}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
|
20
|
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
|
24
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function TooltipContent({
|
|
28
|
+
className,
|
|
29
|
+
side = "top",
|
|
30
|
+
sideOffset = 4,
|
|
31
|
+
align = "center",
|
|
32
|
+
alignOffset = 0,
|
|
33
|
+
children,
|
|
34
|
+
...props
|
|
35
|
+
}: TooltipPrimitive.Popup.Props &
|
|
36
|
+
Pick<
|
|
37
|
+
TooltipPrimitive.Positioner.Props,
|
|
38
|
+
"align" | "alignOffset" | "side" | "sideOffset"
|
|
39
|
+
>) {
|
|
40
|
+
return (
|
|
41
|
+
<TooltipPrimitive.Portal>
|
|
42
|
+
<TooltipPrimitive.Positioner
|
|
43
|
+
align={align}
|
|
44
|
+
alignOffset={alignOffset}
|
|
45
|
+
side={side}
|
|
46
|
+
sideOffset={sideOffset}
|
|
47
|
+
className="isolate z-overlay"
|
|
48
|
+
>
|
|
49
|
+
<TooltipPrimitive.Popup
|
|
50
|
+
data-slot="tooltip-content"
|
|
51
|
+
className={cn(
|
|
52
|
+
"z-overlay inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-overlay **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
53
|
+
className
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
<TooltipPrimitive.Arrow className="z-overlay size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
|
59
|
+
</TooltipPrimitive.Popup>
|
|
60
|
+
</TooltipPrimitive.Positioner>
|
|
61
|
+
</TooltipPrimitive.Portal>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useMemo, useState } from 'react';
|
|
4
|
+
import type { FlowHoverValue } from '@terrafirmagreg-team/ui/types/canvas';
|
|
5
|
+
|
|
6
|
+
export const FlowHoverContext = createContext<FlowHoverValue>({
|
|
7
|
+
id: null,
|
|
8
|
+
setId: () => {},
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export function useFlowHover(): FlowHoverValue {
|
|
12
|
+
const [id, setId] = useState<string | null>(null);
|
|
13
|
+
return useMemo(() => ({ id, setId }), [id]);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useFlowHoverState(): FlowHoverValue {
|
|
17
|
+
return useContext(FlowHoverContext);
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
import { VIEWPORT_SAVE_MS, writeStoredViewport } from '@terrafirmagreg-team/ui/lib/canvas/viewport';
|
|
5
|
+
import type { StoredViewport } from '@terrafirmagreg-team/ui/types/canvas';
|
|
6
|
+
|
|
7
|
+
export function useFlowViewportPersist(storageKey: string, resetKey: string) {
|
|
8
|
+
const ready = useRef(false);
|
|
9
|
+
const timer = useRef(0);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
ready.current = false;
|
|
13
|
+
}, [resetKey]);
|
|
14
|
+
|
|
15
|
+
useEffect(() => () => window.clearTimeout(timer.current), [storageKey]);
|
|
16
|
+
|
|
17
|
+
const markReady = useCallback(() => {
|
|
18
|
+
ready.current = true;
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const persist = useCallback(
|
|
22
|
+
(viewport: StoredViewport) => {
|
|
23
|
+
if (!ready.current) return;
|
|
24
|
+
window.clearTimeout(timer.current);
|
|
25
|
+
timer.current = window.setTimeout(() => {
|
|
26
|
+
writeStoredViewport(storageKey, viewport);
|
|
27
|
+
}, VIEWPORT_SAVE_MS);
|
|
28
|
+
},
|
|
29
|
+
[storageKey],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return { markReady, persist };
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo, type RefObject } from 'react';
|
|
4
|
+
import { useElementSize } from '@terrafirmagreg-team/ui/hooks/use-element-size';
|
|
5
|
+
|
|
6
|
+
export type FitDim = { w: number; h: number };
|
|
7
|
+
|
|
8
|
+
type FitPageList<T> = {
|
|
9
|
+
page: number;
|
|
10
|
+
pages: number;
|
|
11
|
+
shown: T[];
|
|
12
|
+
to: number;
|
|
13
|
+
total: number;
|
|
14
|
+
measured: boolean;
|
|
15
|
+
box: FitDim;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function useFitPages<T>(
|
|
19
|
+
boxRef: RefObject<HTMLElement | null>,
|
|
20
|
+
items: readonly T[],
|
|
21
|
+
dim: (item: T, box: FitDim) => FitDim,
|
|
22
|
+
urlPage: number,
|
|
23
|
+
gap = 8,
|
|
24
|
+
focus?: number,
|
|
25
|
+
vertical = false,
|
|
26
|
+
): FitPageList<T> {
|
|
27
|
+
const { w, h } = useElementSize(boxRef);
|
|
28
|
+
return useMemo(() => {
|
|
29
|
+
const measured = w > 0 && h > 0;
|
|
30
|
+
const box: FitDim = { w, h };
|
|
31
|
+
if (!measured) {
|
|
32
|
+
return {
|
|
33
|
+
page: 0,
|
|
34
|
+
pages: 1,
|
|
35
|
+
shown: [],
|
|
36
|
+
to: 0,
|
|
37
|
+
total: items.length,
|
|
38
|
+
measured,
|
|
39
|
+
box,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const packed: T[][] = [];
|
|
44
|
+
let page: T[] = [];
|
|
45
|
+
let shelfW = 0;
|
|
46
|
+
let shelfH = 0;
|
|
47
|
+
let usedH = 0;
|
|
48
|
+
let focusPage = -1;
|
|
49
|
+
items.forEach((item, i) => {
|
|
50
|
+
const d = dim(item, box);
|
|
51
|
+
const iw = vertical ? w : Math.max(1, d.w);
|
|
52
|
+
const ih = Math.max(1, d.h);
|
|
53
|
+
if (shelfW > 0 && shelfW + gap + iw > w) {
|
|
54
|
+
usedH += shelfH + (usedH > 0 ? gap : 0);
|
|
55
|
+
shelfW = 0;
|
|
56
|
+
shelfH = 0;
|
|
57
|
+
if (usedH + ih > h && page.length) {
|
|
58
|
+
packed.push(page);
|
|
59
|
+
page = [];
|
|
60
|
+
usedH = 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
shelfW = shelfW === 0 ? iw : shelfW + gap + iw;
|
|
64
|
+
shelfH = Math.max(shelfH, ih);
|
|
65
|
+
if (focus === i) focusPage = packed.length;
|
|
66
|
+
page.push(item);
|
|
67
|
+
});
|
|
68
|
+
if (page.length) packed.push(page);
|
|
69
|
+
if (!packed.length) packed.push([]);
|
|
70
|
+
|
|
71
|
+
const raw = Number.isFinite(urlPage) ? Math.floor(urlPage) : 0;
|
|
72
|
+
const clamped =
|
|
73
|
+
focusPage >= 0
|
|
74
|
+
? focusPage
|
|
75
|
+
: Math.max(0, Math.min(raw, packed.length - 1));
|
|
76
|
+
let to = 0;
|
|
77
|
+
for (let i = 0; i <= clamped && i < packed.length; i += 1) {
|
|
78
|
+
to += packed[i].length;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
page: clamped,
|
|
82
|
+
pages: packed.length,
|
|
83
|
+
shown: packed[clamped],
|
|
84
|
+
to,
|
|
85
|
+
total: items.length,
|
|
86
|
+
measured,
|
|
87
|
+
box,
|
|
88
|
+
};
|
|
89
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- dim is item-keyed
|
|
90
|
+
}, [items, w, h, urlPage, gap, focus, vertical]);
|
|
91
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore } from 'react';
|
|
4
|
+
|
|
5
|
+
let search = '';
|
|
6
|
+
const listeners = new Set<() => void>();
|
|
7
|
+
|
|
8
|
+
export function publishLocationSearch(value: string) {
|
|
9
|
+
if (search === value) return;
|
|
10
|
+
search = value;
|
|
11
|
+
for (const listener of listeners) listener();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function subscribe(listener: () => void) {
|
|
15
|
+
listeners.add(listener);
|
|
16
|
+
return () => listeners.delete(listener);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useLocationSearch(): string {
|
|
20
|
+
return useSyncExternalStore(
|
|
21
|
+
subscribe,
|
|
22
|
+
() => search,
|
|
23
|
+
() => '',
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { uiConfig } from '@terrafirmagreg-team/ui/lib/config';
|
|
4
|
+
import { createAsyncCache } from '@terrafirmagreg-team/ui/lib/utils/async-cache';
|
|
5
|
+
import type { PagePreviewData } from '@terrafirmagreg-team/ui/types/preview';
|
|
6
|
+
|
|
7
|
+
const previewCache = createAsyncCache(
|
|
8
|
+
async (key: string): Promise<PagePreviewData | undefined> => {
|
|
9
|
+
const [href, lang] = key.split(' ');
|
|
10
|
+
return uiConfig().loadPagePreview?.(href ?? '', { lang: lang || undefined });
|
|
11
|
+
},
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export function loadPagePreview(
|
|
15
|
+
href: string,
|
|
16
|
+
lang?: string,
|
|
17
|
+
): Promise<PagePreviewData | undefined> {
|
|
18
|
+
return previewCache(`${href} ${lang ?? ''}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function usePagePreview(
|
|
22
|
+
href: string,
|
|
23
|
+
active: boolean,
|
|
24
|
+
lang?: string,
|
|
25
|
+
): PagePreviewData | null | undefined {
|
|
26
|
+
const [preview, setPreview] = useState<PagePreviewData | null | undefined>(
|
|
27
|
+
null,
|
|
28
|
+
);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!active) return;
|
|
31
|
+
let mounted = true;
|
|
32
|
+
void loadPagePreview(href, lang).then((data) => {
|
|
33
|
+
if (mounted) setPreview(data);
|
|
34
|
+
});
|
|
35
|
+
return () => {
|
|
36
|
+
mounted = false;
|
|
37
|
+
};
|
|
38
|
+
}, [href, active, lang]);
|
|
39
|
+
return preview;
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
export function usePagedList<T>(
|
|
6
|
+
items: readonly T[],
|
|
7
|
+
pageIndex: number,
|
|
8
|
+
pageSize: number,
|
|
9
|
+
) {
|
|
10
|
+
return useMemo(() => {
|
|
11
|
+
const size = Math.max(1, Math.floor(pageSize) || 1);
|
|
12
|
+
const pages = Math.max(1, Math.ceil(items.length / size));
|
|
13
|
+
const raw = Number.isFinite(pageIndex) ? Math.floor(pageIndex) : 0;
|
|
14
|
+
const page = Math.max(0, Math.min(raw, pages - 1));
|
|
15
|
+
const shown = items.slice(page * size, (page + 1) * size);
|
|
16
|
+
const from = items.length ? page * size + 1 : 0;
|
|
17
|
+
const to = Math.min(items.length, (page + 1) * size);
|
|
18
|
+
return { page, pages, shown, from, to, total: items.length };
|
|
19
|
+
}, [items, pageIndex, pageSize]);
|
|
20
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMediaQuery } from '@terrafirmagreg-team/ui/hooks/use-media-query';
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
createElement,
|
|
6
|
+
useCallback,
|
|
7
|
+
useContext,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useSyncExternalStore,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
} from 'react';
|
|
13
|
+
import {
|
|
14
|
+
applyState,
|
|
15
|
+
clampWidth,
|
|
16
|
+
clearState,
|
|
17
|
+
getServerSnapshot,
|
|
18
|
+
readState,
|
|
19
|
+
subscribe,
|
|
20
|
+
writeState,
|
|
21
|
+
} from '@terrafirmagreg-team/ui/lib/readability/store';
|
|
22
|
+
import type {
|
|
23
|
+
ReadabilityLayout,
|
|
24
|
+
ReadabilityValue,
|
|
25
|
+
SpotlightStyle,
|
|
26
|
+
} from '@terrafirmagreg-team/ui/types/readability';
|
|
27
|
+
|
|
28
|
+
const ReadabilityContext = createContext<ReadabilityValue | null>(null);
|
|
29
|
+
|
|
30
|
+
export function useReadability(): ReadabilityValue {
|
|
31
|
+
const context = useContext(ReadabilityContext);
|
|
32
|
+
if (!context) throw new Error('Missing ReadabilityProvider');
|
|
33
|
+
return context;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ReadabilityProvider({ children }: { children: ReactNode }) {
|
|
37
|
+
const state = useSyncExternalStore(subscribe, readState, getServerSnapshot);
|
|
38
|
+
const coarsePointer = useMediaQuery('(pointer: coarse)') === true;
|
|
39
|
+
const smallScreen = useMediaQuery('(min-width: 768px)') !== true;
|
|
40
|
+
|
|
41
|
+
useLayoutEffect(() => {
|
|
42
|
+
applyState(state);
|
|
43
|
+
}, [state]);
|
|
44
|
+
useLayoutEffect(() => clearState, []);
|
|
45
|
+
|
|
46
|
+
const setLayout = useCallback((layout: ReadabilityLayout) => {
|
|
47
|
+
writeState({ layout });
|
|
48
|
+
}, []);
|
|
49
|
+
const setPageWidth = useCallback((pageWidth: number) => {
|
|
50
|
+
writeState({ pageWidth: clampWidth(pageWidth, 100) });
|
|
51
|
+
}, []);
|
|
52
|
+
const setContentWidth = useCallback((contentWidth: number) => {
|
|
53
|
+
writeState({ contentWidth: clampWidth(contentWidth, 100) });
|
|
54
|
+
}, []);
|
|
55
|
+
const setSpotlight = useCallback((spotlight: boolean) => {
|
|
56
|
+
writeState({ spotlight });
|
|
57
|
+
}, []);
|
|
58
|
+
const setSpotlightStyle = useCallback((spotlightStyle: SpotlightStyle) => {
|
|
59
|
+
writeState({ spotlightStyle });
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
const value = useMemo(
|
|
63
|
+
() => ({
|
|
64
|
+
...state,
|
|
65
|
+
coarsePointer,
|
|
66
|
+
smallScreen,
|
|
67
|
+
setLayout,
|
|
68
|
+
setPageWidth,
|
|
69
|
+
setContentWidth,
|
|
70
|
+
setSpotlight,
|
|
71
|
+
setSpotlightStyle,
|
|
72
|
+
}),
|
|
73
|
+
[
|
|
74
|
+
state,
|
|
75
|
+
coarsePointer,
|
|
76
|
+
smallScreen,
|
|
77
|
+
setLayout,
|
|
78
|
+
setPageWidth,
|
|
79
|
+
setContentWidth,
|
|
80
|
+
setSpotlight,
|
|
81
|
+
setSpotlightStyle,
|
|
82
|
+
],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return createElement(ReadabilityContext.Provider, { value }, children);
|
|
86
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { createContext, use } from 'react';
|
|
3
|
+
import type {
|
|
4
|
+
SearchItemType,
|
|
5
|
+
SearchResult,
|
|
6
|
+
} from '@terrafirmagreg-team/ui/types/search';
|
|
7
|
+
|
|
8
|
+
export const SearchDialogContext = createContext<{
|
|
9
|
+
open: boolean;
|
|
10
|
+
onOpenChange: (open: boolean) => void;
|
|
11
|
+
search: string;
|
|
12
|
+
onSearchChange: (v: string) => void;
|
|
13
|
+
onSelect: (item: SearchItemType) => void;
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
data?: SearchResult;
|
|
16
|
+
error?: Error;
|
|
17
|
+
|
|
18
|
+
query: string;
|
|
19
|
+
|
|
20
|
+
getActive: () => string | null;
|
|
21
|
+
setActive: (id: string | null) => void;
|
|
22
|
+
subscribeActive: (listener: () => void) => () => void;
|
|
23
|
+
} | null>(null);
|
|
24
|
+
|
|
25
|
+
export const SearchTagsListContext = createContext<{
|
|
26
|
+
value?: string;
|
|
27
|
+
onValueChange: (value: string | undefined) => void;
|
|
28
|
+
allowClear: boolean;
|
|
29
|
+
} | null>(null);
|
|
30
|
+
|
|
31
|
+
export function useSearch() {
|
|
32
|
+
const ctx = use(SearchDialogContext);
|
|
33
|
+
if (!ctx) throw new Error('Missing <SearchDialog />');
|
|
34
|
+
return ctx;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useSearchTagsList() {
|
|
38
|
+
const ctx = use(SearchTagsListContext);
|
|
39
|
+
if (!ctx) throw new Error('Missing <SearchTagsList />');
|
|
40
|
+
return ctx;
|
|
41
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Dialog } from '@base-ui/react/dialog';
|
|
3
|
+
import { createContext, use } from 'react';
|
|
4
|
+
|
|
5
|
+
export const dialogHandle = Dialog.createHandle();
|
|
6
|
+
|
|
7
|
+
export interface SearchContextType {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
open: boolean;
|
|
10
|
+
hotKey: { display: React.ReactNode; key: string | ((e: KeyboardEvent) => boolean) }[];
|
|
11
|
+
setOpenSearch: (value: boolean) => void;
|
|
12
|
+
dialogHandle: Dialog.Handle<unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const SearchContext = createContext<SearchContextType>({
|
|
16
|
+
enabled: false,
|
|
17
|
+
open: false,
|
|
18
|
+
hotKey: [],
|
|
19
|
+
setOpenSearch: () => undefined,
|
|
20
|
+
dialogHandle,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export function useSearchContext(): SearchContextType {
|
|
24
|
+
return use(SearchContext);
|
|
25
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
function buildRegexFromQuery(query: string): RegExp | null {
|
|
5
|
+
const source = query
|
|
6
|
+
.trim()
|
|
7
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
8
|
+
.replace(/\s+/g, '|');
|
|
9
|
+
return source ? new RegExp(source, 'gi') : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useHighlightQuery(query: string) {
|
|
13
|
+
return useCallback(
|
|
14
|
+
(element: Element | null) => {
|
|
15
|
+
const regex = buildRegexFromQuery(query);
|
|
16
|
+
if (!element || !regex || typeof Highlight === 'undefined')
|
|
17
|
+
return () => {};
|
|
18
|
+
let highlight = CSS.highlights.get('search-hit');
|
|
19
|
+
if (!highlight)
|
|
20
|
+
CSS.highlights.set('search-hit', (highlight = new Highlight()));
|
|
21
|
+
const walker = document.createTreeWalker(
|
|
22
|
+
element,
|
|
23
|
+
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
|
|
24
|
+
(node) => {
|
|
25
|
+
if (!(node instanceof Element)) return NodeFilter.FILTER_ACCEPT;
|
|
26
|
+
return node.hasAttribute('data-highlight-ignore')
|
|
27
|
+
? NodeFilter.FILTER_REJECT
|
|
28
|
+
: NodeFilter.FILTER_SKIP;
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
const ranges: StaticRange[] = [];
|
|
32
|
+
while (walker.nextNode()) {
|
|
33
|
+
const node = walker.currentNode as Text;
|
|
34
|
+
for (const match of node.data.matchAll(regex)) {
|
|
35
|
+
const range = new StaticRange({
|
|
36
|
+
startContainer: node,
|
|
37
|
+
startOffset: match.index,
|
|
38
|
+
endContainer: node,
|
|
39
|
+
endOffset: match.index + match[0].length,
|
|
40
|
+
});
|
|
41
|
+
highlight.add(range);
|
|
42
|
+
ranges.push(range);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return () => {
|
|
46
|
+
for (const range of ranges) highlight.delete(range);
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
[query],
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, type DependencyList } from 'react';
|
|
4
|
+
|
|
5
|
+
type AsyncEffectBody = (
|
|
6
|
+
isCancelled: () => boolean,
|
|
7
|
+
) => void | Promise<unknown> | (() => void);
|
|
8
|
+
|
|
9
|
+
export function useAsyncEffect(
|
|
10
|
+
effect: AsyncEffectBody | false | null | undefined,
|
|
11
|
+
deps: DependencyList,
|
|
12
|
+
): void {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!effect) return;
|
|
15
|
+
let cancelled = false;
|
|
16
|
+
const cleanup = effect(() => cancelled);
|
|
17
|
+
return () => {
|
|
18
|
+
cancelled = true;
|
|
19
|
+
if (typeof cleanup === 'function') cleanup();
|
|
20
|
+
};
|
|
21
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- deps owned by caller
|
|
22
|
+
}, deps);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useAsyncValue<T>(
|
|
26
|
+
factory: (() => Promise<T>) | false | null | undefined,
|
|
27
|
+
deps: DependencyList,
|
|
28
|
+
): T | undefined {
|
|
29
|
+
return useAsyncResult(factory, deps).value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useAsyncResult<T>(
|
|
33
|
+
factory: (() => Promise<T>) | false | null | undefined,
|
|
34
|
+
deps: DependencyList,
|
|
35
|
+
): { value: T | undefined; pending: boolean } {
|
|
36
|
+
const [entry, setEntry] = useState<{
|
|
37
|
+
deps: DependencyList;
|
|
38
|
+
value: T | undefined;
|
|
39
|
+
}>();
|
|
40
|
+
useAsyncEffect(async (isCancelled) => {
|
|
41
|
+
if (!factory) return;
|
|
42
|
+
try {
|
|
43
|
+
const next = await factory();
|
|
44
|
+
if (!isCancelled()) setEntry({ deps, value: next });
|
|
45
|
+
} catch {
|
|
46
|
+
if (!isCancelled()) setEntry({ deps, value: undefined });
|
|
47
|
+
}
|
|
48
|
+
}, deps);
|
|
49
|
+
const resolved =
|
|
50
|
+
entry !== undefined &&
|
|
51
|
+
entry.deps.length === deps.length &&
|
|
52
|
+
entry.deps.every((dep, i) => Object.is(dep, deps[i]));
|
|
53
|
+
return {
|
|
54
|
+
value: factory && resolved ? entry?.value : undefined,
|
|
55
|
+
pending: Boolean(factory) && !resolved,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
const IGNORE = '[role="menu"],[role="listbox"],[role="dialog"]';
|
|
6
|
+
|
|
7
|
+
export function useCloseOnScroll(open: boolean, close: () => void): void {
|
|
8
|
+
const closeRef = useRef(close);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
closeRef.current = close;
|
|
11
|
+
}, [close]);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!open) return;
|
|
15
|
+
const onScroll = (event: Event) => {
|
|
16
|
+
if (event.target instanceof Element && event.target.closest(IGNORE))
|
|
17
|
+
return;
|
|
18
|
+
closeRef.current();
|
|
19
|
+
};
|
|
20
|
+
window.addEventListener('scroll', onScroll, true);
|
|
21
|
+
return () => window.removeEventListener('scroll', onScroll, true);
|
|
22
|
+
}, [open]);
|
|
23
|
+
}
|