@open-slide/core 1.7.0 → 1.9.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/dist/{build-tLrkKUHr.js → build-ZM7IfDO-.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-PwUHqZ_X.js → config-BAZeaz2P.js} +289 -246
- package/dist/{config-CfMThYN9.d.ts → config-D_5nlXFU.d.ts} +6 -1
- package/dist/{dev-DpCIRbhT.js → dev-BQkNTG_t.js} +1 -1
- package/dist/format-CYOb2cAQ.js +1573 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +38 -4
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +1 -1144
- package/dist/{preview-BSGlM6Se.js → preview-D8hUtbRA.js} +1 -1
- package/dist/{types-B-KrjgX8.d.ts → types-AalTbxMj.d.ts} +17 -3
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +1 -1
- package/package.json +2 -1
- package/skills/create-theme/SKILL.md +1 -1
- package/src/app/components/inspector/comment-widget.tsx +16 -2
- package/src/app/components/language-toggle.tsx +39 -0
- package/src/app/components/player.tsx +12 -17
- package/src/app/components/pptx-progress-toast.tsx +32 -0
- package/src/app/components/sidebar/folder-item.tsx +7 -2
- package/src/app/components/sidebar/sidebar-footer.tsx +51 -0
- package/src/app/components/sidebar/sidebar.tsx +95 -17
- package/src/app/lib/design-presets.ts +1 -1
- package/src/app/lib/export-pptx.ts +284 -0
- package/src/app/lib/folders.ts +28 -0
- package/src/app/lib/inspector/fiber.test.ts +154 -0
- package/src/app/lib/inspector/fiber.ts +12 -1
- package/src/app/lib/locale-store.ts +67 -0
- package/src/app/lib/use-click-page-navigation.ts +52 -0
- package/src/app/lib/use-is-mobile.ts +21 -0
- package/src/app/lib/use-locale.ts +4 -16
- package/src/app/routes/home-shell.tsx +8 -0
- package/src/app/routes/home.tsx +1 -1
- package/src/app/routes/slide.tsx +145 -53
- package/src/app/virtual.d.ts +1 -0
- package/src/locale/en.ts +18 -3
- package/src/locale/ja.ts +19 -3
- package/src/locale/types.ts +18 -3
- package/src/locale/zh-cn.ts +17 -3
- package/src/locale/zh-tw.ts +17 -3
- package/dist/en-BDnM5zKJ.js +0 -378
- package/src/app/components/click-nav-zones.tsx +0 -36
|
@@ -44,6 +44,7 @@ type Locale = {
|
|
|
44
44
|
folders: string;
|
|
45
45
|
newFolder: string;
|
|
46
46
|
folderName: string;
|
|
47
|
+
updateAvailable: string;
|
|
47
48
|
changeIcon: string;
|
|
48
49
|
iconEmojiTab: string;
|
|
49
50
|
iconColorTab: string;
|
|
@@ -90,6 +91,7 @@ type Locale = {
|
|
|
90
91
|
/** template: "Deleted folder “{name}”" */
|
|
91
92
|
toastFolderDeleted: string;
|
|
92
93
|
toastFolderDeleteFailed: string;
|
|
94
|
+
toastFolderReorderFailed: string;
|
|
93
95
|
pickIcon: string;
|
|
94
96
|
};
|
|
95
97
|
slide: {
|
|
@@ -105,7 +107,12 @@ type Locale = {
|
|
|
105
107
|
toastCopyLinkFailed: string;
|
|
106
108
|
exportAsHtml: string;
|
|
107
109
|
exportAsPdf: string;
|
|
110
|
+
exportAsImagePptx: string;
|
|
111
|
+
exportAsPptx: string;
|
|
112
|
+
comingSoon: string;
|
|
113
|
+
pptxComingSoonTooltip: string;
|
|
108
114
|
pdfExportFailed: string;
|
|
115
|
+
imagePptxExportFailed: string;
|
|
109
116
|
pdfExportSafariUnsupported: string;
|
|
110
117
|
present: string;
|
|
111
118
|
presentMenuAria: string;
|
|
@@ -358,6 +365,13 @@ type Locale = {
|
|
|
358
365
|
printing: string;
|
|
359
366
|
done: string;
|
|
360
367
|
};
|
|
368
|
+
pptxToast: {
|
|
369
|
+
title: string;
|
|
370
|
+
/** template: "Rendering page {current} of {total}" */
|
|
371
|
+
processing: string;
|
|
372
|
+
generating: string;
|
|
373
|
+
done: string;
|
|
374
|
+
};
|
|
361
375
|
themeToggle: {
|
|
362
376
|
toggleAria: string;
|
|
363
377
|
title: string;
|
|
@@ -365,9 +379,9 @@ type Locale = {
|
|
|
365
379
|
dark: string;
|
|
366
380
|
system: string;
|
|
367
381
|
};
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
382
|
+
languageToggle: {
|
|
383
|
+
toggleAria: string;
|
|
384
|
+
title: string;
|
|
371
385
|
};
|
|
372
386
|
imagePlaceholder: {
|
|
373
387
|
dropOverlay: string;
|
package/dist/vite/index.d.ts
CHANGED
package/dist/vite/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-slide/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Runtime and CLI for open-slide — write slides in slides/, we handle the rest.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"emoji-picker-react": "^4.18.0",
|
|
75
75
|
"fast-glob": "^3.3.2",
|
|
76
76
|
"fflate": "^0.8.2",
|
|
77
|
+
"html-to-image": "^1.11.13",
|
|
77
78
|
"lucide-react": "^1.8.0",
|
|
78
79
|
"next-themes": "^0.4.6",
|
|
79
80
|
"radix-ui": "^1.4.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: create-theme
|
|
3
|
-
description: Use this skill when the user wants to create, draft, author, or extract a slide theme in this open-slide repo. Triggers on phrases like "create a theme", "make a theme called X", "extract a theme from <slide>", "build a theme from these images". Produces two paired files under `themes
|
|
3
|
+
description: Use this skill when the user wants to create, draft, author, or extract a slide theme in this open-slide repo. Triggers on phrases like "create a theme", "make a theme called X", "extract a theme from <slide>", "build a theme from these images". Produces two paired files under `themes/` — `<id>.md` (palette, typography, layout, fixed Title/Footer components, motion) and `<id>.demo.tsx` (a runnable demo slide that the dev-UI Themes panel previews). Do NOT use for editing real slides — only for authoring the theme bundle.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Create a slide theme
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MessageSquare, Trash2, X } from 'lucide-react';
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { format, plural, useLocale } from '@/lib/use-locale';
|
|
4
4
|
import { useInspector } from './inspector-provider';
|
|
5
5
|
|
|
@@ -8,9 +8,23 @@ export function CommentWidget() {
|
|
|
8
8
|
const { comments, remove, error } = useInspector();
|
|
9
9
|
const [open, setOpen] = useState(false);
|
|
10
10
|
const count = comments.length;
|
|
11
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!open) return;
|
|
15
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
16
|
+
if (!ref.current?.contains(e.target as Node)) setOpen(false);
|
|
17
|
+
};
|
|
18
|
+
document.addEventListener('pointerdown', onPointerDown);
|
|
19
|
+
return () => document.removeEventListener('pointerdown', onPointerDown);
|
|
20
|
+
}, [open]);
|
|
11
21
|
|
|
12
22
|
return (
|
|
13
|
-
<div
|
|
23
|
+
<div
|
|
24
|
+
ref={ref}
|
|
25
|
+
data-inspector-ui
|
|
26
|
+
className="absolute right-4 bottom-4 z-20 flex flex-col items-end gap-2"
|
|
27
|
+
>
|
|
14
28
|
{open && (
|
|
15
29
|
<div className="w-80 rounded-md border bg-card shadow-xl animate-in fade-in-0 slide-in-from-bottom-2 duration-200">
|
|
16
30
|
<div className="flex items-center justify-between border-b px-3 py-2">
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Languages } from 'lucide-react';
|
|
2
|
+
import { buttonVariants } from '@/components/ui/button';
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
DropdownMenuContent,
|
|
6
|
+
DropdownMenuItem,
|
|
7
|
+
DropdownMenuTrigger,
|
|
8
|
+
} from '@/components/ui/dropdown-menu';
|
|
9
|
+
import { LOCALE_OPTIONS, setLocale } from '@/lib/locale-store';
|
|
10
|
+
import { useLocale } from '@/lib/use-locale';
|
|
11
|
+
import { cn } from '@/lib/utils';
|
|
12
|
+
|
|
13
|
+
export function LanguageToggle() {
|
|
14
|
+
const t = useLocale();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<DropdownMenu>
|
|
18
|
+
<DropdownMenuTrigger
|
|
19
|
+
type="button"
|
|
20
|
+
aria-label={t.languageToggle.toggleAria}
|
|
21
|
+
title={t.languageToggle.title}
|
|
22
|
+
className={cn(buttonVariants({ variant: 'ghost', size: 'icon-sm' }))}
|
|
23
|
+
>
|
|
24
|
+
<Languages className="size-3.5" />
|
|
25
|
+
</DropdownMenuTrigger>
|
|
26
|
+
<DropdownMenuContent align="end" className="min-w-[140px]">
|
|
27
|
+
{LOCALE_OPTIONS.map((option) => (
|
|
28
|
+
<DropdownMenuItem
|
|
29
|
+
key={option.id}
|
|
30
|
+
onSelect={() => setLocale(option.id)}
|
|
31
|
+
data-active={t.id === option.id}
|
|
32
|
+
>
|
|
33
|
+
{option.label}
|
|
34
|
+
</DropdownMenuItem>
|
|
35
|
+
))}
|
|
36
|
+
</DropdownMenuContent>
|
|
37
|
+
</DropdownMenu>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useClickPageNavigation } from '@/lib/use-click-page-navigation';
|
|
2
3
|
import { useWheelPageNavigation } from '@/lib/use-wheel-page-navigation';
|
|
3
4
|
import { cn } from '@/lib/utils';
|
|
4
5
|
import type { DesignSystem } from '../lib/design';
|
|
@@ -92,6 +93,15 @@ export function Player({
|
|
|
92
93
|
|
|
93
94
|
const overlayActive = controls && (overviewOpen || helpOpen);
|
|
94
95
|
|
|
96
|
+
useClickPageNavigation({
|
|
97
|
+
ref: rootRef,
|
|
98
|
+
enabled: !overlayActive,
|
|
99
|
+
canPrev,
|
|
100
|
+
canNext,
|
|
101
|
+
onPrev: goPrev,
|
|
102
|
+
onNext: goNext,
|
|
103
|
+
});
|
|
104
|
+
|
|
95
105
|
useWheelPageNavigation({
|
|
96
106
|
ref: rootRef,
|
|
97
107
|
enabled: !overlayActive,
|
|
@@ -308,23 +318,8 @@ export function Player({
|
|
|
308
318
|
/>
|
|
309
319
|
</SlideCanvas>
|
|
310
320
|
|
|
311
|
-
<button
|
|
312
|
-
type="button"
|
|
313
|
-
aria-label="Previous page"
|
|
314
|
-
onClick={goPrev}
|
|
315
|
-
disabled={!canPrev}
|
|
316
|
-
className={cn('absolute inset-y-0 left-0 z-10 w-[30%]', hideCursor && 'cursor-none')}
|
|
317
|
-
/>
|
|
318
|
-
<button
|
|
319
|
-
type="button"
|
|
320
|
-
aria-label="Next page"
|
|
321
|
-
onClick={goNext}
|
|
322
|
-
disabled={!canNext}
|
|
323
|
-
className={cn('absolute inset-y-0 right-0 z-10 w-[30%]', hideCursor && 'cursor-none')}
|
|
324
|
-
/>
|
|
325
|
-
|
|
326
321
|
{controls && (
|
|
327
|
-
|
|
322
|
+
<div data-osd-chrome style={{ display: 'contents' }}>
|
|
328
323
|
<PresentProgressBar index={index} total={pages.length} visible={chromeVisible} />
|
|
329
324
|
<PresentBlackoutOverlay mode={blackout} />
|
|
330
325
|
<PresentJumpInput pageCount={pages.length} onJump={onIndexChange} />
|
|
@@ -358,7 +353,7 @@ export function Player({
|
|
|
358
353
|
onSelect={onIndexChange}
|
|
359
354
|
/>
|
|
360
355
|
<PresentHelpOverlay open={helpOpen} onOpenChange={setHelpOpen} container={rootEl} />
|
|
361
|
-
|
|
356
|
+
</div>
|
|
362
357
|
)}
|
|
363
358
|
</div>
|
|
364
359
|
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Loader2 } from 'lucide-react';
|
|
2
|
+
import { format, useLocale } from '@/lib/use-locale';
|
|
3
|
+
import type { PptxExportProgress } from '../lib/export-pptx';
|
|
4
|
+
import { Progress } from './ui/progress';
|
|
5
|
+
|
|
6
|
+
export function PptxProgressToast({ progress }: { progress: PptxExportProgress }) {
|
|
7
|
+
const t = useLocale();
|
|
8
|
+
const text =
|
|
9
|
+
progress.phase === 'processing'
|
|
10
|
+
? format(t.pptxToast.processing, {
|
|
11
|
+
current: progress.current.toString().padStart(2, '0'),
|
|
12
|
+
total: progress.total.toString().padStart(2, '0'),
|
|
13
|
+
})
|
|
14
|
+
: progress.phase === 'generating'
|
|
15
|
+
? t.pptxToast.generating
|
|
16
|
+
: t.pptxToast.done;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex w-80 items-start gap-3 rounded-[8px] border border-border bg-popover px-3.5 py-3 text-popover-foreground shadow-floating">
|
|
20
|
+
<Loader2 className="mt-0.5 size-3.5 shrink-0 animate-spin text-brand" />
|
|
21
|
+
<div className="min-w-0 flex-1">
|
|
22
|
+
<p className="font-heading text-[12.5px] font-semibold tracking-tight">
|
|
23
|
+
{t.pptxToast.title}
|
|
24
|
+
</p>
|
|
25
|
+
<p className="truncate font-mono text-[10.5px] tracking-[0.04em] text-muted-foreground">
|
|
26
|
+
{text}
|
|
27
|
+
</p>
|
|
28
|
+
<Progress value={Math.round(progress.percent)} className="mt-2 h-[3px]" />
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -181,9 +181,14 @@ export function FolderItem({
|
|
|
181
181
|
</PopoverContent>
|
|
182
182
|
</Popover>
|
|
183
183
|
) : (
|
|
184
|
-
<
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
onClick={onSelect}
|
|
187
|
+
aria-label={label}
|
|
188
|
+
className="flex size-5 shrink-0 items-center justify-center"
|
|
189
|
+
>
|
|
185
190
|
<FolderIconChip icon={icon} />
|
|
186
|
-
</
|
|
191
|
+
</button>
|
|
187
192
|
)}
|
|
188
193
|
|
|
189
194
|
{renaming && row.kind === 'folder' ? (
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import config from 'virtual:open-slide/config';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
4
|
+
import { format, useLocale } from '@/lib/use-locale';
|
|
5
|
+
|
|
6
|
+
type UpdateCheck = { current: string; latest: string | null; outdated: boolean };
|
|
7
|
+
|
|
8
|
+
export function SidebarFooter() {
|
|
9
|
+
const t = useLocale();
|
|
10
|
+
const [update, setUpdate] = useState<UpdateCheck | null>(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!import.meta.env.DEV) return;
|
|
14
|
+
let cancelled = false;
|
|
15
|
+
fetch('/__update-check')
|
|
16
|
+
.then((res) => (res.ok ? (res.json() as Promise<UpdateCheck>) : null))
|
|
17
|
+
.then((data) => {
|
|
18
|
+
if (!cancelled && data?.outdated) setUpdate(data);
|
|
19
|
+
})
|
|
20
|
+
.catch(() => {});
|
|
21
|
+
return () => {
|
|
22
|
+
cancelled = true;
|
|
23
|
+
};
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
const label = `v${config.version}`;
|
|
27
|
+
|
|
28
|
+
const versionRow = (
|
|
29
|
+
<span className="inline-flex items-center gap-1.5">
|
|
30
|
+
{update?.latest && <span className="size-1.5 rounded-full bg-brand" aria-hidden />}
|
|
31
|
+
{label}
|
|
32
|
+
</span>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="px-4 py-3 text-[11px] text-muted-foreground/70 tabular-nums">
|
|
37
|
+
{update?.latest ? (
|
|
38
|
+
<TooltipProvider delayDuration={200}>
|
|
39
|
+
<Tooltip>
|
|
40
|
+
<TooltipTrigger asChild>{versionRow}</TooltipTrigger>
|
|
41
|
+
<TooltipContent side="top" sideOffset={6} className="max-w-56">
|
|
42
|
+
{format(t.home.updateAvailable, { version: update.latest })}
|
|
43
|
+
</TooltipContent>
|
|
44
|
+
</Tooltip>
|
|
45
|
+
</TooltipProvider>
|
|
46
|
+
) : (
|
|
47
|
+
versionRow
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { Plus } from 'lucide-react';
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { toast } from 'sonner';
|
|
4
|
+
import { LanguageToggle } from '@/components/language-toggle';
|
|
4
5
|
import { ThemeToggle } from '@/components/theme-toggle';
|
|
5
6
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
6
7
|
import type { Folder, FolderIcon } from '@/lib/sdk';
|
|
7
8
|
import { format, useLocale } from '@/lib/use-locale';
|
|
9
|
+
import { cn } from '@/lib/utils';
|
|
8
10
|
import { FolderIconChip, FolderItem } from './folder-item';
|
|
9
11
|
import { IconPicker, PRESET_COLORS } from './icon-picker';
|
|
12
|
+
import { SidebarFooter } from './sidebar-footer';
|
|
10
13
|
|
|
11
14
|
export const DRAFT_ID = 'draft';
|
|
12
15
|
export const THEMES_ID = '__themes__';
|
|
13
16
|
export const ASSETS_ID = '__assets__';
|
|
14
17
|
|
|
18
|
+
export const FOLDER_DND_MIME = 'application/x-folder-id';
|
|
19
|
+
|
|
15
20
|
export function Sidebar({
|
|
16
21
|
folders,
|
|
17
22
|
countFor,
|
|
@@ -25,6 +30,7 @@ export function Sidebar({
|
|
|
25
30
|
onDelete,
|
|
26
31
|
onDropToFolder,
|
|
27
32
|
onDropToDraft,
|
|
33
|
+
onReorder,
|
|
28
34
|
}: {
|
|
29
35
|
folders: Folder[];
|
|
30
36
|
countFor: (folderId: string | null) => number;
|
|
@@ -38,7 +44,23 @@ export function Sidebar({
|
|
|
38
44
|
onDelete: (id: string) => void;
|
|
39
45
|
onDropToFolder: (folderId: string, slideId: string) => void;
|
|
40
46
|
onDropToDraft: (slideId: string) => void;
|
|
47
|
+
onReorder: (ids: string[]) => void;
|
|
41
48
|
}) {
|
|
49
|
+
const [dragId, setDragId] = useState<string | null>(null);
|
|
50
|
+
const [dropTarget, setDropTarget] = useState<{ id: string; before: boolean } | null>(null);
|
|
51
|
+
|
|
52
|
+
const finishReorder = (toId: string, before: boolean) => {
|
|
53
|
+
const fromId = dragId;
|
|
54
|
+
setDragId(null);
|
|
55
|
+
setDropTarget(null);
|
|
56
|
+
if (!fromId || fromId === toId) return;
|
|
57
|
+
const ids = folders.map((f) => f.id);
|
|
58
|
+
if (!ids.includes(fromId) || !ids.includes(toId)) return;
|
|
59
|
+
const next = ids.filter((id) => id !== fromId);
|
|
60
|
+
next.splice(next.indexOf(toId) + (before ? 0 : 1), 0, fromId);
|
|
61
|
+
if (next.every((id, i) => id === ids[i])) return;
|
|
62
|
+
onReorder(next);
|
|
63
|
+
};
|
|
42
64
|
const [creating, setCreating] = useState(false);
|
|
43
65
|
const [newName, setNewName] = useState('');
|
|
44
66
|
const [newIcon, setNewIcon] = useState<FolderIcon>(() => ({
|
|
@@ -104,7 +126,8 @@ export function Sidebar({
|
|
|
104
126
|
<aside className="paper relative flex h-full w-[16.5rem] shrink-0 flex-col border-r border-hairline bg-sidebar text-sidebar-foreground">
|
|
105
127
|
<div className="flex items-center justify-between px-4 pt-5 pb-4">
|
|
106
128
|
<h1 className="font-heading text-lg font-bold tracking-tight">{t.home.appTitle}</h1>
|
|
107
|
-
<div className="-mr-1.5">
|
|
129
|
+
<div className="-mr-1.5 flex items-center">
|
|
130
|
+
<LanguageToggle />
|
|
108
131
|
<ThemeToggle />
|
|
109
132
|
</div>
|
|
110
133
|
</div>
|
|
@@ -139,22 +162,73 @@ export function Sidebar({
|
|
|
139
162
|
</div>
|
|
140
163
|
|
|
141
164
|
<div className="flex-1 overflow-y-auto px-2 pb-2">
|
|
142
|
-
{folders.map((folder) =>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
{folders.map((folder) => {
|
|
166
|
+
const isDropTarget = dropTarget?.id === folder.id;
|
|
167
|
+
const before = isDropTarget && dropTarget.before;
|
|
168
|
+
const after = isDropTarget && !dropTarget.before;
|
|
169
|
+
return (
|
|
170
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: drag-and-drop handle wraps the row
|
|
171
|
+
<div
|
|
172
|
+
key={folder.id}
|
|
173
|
+
className={cn(
|
|
174
|
+
'relative',
|
|
175
|
+
before &&
|
|
176
|
+
'before:absolute before:inset-x-2 before:-top-px before:h-[2px] before:rounded-full before:bg-brand',
|
|
177
|
+
after &&
|
|
178
|
+
'after:absolute after:inset-x-2 after:-bottom-px after:h-[2px] after:rounded-full after:bg-brand',
|
|
179
|
+
dragId === folder.id && 'opacity-50',
|
|
180
|
+
)}
|
|
181
|
+
draggable={import.meta.env.DEV}
|
|
182
|
+
onDragStart={(e) => {
|
|
183
|
+
if (!import.meta.env.DEV) return;
|
|
184
|
+
e.dataTransfer.setData(FOLDER_DND_MIME, folder.id);
|
|
185
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
186
|
+
setDragId(folder.id);
|
|
187
|
+
}}
|
|
188
|
+
onDragEnd={() => {
|
|
189
|
+
setDragId(null);
|
|
190
|
+
setDropTarget(null);
|
|
191
|
+
}}
|
|
192
|
+
onDragOver={(e) => {
|
|
193
|
+
if (!e.dataTransfer.types.includes(FOLDER_DND_MIME)) return;
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
e.dataTransfer.dropEffect = 'move';
|
|
196
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
197
|
+
const isBefore = e.clientY < rect.top + rect.height / 2;
|
|
198
|
+
if (!dropTarget || dropTarget.id !== folder.id || dropTarget.before !== isBefore) {
|
|
199
|
+
setDropTarget({ id: folder.id, before: isBefore });
|
|
200
|
+
}
|
|
201
|
+
}}
|
|
202
|
+
onDragLeave={(e) => {
|
|
203
|
+
if (e.currentTarget.contains(e.relatedTarget as Node | null)) return;
|
|
204
|
+
if (dropTarget?.id === folder.id) setDropTarget(null);
|
|
205
|
+
}}
|
|
206
|
+
onDrop={(e) => {
|
|
207
|
+
const fromId = e.dataTransfer.getData(FOLDER_DND_MIME);
|
|
208
|
+
if (!fromId) return;
|
|
209
|
+
e.preventDefault();
|
|
210
|
+
e.stopPropagation();
|
|
211
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
212
|
+
const isBefore = e.clientY < rect.top + rect.height / 2;
|
|
213
|
+
finishReorder(folder.id, isBefore);
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
<FolderItem
|
|
217
|
+
row={{
|
|
218
|
+
kind: 'folder',
|
|
219
|
+
folder,
|
|
220
|
+
onRename: (name) => onRename(folder.id, name),
|
|
221
|
+
onChangeIcon: (icon) => onChangeIcon(folder.id, icon),
|
|
222
|
+
onDelete: () => onDelete(folder.id),
|
|
223
|
+
}}
|
|
224
|
+
count={countFor(folder.id)}
|
|
225
|
+
selected={selectedId === folder.id}
|
|
226
|
+
onSelect={() => onSelect(folder.id)}
|
|
227
|
+
onDropSlide={(slideId) => onDropToFolder(folder.id, slideId)}
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
})}
|
|
158
232
|
|
|
159
233
|
{import.meta.env.DEV &&
|
|
160
234
|
(creating ? (
|
|
@@ -200,6 +274,10 @@ export function Sidebar({
|
|
|
200
274
|
</button>
|
|
201
275
|
))}
|
|
202
276
|
</div>
|
|
277
|
+
|
|
278
|
+
<div className="border-t border-hairline">
|
|
279
|
+
<SidebarFooter />
|
|
280
|
+
</div>
|
|
203
281
|
</aside>
|
|
204
282
|
);
|
|
205
283
|
}
|
|
@@ -7,7 +7,7 @@ const SERIF_GEORGIA = 'Georgia, "Times New Roman", serif';
|
|
|
7
7
|
const SERIF_TIMES = '"Times New Roman", Times, serif';
|
|
8
8
|
const MONO_SF = '"SF Mono", "JetBrains Mono", Menlo, monospace';
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const designPresets: DesignSystem[] = [
|
|
11
11
|
defaultDesign,
|
|
12
12
|
{
|
|
13
13
|
palette: { bg: '#0f1115', text: '#f5f3ee', accent: '#7cc4ff' },
|