@karimsa/mdxserve 0.2.0 → 0.4.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/README.md +60 -10
- package/client/CodeBlock.tsx +24 -0
- package/client/DocView.tsx +9 -2
- package/client/ListingView.tsx +14 -10
- package/client/MarkdownTable.tsx +190 -0
- package/client/MdSection.tsx +56 -21
- package/client/MdSectionEditor.tsx +149 -2
- package/client/Mermaid.tsx +33 -7
- package/client/Settings.css +17 -0
- package/client/Settings.tsx +132 -0
- package/client/Table.tsx +2 -14
- package/client/app.css +339 -1
- package/client/block-state/README.md +39 -0
- package/client/block-state/storage.ts +73 -0
- package/client/block-state/useBlockState.ts +120 -0
- package/client/builtins/DataTable.tsx +269 -0
- package/client/builtins/chart-data.ts +22 -10
- package/client/builtins/index.ts +10 -0
- package/client/data-table/ColumnMenu.tsx +168 -0
- package/client/data-table/ColumnOptions.tsx +141 -0
- package/client/data-table/TableCell.tsx +76 -0
- package/client/data-table/TableHeaderCell.tsx +94 -0
- package/client/data-table/model.ts +194 -0
- package/client/data-table/navigation.ts +34 -0
- package/client/data-table/table-state.ts +196 -0
- package/client/data-table/useColumnMenu.ts +84 -0
- package/client/data-table/useTableState.ts +36 -0
- package/client/design/base/prose.css +4 -0
- package/client/diagrams/Dialog.tsx +577 -0
- package/client/diagrams/EditDialog.tsx +67 -0
- package/client/diagrams/Preferences.tsx +213 -0
- package/client/diagrams/edit-block.ts +93 -0
- package/client/diagrams/edit-context.ts +4 -0
- package/client/diagrams/session-id.ts +8 -0
- package/client/diagrams/slash.ts +94 -0
- package/client/diagrams/source.ts +73 -0
- package/client/diagrams/style.css +599 -0
- package/client/entry.tsx +2 -0
- package/client/mdx-components-base.ts +2 -2
- package/client/router.ts +8 -1
- package/client/shell/AppShell.tsx +38 -15
- package/client/shell/Sidebar.tsx +20 -2
- package/client/shell/StandaloneShell.tsx +19 -6
- package/client/shell/TocRail.tsx +4 -3
- package/client/shell/TopBar.tsx +17 -5
- package/client/state.ts +14 -0
- package/client/ui/ConfirmDeleteDialog.tsx +48 -91
- package/client/ui/ExpandModal.tsx +63 -104
- package/client/ui/Modal.tsx +123 -0
- package/client/ui/ResizeHandle.tsx +1 -1
- package/client/ui/SearchDialog.tsx +81 -88
- package/client/ui/modal.css +42 -0
- package/dist/cli.js +2191 -778
- package/dist/diagram-worker.js +17 -0
- package/dist/registry.json +297 -0
- package/package.json +42 -2
- package/skills/mdxserve/SKILL.md +56 -1
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Modal } from "./Modal";
|
|
3
|
+
import { useSpring } from "framer-motion";
|
|
4
4
|
import { Icon } from "./Icon";
|
|
5
5
|
import { IconButton } from "./IconButton";
|
|
6
6
|
import { Kbd } from "./Kbd";
|
|
7
|
-
import { TRANSITIONS
|
|
7
|
+
import { TRANSITIONS } from "../motion";
|
|
8
8
|
|
|
9
9
|
export interface ExpandModalProps {
|
|
10
10
|
open: boolean;
|
|
11
11
|
onClose: () => void;
|
|
12
|
+
onExitComplete?: () => void;
|
|
12
13
|
/** Lucide icon shown before the title. */
|
|
13
14
|
icon: string;
|
|
14
15
|
title: string;
|
|
@@ -114,13 +115,14 @@ function resizeFromCentre(
|
|
|
114
115
|
export function ExpandModal({
|
|
115
116
|
open,
|
|
116
117
|
onClose,
|
|
118
|
+
onExitComplete,
|
|
117
119
|
icon,
|
|
118
120
|
title,
|
|
119
121
|
hint,
|
|
120
122
|
actions,
|
|
121
123
|
children,
|
|
122
124
|
}: ExpandModalProps) {
|
|
123
|
-
const panelRef = useRef<
|
|
125
|
+
const panelRef = useRef<HTMLDialogElement>(null);
|
|
124
126
|
// Whether the reader has taken over the size; the springs below hold it.
|
|
125
127
|
const [custom, setCustom] = useState(false);
|
|
126
128
|
const [dragging, setDragging] = useState<Edge | null>(null);
|
|
@@ -137,35 +139,14 @@ export function ExpandModal({
|
|
|
137
139
|
grab: Point;
|
|
138
140
|
} | null>(null);
|
|
139
141
|
|
|
140
|
-
// Document-level so Escape works wherever focus landed (a pan surface or
|
|
141
|
-
// chart is a plain element and never holds focus itself). A control in the
|
|
142
|
-
// header that already consumed the key (the layout menu closing itself)
|
|
143
|
-
// marks the event default-prevented, and that Escape must not also close
|
|
144
|
-
// the modal.
|
|
145
142
|
useEffect(() => {
|
|
146
143
|
if (!open) return;
|
|
147
|
-
function onKeyDown(event: KeyboardEvent) {
|
|
148
|
-
if (event.key === "Escape" && !event.defaultPrevented) {
|
|
149
|
-
event.preventDefault();
|
|
150
|
-
onClose();
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
document.addEventListener("keydown", onKeyDown);
|
|
154
|
-
// Wheel-zooming the content must not also scroll the document behind the scrim.
|
|
155
|
-
const previousOverflow = document.body.style.overflow;
|
|
156
|
-
document.body.style.overflow = "hidden";
|
|
157
144
|
return () => {
|
|
158
|
-
document.removeEventListener("keydown", onKeyDown);
|
|
159
|
-
document.body.style.overflow = previousOverflow;
|
|
160
|
-
// A drag interrupted by closing (Escape, scrim click, unmount) never sees
|
|
161
|
-
// pointerup, so release its global state here: otherwise text selection
|
|
162
|
-
// stays disabled page-wide and the next open treats a hover over a handle
|
|
163
|
-
// as an in-progress drag.
|
|
164
145
|
drag.current = null;
|
|
165
146
|
setDragging(null);
|
|
166
147
|
document.body.style.userSelect = "";
|
|
167
148
|
};
|
|
168
|
-
}, [open
|
|
149
|
+
}, [open]);
|
|
169
150
|
|
|
170
151
|
// A custom size lasts for one viewing: reopening starts from the default inset.
|
|
171
152
|
const resetSize = useCallback(() => {
|
|
@@ -174,7 +155,7 @@ export function ExpandModal({
|
|
|
174
155
|
}, []);
|
|
175
156
|
|
|
176
157
|
useEffect(() => {
|
|
177
|
-
if (
|
|
158
|
+
if (open) resetSize();
|
|
178
159
|
}, [open, resetSize]);
|
|
179
160
|
|
|
180
161
|
// Keep a custom size inside the window if the window shrinks underneath it.
|
|
@@ -260,83 +241,61 @@ export function ExpandModal({
|
|
|
260
241
|
document.body.style.userSelect = "";
|
|
261
242
|
}, []);
|
|
262
243
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
244
|
+
return (
|
|
245
|
+
<Modal
|
|
246
|
+
open={open}
|
|
247
|
+
onClose={onClose}
|
|
248
|
+
onExitComplete={onExitComplete}
|
|
249
|
+
panelRef={panelRef}
|
|
250
|
+
aria-label={`Expanded ${title.toLowerCase()}`}
|
|
251
|
+
// A dragged panel takes an explicit size; auto margins keep it centered.
|
|
252
|
+
style={
|
|
253
|
+
custom
|
|
254
|
+
? { width: widthSpring, height: heightSpring, maxWidth: "none", flex: "none" }
|
|
255
|
+
: undefined
|
|
256
|
+
}
|
|
257
|
+
className="flex h-[calc(100dvh-32px)] w-[min(1600px,calc(100vw-32px))] flex-col overflow-visible sm:h-[calc(100dvh-64px)] sm:w-[min(1600px,calc(100vw-64px))]"
|
|
258
|
+
>
|
|
259
|
+
<div className="flex shrink-0 items-center gap-2 rounded-t-xl border-b border-border-subtle px-3 py-2">
|
|
260
|
+
<Icon name={icon} size="md" className="text-text-muted" />
|
|
261
|
+
<span className="font-sans font-semibold text-text-heading text-[length:var(--size-sm)]">
|
|
262
|
+
{title}
|
|
263
|
+
</span>
|
|
264
|
+
<span className="hidden items-center gap-1.5 font-sans text-[length:var(--size-xs)] text-text-subtle sm:flex">
|
|
265
|
+
{hint ? <>{hint} · </> : null}
|
|
266
|
+
drag an edge to resize · <Kbd>esc</Kbd> to close
|
|
267
|
+
</span>
|
|
268
|
+
<div className="ml-auto flex items-center gap-2">
|
|
269
|
+
{actions}
|
|
270
|
+
<IconButton icon="x" label="Close" size="md" autoFocus onClick={onClose} />
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
<div className="min-h-0 flex-1 overflow-hidden rounded-b-xl">{children}</div>
|
|
274
|
+
{EDGE_HANDLES.map(({ edge, className, bar }) => (
|
|
275
|
+
<div
|
|
276
|
+
key={edge}
|
|
277
|
+
role="separator"
|
|
278
|
+
aria-label={`Resize ${edge} edge`}
|
|
279
|
+
className={"group absolute z-10 " + className}
|
|
280
|
+
onPointerDown={(event) => onHandleDown(edge, event)}
|
|
281
|
+
onPointerMove={onHandleMove}
|
|
282
|
+
onPointerUp={onHandleUp}
|
|
283
|
+
onPointerCancel={onHandleUp}
|
|
284
|
+
onDoubleClick={resetSize}
|
|
278
285
|
>
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
className="relative flex h-full w-full max-w-[1600px] flex-col rounded-xl border border-border-default bg-surface-raised shadow-lg"
|
|
295
|
-
>
|
|
296
|
-
<div className="flex shrink-0 items-center gap-2 rounded-t-xl border-b border-border-subtle px-3 py-2">
|
|
297
|
-
<Icon name={icon} size="md" className="text-text-muted" />
|
|
298
|
-
<span className="font-sans font-semibold text-text-heading text-[length:var(--size-sm)]">
|
|
299
|
-
{title}
|
|
300
|
-
</span>
|
|
301
|
-
<span className="hidden items-center gap-1.5 font-sans text-[length:var(--size-xs)] text-text-subtle sm:flex">
|
|
302
|
-
{hint ? <>{hint} · </> : null}
|
|
303
|
-
drag an edge to resize · <Kbd>esc</Kbd> to close
|
|
304
|
-
</span>
|
|
305
|
-
<div className="ml-auto flex items-center gap-2">
|
|
306
|
-
{actions}
|
|
307
|
-
<IconButton icon="x" label="Close" size="md" autoFocus onClick={onClose} />
|
|
308
|
-
</div>
|
|
309
|
-
</div>
|
|
310
|
-
<div className="min-h-0 flex-1 overflow-hidden rounded-b-xl">{children}</div>
|
|
311
|
-
{EDGE_HANDLES.map(({ edge, className, bar }) => (
|
|
312
|
-
<div
|
|
313
|
-
key={edge}
|
|
314
|
-
role="separator"
|
|
315
|
-
aria-label={`Resize ${edge} edge`}
|
|
316
|
-
className={"group absolute z-10 " + className}
|
|
317
|
-
onPointerDown={(event) => onHandleDown(edge, event)}
|
|
318
|
-
onPointerMove={onHandleMove}
|
|
319
|
-
onPointerUp={onHandleUp}
|
|
320
|
-
onPointerCancel={onHandleUp}
|
|
321
|
-
onDoubleClick={resetSize}
|
|
322
|
-
>
|
|
323
|
-
{bar ? (
|
|
324
|
-
<span
|
|
325
|
-
className={
|
|
326
|
-
"absolute rounded-full transition-opacity " +
|
|
327
|
-
bar +
|
|
328
|
-
(dragging === edge
|
|
329
|
-
? " bg-surface-accent opacity-100"
|
|
330
|
-
: " bg-border-default opacity-0 group-hover:opacity-100")
|
|
331
|
-
}
|
|
332
|
-
/>
|
|
333
|
-
) : null}
|
|
334
|
-
</div>
|
|
335
|
-
))}
|
|
336
|
-
</motion.div>
|
|
337
|
-
</motion.div>
|
|
338
|
-
) : null}
|
|
339
|
-
</AnimatePresence>,
|
|
340
|
-
document.body,
|
|
286
|
+
{bar ? (
|
|
287
|
+
<span
|
|
288
|
+
className={
|
|
289
|
+
"absolute rounded-full transition-opacity " +
|
|
290
|
+
bar +
|
|
291
|
+
(dragging === edge
|
|
292
|
+
? " bg-surface-accent opacity-100"
|
|
293
|
+
: " bg-border-default opacity-0 group-hover:opacity-100")
|
|
294
|
+
}
|
|
295
|
+
/>
|
|
296
|
+
) : null}
|
|
297
|
+
</div>
|
|
298
|
+
))}
|
|
299
|
+
</Modal>
|
|
341
300
|
);
|
|
342
301
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useEffect, useRef, type Ref } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import { AnimatePresence, motion, useIsPresent, type HTMLMotionProps } from "framer-motion";
|
|
4
|
+
import { VARIANTS } from "../motion";
|
|
5
|
+
|
|
6
|
+
type ModalProps = Omit<HTMLMotionProps<"dialog">, "onClose" | "onCancel" | "ref" | "open"> & {
|
|
7
|
+
open: boolean;
|
|
8
|
+
onClose?: () => void;
|
|
9
|
+
onExitComplete?: () => void;
|
|
10
|
+
/** Disable Escape and backdrop dismissal while an operation is pending. */
|
|
11
|
+
dismissible?: boolean;
|
|
12
|
+
dismissOnBackdrop?: boolean;
|
|
13
|
+
/** Override backdrop dismissal, for example to protect an unsaved draft. */
|
|
14
|
+
onBackdropClick?: () => void;
|
|
15
|
+
placement?: "top" | "center";
|
|
16
|
+
panelRef?: Ref<HTMLDialogElement>;
|
|
17
|
+
initialFocus?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// A parent and its nested modal can unmount in either order.
|
|
21
|
+
const scrollLocks = new WeakMap<Document, { count: number; overflow: string }>();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Native modality supplies focus containment, focus return and nested top-layer ordering.
|
|
25
|
+
* Toggle `open` to close, or wrap a conditionally mounted owner in AnimatePresence.
|
|
26
|
+
* Exit propagation keeps the native dialog open until its panel has finished fading.
|
|
27
|
+
*/
|
|
28
|
+
export function Modal({ open, onExitComplete, ...props }: ModalProps) {
|
|
29
|
+
if (typeof document === "undefined") return null;
|
|
30
|
+
return createPortal(
|
|
31
|
+
<AnimatePresence propagate onExitComplete={onExitComplete}>
|
|
32
|
+
{open && <ModalSurface key="modal" {...props} />}
|
|
33
|
+
</AnimatePresence>,
|
|
34
|
+
document.body,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ModalSurface({
|
|
39
|
+
onClose,
|
|
40
|
+
dismissible = true,
|
|
41
|
+
dismissOnBackdrop = true,
|
|
42
|
+
onBackdropClick,
|
|
43
|
+
placement = "center",
|
|
44
|
+
panelRef,
|
|
45
|
+
initialFocus,
|
|
46
|
+
className = "",
|
|
47
|
+
children,
|
|
48
|
+
onKeyDown,
|
|
49
|
+
...props
|
|
50
|
+
}: Omit<ModalProps, "open">) {
|
|
51
|
+
const present = useIsPresent();
|
|
52
|
+
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
53
|
+
const backdropPress = useRef(false);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const dialog = dialogRef.current!;
|
|
56
|
+
const previousFocus = document.activeElement;
|
|
57
|
+
const scrollLock = scrollLocks.get(document) ?? {
|
|
58
|
+
count: 0,
|
|
59
|
+
overflow: document.body.style.overflow,
|
|
60
|
+
};
|
|
61
|
+
scrollLock.count += 1;
|
|
62
|
+
scrollLocks.set(document, scrollLock);
|
|
63
|
+
dialog.showModal();
|
|
64
|
+
if (initialFocus) dialog.querySelector<HTMLElement>(initialFocus)?.focus();
|
|
65
|
+
document.body.style.overflow = "hidden";
|
|
66
|
+
return () => {
|
|
67
|
+
dialog.close();
|
|
68
|
+
scrollLock.count -= 1;
|
|
69
|
+
if (scrollLock.count === 0) {
|
|
70
|
+
document.body.style.overflow = scrollLock.overflow;
|
|
71
|
+
scrollLocks.delete(document);
|
|
72
|
+
}
|
|
73
|
+
if (previousFocus instanceof HTMLElement && previousFocus.isConnected) previousFocus.focus();
|
|
74
|
+
};
|
|
75
|
+
}, [initialFocus]);
|
|
76
|
+
|
|
77
|
+
function isBackdrop(event: React.MouseEvent<HTMLDialogElement>) {
|
|
78
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
79
|
+
return (
|
|
80
|
+
event.target === event.currentTarget &&
|
|
81
|
+
(event.clientX < bounds.left ||
|
|
82
|
+
event.clientX > bounds.right ||
|
|
83
|
+
event.clientY < bounds.top ||
|
|
84
|
+
event.clientY > bounds.bottom)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<motion.dialog
|
|
90
|
+
{...VARIANTS.pop}
|
|
91
|
+
{...props}
|
|
92
|
+
ref={(dialog) => {
|
|
93
|
+
dialogRef.current = dialog;
|
|
94
|
+
if (typeof panelRef === "function") return panelRef(dialog);
|
|
95
|
+
if (panelRef) panelRef.current = dialog;
|
|
96
|
+
}}
|
|
97
|
+
aria-modal="true"
|
|
98
|
+
data-exiting={!present || undefined}
|
|
99
|
+
className={`modal modal--${placement} ${className}`}
|
|
100
|
+
onCancel={(event) => {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
event.stopPropagation();
|
|
103
|
+
if (present && dismissible) onClose?.();
|
|
104
|
+
}}
|
|
105
|
+
onKeyDown={(event) => {
|
|
106
|
+
onKeyDown?.(event);
|
|
107
|
+
event.stopPropagation();
|
|
108
|
+
}}
|
|
109
|
+
onPointerDown={(event) => {
|
|
110
|
+
backdropPress.current = isBackdrop(event);
|
|
111
|
+
}}
|
|
112
|
+
onClick={(event) => {
|
|
113
|
+
event.stopPropagation();
|
|
114
|
+
const outside = backdropPress.current && isBackdrop(event);
|
|
115
|
+
backdropPress.current = false;
|
|
116
|
+
if (present && outside && dismissible && dismissOnBackdrop)
|
|
117
|
+
(onBackdropClick ?? onClose)?.();
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
{children}
|
|
121
|
+
</motion.dialog>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -178,7 +178,7 @@ export function ResizeHandle({
|
|
|
178
178
|
onPointerCancel={onPointerUp}
|
|
179
179
|
style={{ width: HANDLE_ZONE, ...zoneStyle }}
|
|
180
180
|
className={
|
|
181
|
-
"absolute inset-y-0 z-10 flex cursor-col-resize " +
|
|
181
|
+
"absolute inset-y-0 z-10 hidden md:flex cursor-col-resize " +
|
|
182
182
|
(anchored
|
|
183
183
|
? "items-center " + (side === "right" ? "justify-center" : "justify-end")
|
|
184
184
|
: // A column so the sticky pill can slide along the zone's full height.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Modal } from "./Modal";
|
|
1
2
|
import {
|
|
2
3
|
useEffect,
|
|
3
4
|
useState,
|
|
@@ -5,10 +6,10 @@ import {
|
|
|
5
6
|
type KeyboardEvent,
|
|
6
7
|
type ReactNode,
|
|
7
8
|
} from "react";
|
|
8
|
-
import {
|
|
9
|
+
import { motion, type MotionProps } from "framer-motion";
|
|
9
10
|
import { Icon } from "./Icon";
|
|
10
11
|
import { Kbd } from "./Kbd";
|
|
11
|
-
import { TRANSITIONS
|
|
12
|
+
import { TRANSITIONS } from "../motion";
|
|
12
13
|
|
|
13
14
|
export interface SearchResult {
|
|
14
15
|
title: ReactNode;
|
|
@@ -49,7 +50,7 @@ function highlight(text: ReactNode, terms: string[] | undefined): ReactNode {
|
|
|
49
50
|
// React still types on `HTMLAttributes`); `Omit` it, along with the
|
|
50
51
|
// motion.div-owned event props, before extending.
|
|
51
52
|
export interface SearchDialogProps extends Omit<
|
|
52
|
-
HTMLAttributes<
|
|
53
|
+
HTMLAttributes<HTMLDialogElement>,
|
|
53
54
|
"results" | "onSelect" | keyof MotionProps
|
|
54
55
|
> {
|
|
55
56
|
open?: boolean;
|
|
@@ -79,7 +80,7 @@ export function SearchDialog({
|
|
|
79
80
|
setHighlighted(0);
|
|
80
81
|
}, [query, results, open]);
|
|
81
82
|
|
|
82
|
-
function handleKeyDown(event: KeyboardEvent<
|
|
83
|
+
function handleKeyDown(event: KeyboardEvent<HTMLDialogElement>) {
|
|
83
84
|
if (event.key === "ArrowDown") {
|
|
84
85
|
event.preventDefault();
|
|
85
86
|
setHighlighted((current) => Math.min(current + 1, Math.max(results.length - 1, 0)));
|
|
@@ -92,96 +93,88 @@ export function SearchDialog({
|
|
|
92
93
|
event.preventDefault();
|
|
93
94
|
onSelect?.(result);
|
|
94
95
|
}
|
|
95
|
-
} else if (event.key === "Escape") {
|
|
96
|
-
event.preventDefault();
|
|
97
|
-
onClose?.();
|
|
98
96
|
}
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
return (
|
|
102
|
-
<
|
|
103
|
-
{open
|
|
104
|
-
|
|
105
|
-
|
|
100
|
+
<Modal
|
|
101
|
+
open={open}
|
|
102
|
+
onClose={onClose}
|
|
103
|
+
placement="top"
|
|
104
|
+
aria-label="Search docs"
|
|
105
|
+
className={"w-[560px] max-w-[calc(100vw-2rem)] overflow-hidden " + (className ?? "")}
|
|
106
|
+
onKeyDown={handleKeyDown}
|
|
107
|
+
{...rest}
|
|
108
|
+
>
|
|
109
|
+
<div className="flex h-12 items-center gap-3 border-b border-border-subtle px-4">
|
|
110
|
+
<Icon name="search" size="md" className="text-text-subtle" />
|
|
111
|
+
<input
|
|
112
|
+
autoFocus
|
|
113
|
+
data-bare-focus
|
|
114
|
+
value={query}
|
|
115
|
+
onChange={(event) => onQueryChange?.(event.target.value)}
|
|
116
|
+
placeholder="Search docs"
|
|
117
|
+
aria-label="Search docs"
|
|
118
|
+
className="min-w-0 flex-1 border-0 bg-transparent text-[18px] font-normal text-text-body outline-none placeholder:text-text-subtle"
|
|
119
|
+
/>
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
aria-label="Close search"
|
|
106
123
|
onClick={onClose}
|
|
107
|
-
|
|
108
|
-
className="fixed inset-0 z-[var(--z-modal)] flex justify-center bg-[var(--scrim)] pt-[10vh] backdrop-blur-sm"
|
|
124
|
+
className="flex h-11 w-11 shrink-0 cursor-pointer items-center justify-center rounded-md text-text-subtle md:hidden"
|
|
109
125
|
>
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
126
|
+
<Icon name="x" size="sm" />
|
|
127
|
+
</button>
|
|
128
|
+
<span className="hidden md:contents">
|
|
129
|
+
<Kbd>esc</Kbd>
|
|
130
|
+
</span>
|
|
131
|
+
</div>
|
|
132
|
+
<motion.div layout transition={TRANSITIONS.glide} className="max-h-80 overflow-y-auto p-2">
|
|
133
|
+
{results.length === 0 ? (
|
|
134
|
+
<div className="p-6 text-center text-[13px] leading-normal font-medium text-text-subtle">
|
|
135
|
+
No matches
|
|
136
|
+
</div>
|
|
137
|
+
) : (
|
|
138
|
+
results.map((result, index) => {
|
|
139
|
+
const active = index === highlighted;
|
|
140
|
+
return (
|
|
141
|
+
<motion.div
|
|
142
|
+
key={(result.path ?? "") + index}
|
|
143
|
+
layout
|
|
144
|
+
initial={{ opacity: 0, y: 4 }}
|
|
145
|
+
animate={{ opacity: 1, y: 0 }}
|
|
146
|
+
transition={{
|
|
147
|
+
...TRANSITIONS.glide,
|
|
148
|
+
delay: Math.min(index, STAGGER_CAP - 1) * 0.02,
|
|
149
|
+
}}
|
|
150
|
+
onMouseEnter={() => setHighlighted(index)}
|
|
151
|
+
onClick={() => onSelect?.(result)}
|
|
152
|
+
className={
|
|
153
|
+
"flex cursor-pointer items-start gap-3 rounded-md p-2.5 " +
|
|
154
|
+
(active ? "bg-surface-hover" : "bg-transparent")
|
|
155
|
+
}
|
|
156
|
+
>
|
|
157
|
+
<Icon name="file-text" size="sm" className="mt-0.5 text-text-subtle" />
|
|
158
|
+
<div className="min-w-0 flex-1">
|
|
159
|
+
{result.label ? (
|
|
160
|
+
<div className="overflow-hidden text-ellipsis whitespace-nowrap font-mono text-[length:var(--size-2xs)] text-text-subtle">
|
|
161
|
+
{highlight(result.label, result.terms)}
|
|
162
|
+
</div>
|
|
163
|
+
) : null}
|
|
164
|
+
<div className="text-[13px] leading-normal font-semibold text-text-heading">
|
|
165
|
+
{highlight(result.title, result.terms)}
|
|
166
|
+
</div>
|
|
167
|
+
{result.excerpt ? (
|
|
168
|
+
<div className="mt-px overflow-hidden text-ellipsis whitespace-nowrap text-[12px] font-normal text-text-muted">
|
|
169
|
+
{highlight(result.excerpt, result.terms)}
|
|
170
|
+
</div>
|
|
171
|
+
) : null}
|
|
140
172
|
</div>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
layout
|
|
148
|
-
initial={{ opacity: 0, y: 4 }}
|
|
149
|
-
animate={{ opacity: 1, y: 0 }}
|
|
150
|
-
transition={{
|
|
151
|
-
...TRANSITIONS.glide,
|
|
152
|
-
delay: Math.min(index, STAGGER_CAP - 1) * 0.02,
|
|
153
|
-
}}
|
|
154
|
-
onMouseEnter={() => setHighlighted(index)}
|
|
155
|
-
onClick={() => onSelect?.(result)}
|
|
156
|
-
className={
|
|
157
|
-
"flex cursor-pointer items-start gap-3 rounded-md p-2.5 " +
|
|
158
|
-
(active ? "bg-surface-hover" : "bg-transparent")
|
|
159
|
-
}
|
|
160
|
-
>
|
|
161
|
-
<Icon name="file-text" size="sm" className="mt-0.5 text-text-subtle" />
|
|
162
|
-
<div className="min-w-0 flex-1">
|
|
163
|
-
{result.label ? (
|
|
164
|
-
<div className="overflow-hidden text-ellipsis whitespace-nowrap font-mono text-[length:var(--size-2xs)] text-text-subtle">
|
|
165
|
-
{highlight(result.label, result.terms)}
|
|
166
|
-
</div>
|
|
167
|
-
) : null}
|
|
168
|
-
<div className="text-[13px] leading-normal font-semibold text-text-heading">
|
|
169
|
-
{highlight(result.title, result.terms)}
|
|
170
|
-
</div>
|
|
171
|
-
{result.excerpt ? (
|
|
172
|
-
<div className="mt-px overflow-hidden text-ellipsis whitespace-nowrap text-[12px] font-normal text-text-muted">
|
|
173
|
-
{highlight(result.excerpt, result.terms)}
|
|
174
|
-
</div>
|
|
175
|
-
) : null}
|
|
176
|
-
</div>
|
|
177
|
-
</motion.div>
|
|
178
|
-
);
|
|
179
|
-
})
|
|
180
|
-
)}
|
|
181
|
-
</motion.div>
|
|
182
|
-
</motion.div>
|
|
183
|
-
</motion.div>
|
|
184
|
-
) : null}
|
|
185
|
-
</AnimatePresence>
|
|
173
|
+
</motion.div>
|
|
174
|
+
);
|
|
175
|
+
})
|
|
176
|
+
)}
|
|
177
|
+
</motion.div>
|
|
178
|
+
</Modal>
|
|
186
179
|
);
|
|
187
180
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
.modal {
|
|
2
|
+
position: fixed;
|
|
3
|
+
inset: 0;
|
|
4
|
+
margin: auto;
|
|
5
|
+
padding: 0;
|
|
6
|
+
max-width: calc(100vw - 32px);
|
|
7
|
+
max-height: calc(100dvh - 32px);
|
|
8
|
+
border: 1px solid var(--border-default);
|
|
9
|
+
border-radius: var(--radius-xl, 12px);
|
|
10
|
+
background: var(--surface-raised);
|
|
11
|
+
color: var(--text-body);
|
|
12
|
+
box-shadow: 0 28px 100px #0006;
|
|
13
|
+
}
|
|
14
|
+
.modal::backdrop {
|
|
15
|
+
background: var(--scrim);
|
|
16
|
+
backdrop-filter: blur(4px);
|
|
17
|
+
animation: modal-backdrop-in var(--dur-fast) var(--ease-standard) both;
|
|
18
|
+
}
|
|
19
|
+
.modal--top {
|
|
20
|
+
margin-top: 10vh;
|
|
21
|
+
max-height: 80dvh;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.modal[data-exiting]::backdrop {
|
|
25
|
+
animation-name: modal-backdrop-out;
|
|
26
|
+
}
|
|
27
|
+
@keyframes modal-backdrop-in {
|
|
28
|
+
from {
|
|
29
|
+
opacity: 0;
|
|
30
|
+
}
|
|
31
|
+
to {
|
|
32
|
+
opacity: 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
@keyframes modal-backdrop-out {
|
|
36
|
+
from {
|
|
37
|
+
opacity: 1;
|
|
38
|
+
}
|
|
39
|
+
to {
|
|
40
|
+
opacity: 0;
|
|
41
|
+
}
|
|
42
|
+
}
|