@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.
Files changed (57) hide show
  1. package/README.md +60 -10
  2. package/client/CodeBlock.tsx +24 -0
  3. package/client/DocView.tsx +9 -2
  4. package/client/ListingView.tsx +14 -10
  5. package/client/MarkdownTable.tsx +190 -0
  6. package/client/MdSection.tsx +56 -21
  7. package/client/MdSectionEditor.tsx +149 -2
  8. package/client/Mermaid.tsx +33 -7
  9. package/client/Settings.css +17 -0
  10. package/client/Settings.tsx +132 -0
  11. package/client/Table.tsx +2 -14
  12. package/client/app.css +339 -1
  13. package/client/block-state/README.md +39 -0
  14. package/client/block-state/storage.ts +73 -0
  15. package/client/block-state/useBlockState.ts +120 -0
  16. package/client/builtins/DataTable.tsx +269 -0
  17. package/client/builtins/chart-data.ts +22 -10
  18. package/client/builtins/index.ts +10 -0
  19. package/client/data-table/ColumnMenu.tsx +168 -0
  20. package/client/data-table/ColumnOptions.tsx +141 -0
  21. package/client/data-table/TableCell.tsx +76 -0
  22. package/client/data-table/TableHeaderCell.tsx +94 -0
  23. package/client/data-table/model.ts +194 -0
  24. package/client/data-table/navigation.ts +34 -0
  25. package/client/data-table/table-state.ts +196 -0
  26. package/client/data-table/useColumnMenu.ts +84 -0
  27. package/client/data-table/useTableState.ts +36 -0
  28. package/client/design/base/prose.css +4 -0
  29. package/client/diagrams/Dialog.tsx +577 -0
  30. package/client/diagrams/EditDialog.tsx +67 -0
  31. package/client/diagrams/Preferences.tsx +213 -0
  32. package/client/diagrams/edit-block.ts +93 -0
  33. package/client/diagrams/edit-context.ts +4 -0
  34. package/client/diagrams/session-id.ts +8 -0
  35. package/client/diagrams/slash.ts +94 -0
  36. package/client/diagrams/source.ts +73 -0
  37. package/client/diagrams/style.css +599 -0
  38. package/client/entry.tsx +2 -0
  39. package/client/mdx-components-base.ts +2 -2
  40. package/client/router.ts +8 -1
  41. package/client/shell/AppShell.tsx +38 -15
  42. package/client/shell/Sidebar.tsx +20 -2
  43. package/client/shell/StandaloneShell.tsx +19 -6
  44. package/client/shell/TocRail.tsx +4 -3
  45. package/client/shell/TopBar.tsx +17 -5
  46. package/client/state.ts +14 -0
  47. package/client/ui/ConfirmDeleteDialog.tsx +48 -91
  48. package/client/ui/ExpandModal.tsx +63 -104
  49. package/client/ui/Modal.tsx +123 -0
  50. package/client/ui/ResizeHandle.tsx +1 -1
  51. package/client/ui/SearchDialog.tsx +81 -88
  52. package/client/ui/modal.css +42 -0
  53. package/dist/cli.js +2191 -778
  54. package/dist/diagram-worker.js +17 -0
  55. package/dist/registry.json +297 -0
  56. package/package.json +42 -2
  57. package/skills/mdxserve/SKILL.md +56 -1
@@ -1,14 +1,15 @@
1
1
  import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
2
- import { createPortal } from "react-dom";
3
- import { AnimatePresence, motion, useSpring } from "framer-motion";
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, VARIANTS } from "../motion";
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<HTMLDivElement>(null);
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, onClose]);
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 (!open) resetSize();
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
- // The SSR entry renders the same component tree; never touch `document` at
264
- // render time there (the modal is only ever opened client-side anyway).
265
- if (typeof document === "undefined") return null;
266
-
267
- // Portal to <body>: callers sit inside the route wrapper, whose fadeRise
268
- // transform would otherwise turn `fixed` into "fixed to the column" and let
269
- // the scrim miss the top bar and sidebar.
270
- return createPortal(
271
- <AnimatePresence>
272
- {open ? (
273
- <motion.div
274
- key="scrim"
275
- onClick={onClose}
276
- {...VARIANTS.scrim}
277
- className="fixed inset-0 z-[var(--z-modal)] flex items-center justify-center bg-[var(--scrim)] p-4 backdrop-blur-sm sm:p-8"
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
- <motion.div
280
- ref={panelRef}
281
- onClick={(event) => event.stopPropagation()}
282
- {...VARIANTS.pop}
283
- role="dialog"
284
- aria-modal="true"
285
- aria-label={`Expanded ${title.toLowerCase()}`}
286
- // Once an edge has been dragged the panel takes an explicit size and stays
287
- // centred by the scrim's flex box; until then it fills the padded area
288
- // (capped for very wide windows).
289
- style={
290
- custom
291
- ? { width: widthSpring, height: heightSpring, maxWidth: "none", flex: "none" }
292
- : undefined
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 { AnimatePresence, motion, type MotionProps } from "framer-motion";
9
+ import { motion, type MotionProps } from "framer-motion";
9
10
  import { Icon } from "./Icon";
10
11
  import { Kbd } from "./Kbd";
11
- import { TRANSITIONS, VARIANTS } from "../motion";
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<HTMLDivElement>,
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<HTMLDivElement>) {
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
- <AnimatePresence>
103
- {open ? (
104
- <motion.div
105
- key="scrim"
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
- {...VARIANTS.scrim}
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
- <motion.div
111
- onClick={(event) => event.stopPropagation()}
112
- onKeyDown={handleKeyDown}
113
- {...VARIANTS.pop}
114
- className={
115
- "w-[560px] max-w-[92vw] self-start overflow-hidden rounded-xl border border-border-default bg-surface-raised shadow-lg" +
116
- (className ? " " + className : "")
117
- }
118
- {...rest}
119
- >
120
- <div className="flex h-12 items-center gap-3 border-b border-border-subtle px-4">
121
- <Icon name="search" size="md" className="text-text-subtle" />
122
- <input
123
- autoFocus
124
- data-bare-focus
125
- value={query}
126
- onChange={(event) => onQueryChange?.(event.target.value)}
127
- placeholder="Search docs"
128
- className="flex-1 border-0 bg-transparent text-[18px] font-normal text-text-body outline-none placeholder:text-text-subtle"
129
- />
130
- <Kbd>esc</Kbd>
131
- </div>
132
- <motion.div
133
- layout
134
- transition={TRANSITIONS.glide}
135
- className="max-h-80 overflow-y-auto p-2"
136
- >
137
- {results.length === 0 ? (
138
- <div className="p-6 text-center text-[13px] leading-normal font-medium text-text-subtle">
139
- No matches
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
- results.map((result, index) => {
143
- const active = index === highlighted;
144
- return (
145
- <motion.div
146
- key={(result.path ?? "") + index}
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
+ }