@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,3 +1,4 @@
|
|
|
1
|
+
import { DiagramPreferences } from "../diagrams/Preferences";
|
|
1
2
|
import { AnimatePresence, motion, useSpring } from "framer-motion";
|
|
2
3
|
import { useAtomValue } from "jotai";
|
|
3
4
|
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|
@@ -30,17 +31,26 @@ import { TocRail } from "./TocRail";
|
|
|
30
31
|
import { TopBar } from "./TopBar";
|
|
31
32
|
import { useContentWidth, useDocMaxWidth } from "./use-doc-width";
|
|
32
33
|
import { DESKTOP_MEDIA } from "../platform";
|
|
33
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
contentLayoutAtom,
|
|
36
|
+
docWidthAtom,
|
|
37
|
+
LISTING_MAX_WIDTH,
|
|
38
|
+
LISTING_MIN_WIDTH,
|
|
39
|
+
listingWidthAtom,
|
|
40
|
+
} from "../state";
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
function sidebarStorageKey(): string {
|
|
43
|
+
// Keep hosted navigation preferences separate from local reader defaults.
|
|
44
|
+
return shellInfo.immutableSite ? "mdxserve-site-sidebar" : "mdxserve-sidebar";
|
|
45
|
+
}
|
|
36
46
|
const SEARCH_DEBOUNCE_MS = 120;
|
|
37
47
|
|
|
38
48
|
function readStoredSidebarOpen(): boolean {
|
|
39
49
|
try {
|
|
40
|
-
const value = localStorage.getItem(
|
|
41
|
-
return value === null ?
|
|
50
|
+
const value = localStorage.getItem(sidebarStorageKey());
|
|
51
|
+
return value === null ? !shellInfo.immutableSite : value === "1";
|
|
42
52
|
} catch {
|
|
43
|
-
return
|
|
53
|
+
return !shellInfo.immutableSite;
|
|
44
54
|
}
|
|
45
55
|
}
|
|
46
56
|
|
|
@@ -132,6 +142,7 @@ function useListingMaxWidth(contentWidth: number, stored: number | null) {
|
|
|
132
142
|
}
|
|
133
143
|
|
|
134
144
|
export function AppShell({ route, navigate }: { route: Route; navigate: (path: string) => void }) {
|
|
145
|
+
const [diagramPreferences, setDiagramPreferences] = useState(false);
|
|
135
146
|
const { theme, toggle } = useTheme();
|
|
136
147
|
const { roots } = useTree();
|
|
137
148
|
|
|
@@ -171,6 +182,7 @@ export function AppShell({ route, navigate }: { route: Route; navigate: (path: s
|
|
|
171
182
|
const [exporting, setExporting] = useState(false);
|
|
172
183
|
const listingWidth = useAtomValue(listingWidthAtom);
|
|
173
184
|
const docWidth = useAtomValue(docWidthAtom);
|
|
185
|
+
const fullWidth = useAtomValue(contentLayoutAtom) === "full-width";
|
|
174
186
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
175
187
|
const contentWidth = useContentWidth(contentRef);
|
|
176
188
|
const listingMaxWidth = useListingMaxWidth(contentWidth, listingWidth);
|
|
@@ -203,7 +215,7 @@ export function AppShell({ route, navigate }: { route: Route; navigate: (path: s
|
|
|
203
215
|
|
|
204
216
|
useEffect(() => {
|
|
205
217
|
try {
|
|
206
|
-
localStorage.setItem(
|
|
218
|
+
localStorage.setItem(sidebarStorageKey(), desktopOpen ? "1" : "0");
|
|
207
219
|
} catch {
|
|
208
220
|
// private mode / storage disabled: the choice just won't persist
|
|
209
221
|
}
|
|
@@ -301,13 +313,22 @@ export function AppShell({ route, navigate }: { route: Route; navigate: (path: s
|
|
|
301
313
|
|
|
302
314
|
return (
|
|
303
315
|
<div className="min-h-screen bg-surface-page">
|
|
316
|
+
<AnimatePresence>
|
|
317
|
+
{diagramPreferences && (
|
|
318
|
+
<DiagramPreferences
|
|
319
|
+
canEditDiagrams={shellInfo.sameMachine && shellInfo.permissions !== "restricted"}
|
|
320
|
+
onClose={() => setDiagramPreferences(false)}
|
|
321
|
+
/>
|
|
322
|
+
)}
|
|
323
|
+
</AnimatePresence>
|
|
304
324
|
<TopBar
|
|
325
|
+
onDiagramPreferences={() => setDiagramPreferences(true)}
|
|
305
326
|
homeHref="/"
|
|
306
327
|
hostLabel={location.host}
|
|
307
|
-
sidebar={{ open: showSidebar, onToggle: handleToggleSidebar }}
|
|
328
|
+
sidebar={{ open: showSidebar || mobileOpen, onToggle: handleToggleSidebar }}
|
|
308
329
|
search={{ onOpen: () => setSearchOpen(true) }}
|
|
309
330
|
exportDoc={
|
|
310
|
-
route.kind === "doc" && shellInfo.sameMachine
|
|
331
|
+
route.kind === "doc" && shellInfo.permissions !== "restricted" && shellInfo.sameMachine
|
|
311
332
|
? { pending: exporting, onExport: handleExport }
|
|
312
333
|
: undefined
|
|
313
334
|
}
|
|
@@ -324,7 +345,7 @@ export function AppShell({ route, navigate }: { route: Route; navigate: (path: s
|
|
|
324
345
|
mobileOpen={mobileOpen}
|
|
325
346
|
onCloseMobile={() => setMobileOpen(false)}
|
|
326
347
|
/>
|
|
327
|
-
<main className="flex flex-1 min-w-0 px-8 pt-10 pb-24">
|
|
348
|
+
<main className="flex flex-1 min-w-0 px-5 pt-6 pb-16 md:px-8 md:pt-10 md:pb-24">
|
|
328
349
|
{/* The resize handles clamp drags to this area, and the max-width
|
|
329
350
|
springs track it, so content never runs under the toc rail. */}
|
|
330
351
|
<div ref={contentRef} data-content-area className="flex flex-1 min-w-0 justify-center">
|
|
@@ -336,16 +357,18 @@ export function AppShell({ route, navigate }: { route: Route; navigate: (path: s
|
|
|
336
357
|
animate="enter"
|
|
337
358
|
exit="exit"
|
|
338
359
|
className={
|
|
339
|
-
route.kind === "listing" || (route.kind === "doc" && docMaxWidth)
|
|
360
|
+
fullWidth || route.kind === "listing" || (route.kind === "doc" && docMaxWidth)
|
|
340
361
|
? "w-full"
|
|
341
362
|
: "w-full max-w-prose"
|
|
342
363
|
}
|
|
343
364
|
style={
|
|
344
|
-
|
|
345
|
-
? { maxWidth:
|
|
346
|
-
: route.kind === "
|
|
347
|
-
? { maxWidth:
|
|
348
|
-
:
|
|
365
|
+
fullWidth
|
|
366
|
+
? { maxWidth: "100%" }
|
|
367
|
+
: route.kind === "listing"
|
|
368
|
+
? { maxWidth: listingMaxWidth }
|
|
369
|
+
: route.kind === "doc" && docMaxWidth
|
|
370
|
+
? { maxWidth: docMaxWidth }
|
|
371
|
+
: undefined
|
|
349
372
|
}
|
|
350
373
|
>
|
|
351
374
|
<Breadcrumb items={breadcrumbItems(route, rootCount)} />
|
package/client/shell/Sidebar.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { AnimatePresence, LayoutGroup, motion } from "framer-motion";
|
|
2
|
-
import { useRef } from "react";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
3
|
import { useAtom } from "jotai";
|
|
4
4
|
import { VARIANTS } from "../motion";
|
|
5
5
|
import { Icon } from "../ui/Icon";
|
|
6
|
+
import { IconButton } from "../ui/IconButton";
|
|
6
7
|
import { ResizeHandle } from "../ui/ResizeHandle";
|
|
7
8
|
import { ListingView } from "../ListingView";
|
|
8
9
|
import { useFolderListing } from "../router";
|
|
@@ -95,6 +96,15 @@ export function Sidebar({
|
|
|
95
96
|
const [width, setWidth] = useAtom(sidebarWidthAtom);
|
|
96
97
|
const asideRef = useRef<HTMLDivElement>(null);
|
|
97
98
|
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!mobileOpen) return;
|
|
101
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
102
|
+
if (event.key === "Escape") onCloseMobile();
|
|
103
|
+
}
|
|
104
|
+
window.addEventListener("keydown", onKeyDown);
|
|
105
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
106
|
+
}, [mobileOpen, onCloseMobile]);
|
|
107
|
+
|
|
98
108
|
return (
|
|
99
109
|
<>
|
|
100
110
|
{/* Mobile overlay: fixed drawer + scrim, closed by default. */}
|
|
@@ -121,9 +131,17 @@ export function Sidebar({
|
|
|
121
131
|
if ((event.target as HTMLElement).closest("a[href]")) onCloseMobile();
|
|
122
132
|
}}
|
|
123
133
|
className={
|
|
124
|
-
"fixed inset-y-0 left-0 z-[var(--z-modal)] overflow-y-auto border-r border-border-subtle bg-surface-raised px-3 py-6 shadow-lg md:hidden w-sidebar"
|
|
134
|
+
"fixed inset-y-0 left-0 z-[var(--z-modal)] overflow-y-auto border-r border-border-subtle bg-surface-raised px-3 py-6 shadow-lg md:hidden w-sidebar max-w-[calc(100vw-3rem)]"
|
|
125
135
|
}
|
|
126
136
|
>
|
|
137
|
+
<div className="mb-3 flex justify-end">
|
|
138
|
+
<IconButton
|
|
139
|
+
icon="x"
|
|
140
|
+
label="Close navigation"
|
|
141
|
+
className="h-11 w-11"
|
|
142
|
+
onClick={onCloseMobile}
|
|
143
|
+
/>
|
|
144
|
+
</div>
|
|
127
145
|
<SidebarBody
|
|
128
146
|
rootDir={rootDir}
|
|
129
147
|
activePath={activePath}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { motion } from "framer-motion";
|
|
1
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
2
2
|
import { useAtomValue } from "jotai";
|
|
3
3
|
import { useCallback, useRef, useState } from "react";
|
|
4
|
+
import { Settings } from "../Settings";
|
|
4
5
|
import { DocView } from "../DocView";
|
|
5
6
|
import { fadeRise } from "../motion";
|
|
6
7
|
import { useTheme } from "../theme";
|
|
7
8
|
import { docModuleCache } from "../doc-module-cache";
|
|
8
|
-
import { docWidthAtom } from "../state";
|
|
9
|
+
import { contentLayoutAtom, docWidthAtom } from "../state";
|
|
9
10
|
import { Footer } from "./Footer";
|
|
10
11
|
import { TocRail } from "./TocRail";
|
|
11
12
|
import { TopBar } from "./TopBar";
|
|
@@ -20,8 +21,10 @@ import type { StandaloneMeta } from "../standalone-entry";
|
|
|
20
21
|
*/
|
|
21
22
|
export function StandaloneShell({ meta }: { meta: StandaloneMeta }) {
|
|
22
23
|
const { theme, toggle } = useTheme();
|
|
24
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
23
25
|
const [tocVersion, setTocVersion] = useState(0);
|
|
24
26
|
const docWidth = useAtomValue(docWidthAtom);
|
|
27
|
+
const fullWidth = useAtomValue(contentLayoutAtom) === "full-width";
|
|
25
28
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
26
29
|
const contentWidth = useContentWidth(contentRef);
|
|
27
30
|
const docMaxWidth = useDocMaxWidth(contentWidth, docWidth);
|
|
@@ -40,15 +43,25 @@ export function StandaloneShell({ meta }: { meta: StandaloneMeta }) {
|
|
|
40
43
|
|
|
41
44
|
return (
|
|
42
45
|
<div className="min-h-screen bg-surface-page">
|
|
43
|
-
<
|
|
44
|
-
|
|
46
|
+
<AnimatePresence>
|
|
47
|
+
{settingsOpen && <Settings onClose={() => setSettingsOpen(false)} />}
|
|
48
|
+
</AnimatePresence>
|
|
49
|
+
<TopBar
|
|
50
|
+
theme={theme}
|
|
51
|
+
onToggleTheme={toggle}
|
|
52
|
+
hostLabel={meta.label}
|
|
53
|
+
onDiagramPreferences={() => setSettingsOpen(true)}
|
|
54
|
+
/>
|
|
55
|
+
<main className="flex flex-1 min-w-0 px-5 pt-6 pb-16 md:px-8 md:pt-10 md:pb-24">
|
|
45
56
|
<div ref={contentRef} data-content-area className="flex flex-1 min-w-0 justify-center">
|
|
46
57
|
<motion.div
|
|
47
58
|
variants={fadeRise}
|
|
48
59
|
initial="initial"
|
|
49
60
|
animate="enter"
|
|
50
|
-
className={docMaxWidth ? "w-full" : "w-full max-w-prose"}
|
|
51
|
-
style={
|
|
61
|
+
className={fullWidth || docMaxWidth ? "w-full" : "w-full max-w-prose"}
|
|
62
|
+
style={
|
|
63
|
+
fullWidth ? { maxWidth: "100%" } : docMaxWidth ? { maxWidth: docMaxWidth } : undefined
|
|
64
|
+
}
|
|
52
65
|
>
|
|
53
66
|
<DocView
|
|
54
67
|
path={meta.path}
|
package/client/shell/TocRail.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useAtom } from "jotai";
|
|
1
|
+
import { useAtom, useAtomValue } from "jotai";
|
|
2
2
|
import { useRef } from "react";
|
|
3
3
|
import { ResizeHandle } from "../ui/ResizeHandle";
|
|
4
4
|
import { TocList } from "../ui/TocList";
|
|
5
|
-
import { TOC_MAX_WIDTH, TOC_MIN_WIDTH, tocWidthAtom } from "../state";
|
|
5
|
+
import { TOC_MAX_WIDTH, TOC_MIN_WIDTH, tocVisibleAtom, tocWidthAtom } from "../state";
|
|
6
6
|
import { useToc } from "./useToc";
|
|
7
7
|
|
|
8
8
|
function clampWidth(px: number): number {
|
|
@@ -13,9 +13,10 @@ function clampWidth(px: number): number {
|
|
|
13
13
|
export function TocRail({ path, version }: { path: string; version: number }) {
|
|
14
14
|
const { items, activeId, setActiveId } = useToc(path, version);
|
|
15
15
|
const [width, setWidth] = useAtom(tocWidthAtom);
|
|
16
|
+
const visible = useAtomValue(tocVisibleAtom);
|
|
16
17
|
const railRef = useRef<HTMLDivElement>(null);
|
|
17
18
|
const hasH2 = items.some((item) => item.level === 2);
|
|
18
|
-
if (!hasH2) return null;
|
|
19
|
+
if (!visible || !hasH2) return null;
|
|
19
20
|
|
|
20
21
|
function handleSelect(id: string) {
|
|
21
22
|
setActiveId(id);
|
package/client/shell/TopBar.tsx
CHANGED
|
@@ -44,6 +44,7 @@ export function TopBar({
|
|
|
44
44
|
exportDoc,
|
|
45
45
|
theme,
|
|
46
46
|
onToggleTheme,
|
|
47
|
+
onDiagramPreferences,
|
|
47
48
|
}: {
|
|
48
49
|
/** Wordmark's link target; undefined renders it as a plain (non-link) span. */
|
|
49
50
|
homeHref?: string;
|
|
@@ -57,13 +58,14 @@ export function TopBar({
|
|
|
57
58
|
exportDoc?: { pending: boolean; onExport: (format: ExportFormat) => void };
|
|
58
59
|
theme: Theme;
|
|
59
60
|
onToggleTheme: () => void;
|
|
61
|
+
onDiagramPreferences?: () => void;
|
|
60
62
|
}) {
|
|
61
63
|
const mac = isApplePlatform();
|
|
62
64
|
|
|
63
65
|
return (
|
|
64
66
|
<header
|
|
65
67
|
data-print-hide
|
|
66
|
-
className="sticky top-0 z-10 flex h-topbar shrink-0 items-center gap-
|
|
68
|
+
className="sticky top-0 z-10 flex h-topbar shrink-0 items-center gap-1 border-b border-border-subtle px-2 md:gap-3 md:px-4 [&>button]:max-md:h-11 [&>button]:max-md:w-11 [&>button]:shrink-0"
|
|
67
69
|
style={{
|
|
68
70
|
background: "color-mix(in oklab, var(--surface-page) 82%, transparent)",
|
|
69
71
|
backdropFilter: "var(--blur-chrome)",
|
|
@@ -80,7 +82,10 @@ export function TopBar({
|
|
|
80
82
|
) : null}
|
|
81
83
|
<Wordmark href={homeHref} />
|
|
82
84
|
{hostLabel ? (
|
|
83
|
-
<span
|
|
85
|
+
<span
|
|
86
|
+
title={hostLabel}
|
|
87
|
+
className="hidden min-w-0 max-w-48 truncate md:block rounded-sm border border-border-default px-1.5 py-0.5 font-mono text-[length:var(--size-2xs)] text-text-subtle"
|
|
88
|
+
>
|
|
84
89
|
{hostLabel}
|
|
85
90
|
</span>
|
|
86
91
|
) : null}
|
|
@@ -89,11 +94,15 @@ export function TopBar({
|
|
|
89
94
|
<button
|
|
90
95
|
type="button"
|
|
91
96
|
onClick={search.onOpen}
|
|
92
|
-
|
|
97
|
+
aria-label="Search docs"
|
|
98
|
+
title="Search docs"
|
|
99
|
+
className="flex h-11 w-11 shrink-0 justify-center md:h-[30px] md:w-60 md:justify-start items-center gap-2 rounded-md border border-border-default bg-surface-card px-2.5 text-[13px] text-text-subtle shadow-xs cursor-pointer"
|
|
93
100
|
>
|
|
94
101
|
<Icon name="search" size="sm" />
|
|
95
|
-
<span className="flex-1 text-left">Search docs</span>
|
|
96
|
-
<
|
|
102
|
+
<span className="hidden md:block flex-1 text-left">Search docs</span>
|
|
103
|
+
<span className="hidden md:contents">
|
|
104
|
+
<Kbd>{mac ? "⌘K" : "Ctrl K"}</Kbd>
|
|
105
|
+
</span>
|
|
97
106
|
</button>
|
|
98
107
|
) : null}
|
|
99
108
|
{exportDoc ? (
|
|
@@ -106,6 +115,9 @@ export function TopBar({
|
|
|
106
115
|
onChange={(format) => exportDoc.onExport(format as ExportFormat)}
|
|
107
116
|
/>
|
|
108
117
|
) : null}
|
|
118
|
+
{onDiagramPreferences && (
|
|
119
|
+
<IconButton icon="settings" label="Settings" size="sm" onClick={onDiagramPreferences} />
|
|
120
|
+
)}
|
|
109
121
|
<IconButton
|
|
110
122
|
icon={theme === "dark" ? "sun" : "moon"}
|
|
111
123
|
label={theme === "dark" ? "Light mode" : "Dark mode"}
|
package/client/state.ts
CHANGED
|
@@ -88,3 +88,17 @@ export const docWidthAtom = atomWithStorage<number | null>("mdxserve.doc.width",
|
|
|
88
88
|
* back to read mode, discarding whatever it had open (see client/MdSection.tsx).
|
|
89
89
|
*/
|
|
90
90
|
export const openSectionAtom = atom<string | null>(null);
|
|
91
|
+
|
|
92
|
+
export type ContentLayout = "flexible" | "full-width";
|
|
93
|
+
|
|
94
|
+
/** Content layout preference; saved widths are retained when full width is active. */
|
|
95
|
+
export const contentLayoutAtom = atomWithStorage<ContentLayout>(
|
|
96
|
+
"mdxserve.content.layout",
|
|
97
|
+
"flexible",
|
|
98
|
+
undefined,
|
|
99
|
+
{ getOnInit: true },
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
export const tocVisibleAtom = atomWithStorage<boolean>("mdxserve.toc.visible", true, undefined, {
|
|
103
|
+
getOnInit: true,
|
|
104
|
+
});
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
2
|
-
import { createPortal } from "react-dom";
|
|
3
|
-
import { AnimatePresence, motion } from "framer-motion";
|
|
4
1
|
import { formatModified, formatSize } from "../format";
|
|
5
2
|
import Button from "../builtins/Button";
|
|
6
3
|
import type { ListingEntry } from "../router";
|
|
7
4
|
import { Icon } from "./Icon";
|
|
8
|
-
import {
|
|
5
|
+
import { Modal } from "./Modal";
|
|
9
6
|
|
|
10
7
|
export function ConfirmDeleteDialog({
|
|
11
8
|
open,
|
|
@@ -21,93 +18,53 @@ export function ConfirmDeleteDialog({
|
|
|
21
18
|
onCancel: () => void;
|
|
22
19
|
onConfirm: () => void;
|
|
23
20
|
}) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// Portal to <body>: this renders inside the route wrapper, whose fadeRise
|
|
41
|
-
// transform would otherwise turn `fixed` into "fixed to the column" and let
|
|
42
|
-
// the scrim miss the top bar and sidebar.
|
|
43
|
-
return createPortal(
|
|
44
|
-
<AnimatePresence>
|
|
45
|
-
{open ? (
|
|
46
|
-
<motion.div
|
|
47
|
-
key="scrim"
|
|
48
|
-
onClick={pending ? undefined : onCancel}
|
|
49
|
-
{...VARIANTS.scrim}
|
|
50
|
-
className="fixed inset-0 z-[var(--z-modal)] flex justify-center bg-[var(--scrim)] pt-[10vh] backdrop-blur-sm"
|
|
51
|
-
>
|
|
52
|
-
<motion.div
|
|
53
|
-
onClick={(event) => event.stopPropagation()}
|
|
54
|
-
{...VARIANTS.pop}
|
|
55
|
-
role="alertdialog"
|
|
56
|
-
aria-modal="true"
|
|
57
|
-
aria-labelledby="confirm-delete-title"
|
|
58
|
-
className="w-[560px] max-w-[92vw] self-start overflow-hidden rounded-xl border border-border-default bg-surface-raised shadow-lg"
|
|
21
|
+
return (
|
|
22
|
+
<Modal
|
|
23
|
+
open={open}
|
|
24
|
+
onClose={onCancel}
|
|
25
|
+
dismissible={!pending}
|
|
26
|
+
placement="top"
|
|
27
|
+
role="alertdialog"
|
|
28
|
+
aria-labelledby="confirm-delete-title"
|
|
29
|
+
className="w-[560px] overflow-hidden"
|
|
30
|
+
>
|
|
31
|
+
<div className="flex items-center gap-2 border-b border-border-subtle px-4 py-3">
|
|
32
|
+
<Icon name="trash-2" size="md" className="text-status-danger-fg" />
|
|
33
|
+
<div className="flex flex-col">
|
|
34
|
+
<span
|
|
35
|
+
id="confirm-delete-title"
|
|
36
|
+
className="font-sans font-semibold text-text-heading text-[length:var(--size-md)]"
|
|
59
37
|
>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
</div>
|
|
92
|
-
<div className="flex justify-end gap-2 border-t border-border-subtle px-4 py-3">
|
|
93
|
-
{/* Start keyboard focus on the safe action. */}
|
|
94
|
-
<Button variant="secondary" size="sm" autoFocus disabled={pending} onClick={onCancel}>
|
|
95
|
-
Cancel
|
|
96
|
-
</Button>
|
|
97
|
-
<Button
|
|
98
|
-
variant="danger"
|
|
99
|
-
size="sm"
|
|
100
|
-
icon="trash-2"
|
|
101
|
-
disabled={pending}
|
|
102
|
-
onClick={onConfirm}
|
|
103
|
-
>
|
|
104
|
-
{pending ? "Moving…" : "Move to Trash"}
|
|
105
|
-
</Button>
|
|
106
|
-
</div>
|
|
107
|
-
</motion.div>
|
|
108
|
-
</motion.div>
|
|
109
|
-
) : null}
|
|
110
|
-
</AnimatePresence>,
|
|
111
|
-
document.body,
|
|
38
|
+
Delete {files.length} {files.length === 1 ? "file" : "files"}?
|
|
39
|
+
</span>
|
|
40
|
+
<span className="font-sans text-[length:var(--size-sm)] text-text-muted">
|
|
41
|
+
They’ll be moved to the system Trash — this can be undone from there.
|
|
42
|
+
</span>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div className="max-h-72 overflow-y-auto p-2">
|
|
46
|
+
{files.map((file) => (
|
|
47
|
+
<div key={file.name} className="flex items-center gap-2 rounded-md px-2 py-1.5">
|
|
48
|
+
<Icon name="file-text" size="sm" className="shrink-0 text-text-subtle" />
|
|
49
|
+
<span className="truncate font-mono text-[length:var(--size-sm)]">{file.name}</span>
|
|
50
|
+
<span className="ml-auto flex shrink-0 gap-4 pl-4 font-mono text-[length:var(--size-xs)] text-text-subtle tabular-nums">
|
|
51
|
+
{typeof file.mtime === "number" ? <span>{formatModified(file.mtime)}</span> : null}
|
|
52
|
+
{typeof file.size === "number" ? (
|
|
53
|
+
<span className="w-16 text-right">{formatSize(file.size)}</span>
|
|
54
|
+
) : null}
|
|
55
|
+
</span>
|
|
56
|
+
</div>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
<div className="flex justify-end gap-2 border-t border-border-subtle px-4 py-3">
|
|
60
|
+
{/* Start keyboard focus on the safe action. */}
|
|
61
|
+
<Button variant="secondary" size="sm" autoFocus disabled={pending} onClick={onCancel}>
|
|
62
|
+
Cancel
|
|
63
|
+
</Button>
|
|
64
|
+
<Button variant="danger" size="sm" icon="trash-2" disabled={pending} onClick={onConfirm}>
|
|
65
|
+
{pending ? "Moving…" : "Move to Trash"}
|
|
66
|
+
</Button>
|
|
67
|
+
</div>
|
|
68
|
+
</Modal>
|
|
112
69
|
);
|
|
113
70
|
}
|