@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.
- package/package.json +93 -3
- package/src/components/blocks/button/icon-button.tsx +49 -0
- package/src/components/blocks/canvas/chevron-edge.tsx +242 -0
- package/src/components/blocks/canvas/flow-canvas.tsx +181 -0
- package/src/components/blocks/canvas/flow-toolbar.tsx +33 -0
- package/src/components/blocks/canvas/flow-viewport.tsx +244 -0
- package/src/components/blocks/fit-box.tsx +26 -0
- package/src/components/blocks/git/github-info.tsx +78 -0
- package/src/components/blocks/lang/content.tsx +52 -0
- package/src/components/blocks/lang/flag.tsx +25 -0
- package/src/components/blocks/lang/link.tsx +23 -0
- package/src/components/blocks/lang/redirect.tsx +48 -0
- package/src/components/blocks/lang/switcher.tsx +54 -0
- package/src/components/blocks/lang/untranslated-callout.tsx +29 -0
- package/src/components/blocks/lang/untranslated-chip.tsx +53 -0
- package/src/components/blocks/link/link-preview.tsx +257 -0
- package/src/components/blocks/list/row.tsx +92 -0
- package/src/components/blocks/media/glb/model.tsx +244 -0
- package/src/components/blocks/media/glb/scene.tsx +373 -0
- package/src/components/blocks/media/glb/viewer.tsx +52 -0
- package/src/components/blocks/media/image.tsx +136 -0
- package/src/components/blocks/media/slideshow.tsx +178 -0
- package/src/components/blocks/nav/breadcrumb.tsx +51 -0
- package/src/components/blocks/nav/copy-menu.tsx +37 -0
- package/src/components/blocks/nav/link.tsx +86 -0
- package/src/components/blocks/nav/site-menu.tsx +78 -0
- package/src/components/blocks/nav/site-title.tsx +61 -0
- package/src/components/blocks/nav/switcher.tsx +206 -0
- package/src/components/blocks/page/empty-state.tsx +32 -0
- package/src/components/blocks/page/heading.tsx +54 -0
- package/src/components/blocks/page/pager.tsx +220 -0
- package/src/components/blocks/readability/fields.tsx +250 -0
- package/src/components/blocks/readability/menu.tsx +247 -0
- package/src/components/blocks/readability/track.tsx +165 -0
- package/src/components/blocks/search/command.tsx +80 -0
- package/src/components/blocks/search/dialog.tsx +724 -0
- package/src/components/blocks/search/provider.tsx +118 -0
- package/src/components/blocks/sidebar.tsx +406 -0
- package/src/components/blocks/sprite.tsx +231 -0
- package/src/components/blocks/text/format-text.tsx +110 -0
- package/src/components/blocks/theme/toggle.tsx +75 -0
- package/src/components/blocks/tooltip/hint.tsx +28 -0
- package/src/components/blocks/tooltip/lazy.tsx +41 -0
- package/src/components/blocks/viewer/controls.tsx +284 -0
- package/src/components/blocks/viewer/float-bar.tsx +51 -0
- package/src/components/layouts/layout.tsx +94 -0
- package/src/components/layouts/left-sidebar/content.tsx +63 -0
- package/src/components/layouts/left-sidebar/context.tsx +55 -0
- package/src/components/layouts/left-sidebar/expand.tsx +23 -0
- package/src/components/layouts/left-sidebar/footer.tsx +111 -0
- package/src/components/layouts/left-sidebar/index.ts +29 -0
- package/src/components/layouts/left-sidebar/items.tsx +103 -0
- package/src/components/layouts/left-sidebar/search.tsx +75 -0
- package/src/components/layouts/left-sidebar/sidebar.tsx +189 -0
- package/src/components/layouts/left-sidebar/tree.tsx +157 -0
- package/src/components/layouts/right-sidebar.tsx +96 -0
- package/src/components/layouts/tree-context.tsx +42 -0
- package/src/components/mdx/accordion.tsx +185 -0
- package/src/components/mdx/banner.tsx +157 -0
- package/src/components/mdx/callout.tsx +97 -0
- package/src/components/mdx/card.tsx +56 -0
- package/src/components/mdx/codeblock.tsx +250 -0
- package/src/components/mdx/collapsible.tsx +33 -0
- package/src/components/mdx/files.tsx +63 -0
- package/src/components/mdx/glow-header.tsx +54 -0
- package/src/components/mdx/gradient-text.tsx +28 -0
- package/src/components/mdx/icon.tsx +35 -0
- package/src/components/mdx/image-zoom.tsx +315 -0
- package/src/components/mdx/math.tsx +30 -0
- package/src/components/mdx/mermaid.tsx +23 -0
- package/src/components/mdx/multiblock.tsx +19 -0
- package/src/components/mdx/preview.tsx +101 -0
- package/src/components/mdx/steps.tsx +10 -0
- package/src/components/mdx/table.tsx +14 -0
- package/src/components/mdx/tabs.tsx +354 -0
- package/src/components/mdx/type-table.tsx +171 -0
- package/src/components/provider.tsx +139 -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 +19 -0
- package/src/hooks/canvas/use-flow-viewport-persist.ts +34 -0
- package/src/hooks/page/use-fit-pages.ts +92 -0
- package/src/hooks/page/use-location-search.ts +31 -0
- package/src/hooks/page/use-page-preview.ts +41 -0
- package/src/hooks/page/use-paged-list.ts +21 -0
- package/src/hooks/readability/use-readability.ts +87 -0
- package/src/hooks/search/use-dialog.ts +42 -0
- package/src/hooks/search/use-highlight-query.ts +58 -0
- package/src/hooks/search/use-provider.ts +26 -0
- package/src/hooks/use-async-value.ts +60 -0
- package/src/hooks/use-close-on-scroll.ts +24 -0
- package/src/hooks/use-copy-button.ts +47 -0
- package/src/hooks/use-cycle-index.ts +76 -0
- package/src/hooks/use-document-title.ts +10 -0
- package/src/hooks/use-element-size.ts +36 -0
- package/src/hooks/use-external-meta.ts +133 -0
- package/src/hooks/use-hot-key.ts +34 -0
- package/src/hooks/use-hover-follow.ts +55 -0
- package/src/hooks/use-in-view.ts +31 -0
- package/src/hooks/use-lang.ts +9 -0
- package/src/hooks/use-last-value.ts +16 -0
- package/src/hooks/use-layout.ts +19 -0
- package/src/hooks/use-marquee.ts +52 -0
- package/src/hooks/use-media-query.ts +15 -0
- package/src/hooks/use-mobile.ts +20 -0
- package/src/hooks/use-modifier-keys.ts +32 -0
- package/src/hooks/use-on-change.ts +36 -0
- package/src/hooks/use-pane-keys.ts +29 -0
- package/src/hooks/use-sidebar.ts +64 -0
- package/src/hooks/use-swipe.ts +27 -0
- package/src/hooks/use-tree.ts +100 -0
- package/src/hooks/viewer/use-in-float-bar.ts +7 -0
- package/src/hooks/viewer/use-viewer-prefs.ts +78 -0
- package/src/lib/canvas/edge.ts +202 -0
- package/src/lib/canvas/geometry.ts +54 -0
- package/src/lib/canvas/state.ts +48 -0
- package/src/lib/canvas/viewport.ts +38 -0
- package/src/lib/canvas/xyflow.ts +2 -0
- package/src/lib/config.ts +106 -0
- package/src/lib/mdx/callout.ts +21 -0
- package/src/lib/mdx/gradient.ts +16 -0
- package/src/lib/mdx/mermaid.ts +15 -0
- package/src/lib/media/crop-opaque.ts +77 -0
- package/src/lib/media/nearby-indexes.ts +7 -0
- package/src/lib/media/trim-transparent.ts +118 -0
- package/src/lib/readability/config.ts +11 -0
- package/src/lib/readability/spotlight.ts +187 -0
- package/src/lib/readability/store.ts +147 -0
- package/src/lib/tree.ts +90 -0
- package/src/lib/utils/cn.ts +2 -0
- package/src/lib/utils/format.ts +22 -0
- package/src/lib/utils/highlight-style.ts +10 -0
- package/src/lib/utils/history.ts +10 -0
- package/src/lib/utils/merge-refs.ts +14 -0
- package/src/lib/utils/nav-active.ts +59 -0
- package/src/lib/utils/stored-value.ts +137 -0
- package/src/lib/utils/urls.ts +18 -0
- package/src/locales/en_us.json +110 -0
- package/src/locales/pl_pl.json +33 -0
- package/src/locales/ru_ru.json +110 -0
- package/src/locales/uk_ua.json +33 -0
- package/src/locales/zh_cn.json +15 -0
- package/src/locales/zh_tw.json +15 -0
- package/src/locales.ts +6 -0
- package/src/types/canvas.ts +54 -0
- package/src/types/glb.ts +26 -0
- package/src/types/mdx-directives.ts +106 -0
- package/src/types/nav.ts +96 -0
- package/src/types/preview.ts +15 -0
- package/src/types/readability.ts +29 -0
- package/src/types/search.ts +57 -0
- package/src/types/tree.ts +19 -0
- package/src/types/viewer.ts +54 -0
- package/styles/content/base.css +198 -0
- package/styles/content/default-colors.css +51 -0
- package/styles/content/shiki.css +107 -0
- package/styles/content/typography.mjs +3637 -0
- package/styles/content.css +5 -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 +430 -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,19 @@
|
|
|
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
|
+
}
|
|
19
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
}
|
|
34
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
}
|
|
92
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo, useSyncExternalStore } from 'react';
|
|
4
|
+
|
|
5
|
+
let search = typeof window === 'undefined' ? '' : window.location.search.slice(1);
|
|
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
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useLocationSearchParams(): URLSearchParams {
|
|
28
|
+
const search = useLocationSearch();
|
|
29
|
+
return useMemo(() => new URLSearchParams(search), [search]);
|
|
30
|
+
}
|
|
31
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { uiConfig } from '@terrafirmagreg-team/ui/lib/config';
|
|
4
|
+
import { createAsyncCache } from '@terrafirmagreg-team/data/lib/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
|
+
}
|
|
41
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
}
|
|
21
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
}
|
|
87
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
}
|
|
42
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { injectHighlightStyle } from '@terrafirmagreg-team/ui/lib/utils/highlight-style';
|
|
4
|
+
|
|
5
|
+
function buildRegexFromQuery(query: string): RegExp | null {
|
|
6
|
+
const source = query
|
|
7
|
+
.trim()
|
|
8
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
9
|
+
.replace(/\s+/g, '|');
|
|
10
|
+
return source ? new RegExp(source, 'gi') : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useHighlightQuery(query: string) {
|
|
14
|
+
return useCallback(
|
|
15
|
+
(element: Element | null) => {
|
|
16
|
+
const regex = buildRegexFromQuery(query);
|
|
17
|
+
if (!element || !regex || typeof Highlight === 'undefined')
|
|
18
|
+
return () => {};
|
|
19
|
+
let highlight = CSS.highlights.get('search-hit');
|
|
20
|
+
if (!highlight) {
|
|
21
|
+
CSS.highlights.set('search-hit', (highlight = new Highlight()));
|
|
22
|
+
injectHighlightStyle(
|
|
23
|
+
'search-hit',
|
|
24
|
+
'color:var(--color-primary);text-decoration-line:underline',
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const walker = document.createTreeWalker(
|
|
28
|
+
element,
|
|
29
|
+
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
|
|
30
|
+
(node) => {
|
|
31
|
+
if (!(node instanceof Element)) return NodeFilter.FILTER_ACCEPT;
|
|
32
|
+
return node.hasAttribute('data-highlight-ignore')
|
|
33
|
+
? NodeFilter.FILTER_REJECT
|
|
34
|
+
: NodeFilter.FILTER_SKIP;
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
const ranges: StaticRange[] = [];
|
|
38
|
+
while (walker.nextNode()) {
|
|
39
|
+
const node = walker.currentNode as Text;
|
|
40
|
+
for (const match of node.data.matchAll(regex)) {
|
|
41
|
+
const range = new StaticRange({
|
|
42
|
+
startContainer: node,
|
|
43
|
+
startOffset: match.index,
|
|
44
|
+
endContainer: node,
|
|
45
|
+
endOffset: match.index + match[0].length,
|
|
46
|
+
});
|
|
47
|
+
highlight.add(range);
|
|
48
|
+
ranges.push(range);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return () => {
|
|
52
|
+
for (const range of ranges) highlight.delete(range);
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
[query],
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
}
|
|
26
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
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; error: unknown } {
|
|
36
|
+
const [entry, setEntry] = useState<{
|
|
37
|
+
deps: DependencyList;
|
|
38
|
+
value: T | undefined;
|
|
39
|
+
error?: unknown;
|
|
40
|
+
}>();
|
|
41
|
+
useAsyncEffect(async (isCancelled) => {
|
|
42
|
+
if (!factory) return;
|
|
43
|
+
try {
|
|
44
|
+
const next = await factory();
|
|
45
|
+
if (!isCancelled()) setEntry({ deps, value: next });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (!isCancelled()) setEntry({ deps, value: undefined, error });
|
|
48
|
+
}
|
|
49
|
+
}, deps);
|
|
50
|
+
const resolved =
|
|
51
|
+
entry !== undefined &&
|
|
52
|
+
entry.deps.length === deps.length &&
|
|
53
|
+
entry.deps.every((dep, i) => Object.is(dep, deps[i]));
|
|
54
|
+
return {
|
|
55
|
+
value: factory && resolved ? entry?.value : undefined,
|
|
56
|
+
pending: Boolean(factory) && !resolved,
|
|
57
|
+
error: factory && resolved ? entry?.error : undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
}
|
|
24
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
export function useCopyButton(onCopy: () => unknown): {
|
|
5
|
+
checked: boolean;
|
|
6
|
+
isLoading: boolean;
|
|
7
|
+
error: boolean;
|
|
8
|
+
onClick: () => void;
|
|
9
|
+
} {
|
|
10
|
+
const [isLoading, setLoading] = useState(false);
|
|
11
|
+
const [checked, setChecked] = useState(false);
|
|
12
|
+
const [error, setError] = useState(false);
|
|
13
|
+
const callbackRef = useRef(onCopy);
|
|
14
|
+
const timeoutRef = useRef<number | null>(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
callbackRef.current = onCopy;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const onClick = useCallback(() => {
|
|
21
|
+
if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
|
|
22
|
+
setChecked(false);
|
|
23
|
+
setError(false);
|
|
24
|
+
setLoading(true);
|
|
25
|
+
Promise.resolve()
|
|
26
|
+
.then(() => callbackRef.current())
|
|
27
|
+
.then(() => {
|
|
28
|
+
setChecked(true);
|
|
29
|
+
timeoutRef.current = window.setTimeout(() => setChecked(false), 1500);
|
|
30
|
+
})
|
|
31
|
+
.catch((err: unknown) => {
|
|
32
|
+
console.error('Failed to copy:', err);
|
|
33
|
+
setError(true);
|
|
34
|
+
timeoutRef.current = window.setTimeout(() => setError(false), 1500);
|
|
35
|
+
})
|
|
36
|
+
.finally(() => setLoading(false));
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
return () => {
|
|
41
|
+
if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
return { checked, isLoading, error, onClick };
|
|
46
|
+
}
|
|
47
|
+
|