@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.
Files changed (43) hide show
  1. package/dist/{build-tLrkKUHr.js → build-ZM7IfDO-.js} +1 -1
  2. package/dist/cli/bin.js +3 -3
  3. package/dist/{config-PwUHqZ_X.js → config-BAZeaz2P.js} +289 -246
  4. package/dist/{config-CfMThYN9.d.ts → config-D_5nlXFU.d.ts} +6 -1
  5. package/dist/{dev-DpCIRbhT.js → dev-BQkNTG_t.js} +1 -1
  6. package/dist/format-CYOb2cAQ.js +1573 -0
  7. package/dist/index.d.ts +4 -4
  8. package/dist/index.js +38 -4
  9. package/dist/locale/index.d.ts +1 -1
  10. package/dist/locale/index.js +1 -1144
  11. package/dist/{preview-BSGlM6Se.js → preview-D8hUtbRA.js} +1 -1
  12. package/dist/{types-B-KrjgX8.d.ts → types-AalTbxMj.d.ts} +17 -3
  13. package/dist/vite/index.d.ts +2 -2
  14. package/dist/vite/index.js +1 -1
  15. package/package.json +2 -1
  16. package/skills/create-theme/SKILL.md +1 -1
  17. package/src/app/components/inspector/comment-widget.tsx +16 -2
  18. package/src/app/components/language-toggle.tsx +39 -0
  19. package/src/app/components/player.tsx +12 -17
  20. package/src/app/components/pptx-progress-toast.tsx +32 -0
  21. package/src/app/components/sidebar/folder-item.tsx +7 -2
  22. package/src/app/components/sidebar/sidebar-footer.tsx +51 -0
  23. package/src/app/components/sidebar/sidebar.tsx +95 -17
  24. package/src/app/lib/design-presets.ts +1 -1
  25. package/src/app/lib/export-pptx.ts +284 -0
  26. package/src/app/lib/folders.ts +28 -0
  27. package/src/app/lib/inspector/fiber.test.ts +154 -0
  28. package/src/app/lib/inspector/fiber.ts +12 -1
  29. package/src/app/lib/locale-store.ts +67 -0
  30. package/src/app/lib/use-click-page-navigation.ts +52 -0
  31. package/src/app/lib/use-is-mobile.ts +21 -0
  32. package/src/app/lib/use-locale.ts +4 -16
  33. package/src/app/routes/home-shell.tsx +8 -0
  34. package/src/app/routes/home.tsx +1 -1
  35. package/src/app/routes/slide.tsx +145 -53
  36. package/src/app/virtual.d.ts +1 -0
  37. package/src/locale/en.ts +18 -3
  38. package/src/locale/ja.ts +19 -3
  39. package/src/locale/types.ts +18 -3
  40. package/src/locale/zh-cn.ts +17 -3
  41. package/src/locale/zh-tw.ts +17 -3
  42. package/dist/en-BDnM5zKJ.js +0 -378
  43. package/src/app/components/click-nav-zones.tsx +0 -36
@@ -1,5 +1,5 @@
1
1
  import "./design-cpzS8aud.js";
2
- import { createViteConfig } from "./config-PwUHqZ_X.js";
2
+ import { createViteConfig } from "./config-BAZeaz2P.js";
3
3
  import { mergeConfig, preview as preview$1 } from "vite";
4
4
 
5
5
  //#region src/cli/preview.ts
@@ -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
- clickNav: {
369
- prevAria: string;
370
- nextAria: string;
382
+ languageToggle: {
383
+ toggleAria: string;
384
+ title: string;
371
385
  };
372
386
  imagePlaceholder: {
373
387
  dropOverlay: string;
@@ -1,5 +1,5 @@
1
- import "../types-B-KrjgX8.js";
2
- import { OpenSlideConfig } from "../config-CfMThYN9.js";
1
+ import "../types-AalTbxMj.js";
2
+ import { OpenSlideConfig } from "../config-D_5nlXFU.js";
3
3
  import { InlineConfig } from "vite";
4
4
 
5
5
  //#region src/vite/config.d.ts
@@ -1,4 +1,4 @@
1
1
  import "../design-cpzS8aud.js";
2
- import { createViteConfig } from "../config-PwUHqZ_X.js";
2
+ import { createViteConfig } from "../config-BAZeaz2P.js";
3
3
 
4
4
  export { createViteConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-slide/core",
3
- "version": "1.7.0",
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/`: `<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.
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 data-inspector-ui className="absolute right-4 bottom-4 z-20 flex flex-col items-end gap-2">
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
- <span className="flex size-5 shrink-0 items-center justify-center">
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
- </span>
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
- <FolderItem
144
- key={folder.id}
145
- row={{
146
- kind: 'folder',
147
- folder,
148
- onRename: (name) => onRename(folder.id, name),
149
- onChangeIcon: (icon) => onChangeIcon(folder.id, icon),
150
- onDelete: () => onDelete(folder.id),
151
- }}
152
- count={countFor(folder.id)}
153
- selected={selectedId === folder.id}
154
- onSelect={() => onSelect(folder.id)}
155
- onDropSlide={(slideId) => onDropToFolder(folder.id, slideId)}
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
- export const designPresets: DesignSystem[] = [
10
+ const designPresets: DesignSystem[] = [
11
11
  defaultDesign,
12
12
  {
13
13
  palette: { bg: '#0f1115', text: '#f5f3ee', accent: '#7cc4ff' },