@autono/open-pages 0.6.0 → 0.7.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 (61) hide show
  1. package/dist/{build-6hM9Ga2d.js → build-CCoxDxL-.js} +11 -10
  2. package/dist/cli/bin.d.ts +1 -1
  3. package/dist/cli/bin.js +427 -30
  4. package/dist/{config-CBbwLHOo.js → config-D_U_aCQz.js} +484 -460
  5. package/dist/{config-C-sasGou.d.ts → config-p-TWNXbv.d.ts} +4 -4
  6. package/dist/{design-C13iz9_4.js → design-Cpqx7SJs.js} +1 -2
  7. package/dist/{dev-BO0cHiYy.js → dev-CTrB263u.js} +40 -20
  8. package/dist/{export-DbVwSmKW.js → export-enKL9R7x.js} +7 -10
  9. package/dist/index.d.ts +6 -8
  10. package/dist/index.js +2 -3
  11. package/dist/locale/index.d.ts +8 -14
  12. package/dist/locale/index.js +67 -32
  13. package/dist/{open-pages-plugin-C0poXLsx.js → open-pages-plugin-C8cFhMB9.js} +23 -38
  14. package/dist/preview-Dmzyd1cf.js +24 -0
  15. package/dist/{sync-ui-CDydLpY4.js → sync-ui-BzRiAGCK.js} +3 -5
  16. package/dist/{types-BzxkFHhd.d.ts → types-DDdUFUIS.d.ts} +19 -6
  17. package/dist/{update-D1U8TVwd.js → update-CWaCKlnl.js} +4 -7
  18. package/dist/{update-package-D9zws9nc.js → update-package-GaNsVdr8.js} +44 -22
  19. package/dist/{version-B6AiiGvT.js → version-DZrXLoAD.js} +19 -3
  20. package/dist/vite/index.d.ts +3 -7
  21. package/dist/vite/index.js +2 -7
  22. package/package.json +22 -21
  23. package/src/app/app.tsx +19 -14
  24. package/src/app/assets/autono-social.png +0 -0
  25. package/src/app/assets/autono.svg +7 -0
  26. package/src/app/components/asset-view.tsx +510 -26
  27. package/src/app/components/command/command-menu.tsx +5 -1
  28. package/src/app/components/command/command.tsx +4 -2
  29. package/src/app/components/command/home-command-menu.tsx +6 -6
  30. package/src/app/components/icon-tooltip.tsx +24 -15
  31. package/src/app/components/sidebar/folder-item.tsx +46 -19
  32. package/src/app/components/sidebar/icon-picker.tsx +1 -1
  33. package/src/app/components/sidebar/sidebar-footer.tsx +18 -7
  34. package/src/app/components/sidebar/sidebar.tsx +91 -64
  35. package/src/app/components/theme-toggle.tsx +4 -5
  36. package/src/app/components/themes/theme-detail.tsx +6 -3
  37. package/src/app/components/themes/themes-gallery.tsx +12 -7
  38. package/src/app/index.html +33 -2
  39. package/src/app/lib/assets.ts +33 -0
  40. package/src/app/lib/last-home-location.ts +18 -0
  41. package/src/app/main.tsx +10 -1
  42. package/src/app/routes/home-shell.tsx +82 -61
  43. package/src/app/routes/home.tsx +42 -39
  44. package/src/app/routes/page.tsx +18 -3
  45. package/src/app/routes/themes.tsx +6 -8
  46. package/src/app/styles.css +40 -4
  47. package/src/app/virtual.d.ts +1 -0
  48. package/src/locale/en.ts +17 -6
  49. package/src/locale/ja.ts +18 -6
  50. package/src/locale/types.ts +17 -4
  51. package/src/locale/zh-cn.ts +19 -8
  52. package/src/locale/zh-tw.ts +17 -6
  53. package/src/shared/update-package.ts +193 -0
  54. package/workspace/deps.json +1 -1
  55. package/dist/export-BsK6OCpc.js +0 -4
  56. package/dist/preview-sOKhzyWB.js +0 -21
  57. package/dist/sync-Be_hxSOF.js +0 -139
  58. package/dist/sync-lKEXyPOh.js +0 -3
  59. package/dist/version-CeffgdfZ.js +0 -3
  60. package/src/app/components/language-toggle.tsx +0 -46
  61. package/src/app/favicon.ico +0 -0
@@ -31,7 +31,9 @@ function CommandDialog({
31
31
  <Dialog open={open} onOpenChange={onOpenChange}>
32
32
  <DialogContent
33
33
  showCloseButton={false}
34
- className="top-[14vh] translate-y-0 gap-0 overflow-hidden p-0 sm:max-w-[560px]"
34
+ // ⌘K fires dozens of times a day — opacity-only and fast, no scale
35
+ // theater on a keyboard-triggered surface.
36
+ className="top-[14vh] translate-y-0 gap-0 overflow-hidden p-0 duration-100 data-starting-style:scale-100 data-ending-style:scale-100 sm:max-w-[560px]"
35
37
  >
36
38
  <DialogTitle className="sr-only">{title}</DialogTitle>
37
39
  <DialogDescription className="sr-only">{description}</DialogDescription>
@@ -107,7 +109,7 @@ function CommandItem({ className, ...props }: React.ComponentProps<typeof Comman
107
109
  data-slot="command-item"
108
110
  className={cn(
109
111
  'relative flex cursor-default select-none items-center gap-2 rounded-[5px] px-2 py-1.5 text-[12.5px] outline-none',
110
- 'data-[selected=true]:bg-muted data-[selected=true]:text-foreground',
112
+ 'data-[selected=true]:bg-muted data-[selected=true]:text-foreground active:bg-muted/80',
111
113
  'data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-45',
112
114
  "[&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:text-muted-foreground [&_svg:not([class*='opacity'])]:opacity-80",
113
115
  className,
@@ -1,11 +1,11 @@
1
- import { Image as ImageIcon, Palette, Presentation } from 'lucide-react';
1
+ import { Presentation } from 'lucide-react';
2
2
  import { useMemo } from 'react';
3
3
  import { useNavigate } from 'react-router-dom';
4
4
  import { useLocale } from '~/lib/use-locale';
5
5
  import { usePageTitles } from '~/lib/use-page-titles';
6
6
  import { pageIds } from '../../lib/pages';
7
7
  import type { Folder } from '../../lib/sdk';
8
- import { FolderIconChip } from '../sidebar/folder-item';
8
+ import { FolderIconChip, SystemViewIcon } from '../sidebar/folder-item';
9
9
  import { ALL_DOCS_ID, ASSETS_ID, DRAFT_ID, THEMES_ID } from '../sidebar/sidebar';
10
10
  import { type CommandGroupSpec, CommandMenu, type CommandSpec } from './command-menu';
11
11
 
@@ -39,14 +39,14 @@ export function HomeCommandMenu({
39
39
  {
40
40
  id: `view-${ALL_DOCS_ID}`,
41
41
  label: t.home.pages,
42
- icon: <FolderIconChip icon={{ type: 'emoji', value: '🎞️' }} />,
42
+ icon: <SystemViewIcon kind="all" />,
43
43
  keywords: ['all', 'pages'],
44
44
  run: () => onSelectView(ALL_DOCS_ID),
45
45
  },
46
46
  {
47
47
  id: `view-${DRAFT_ID}`,
48
48
  label: t.home.draft,
49
- icon: <FolderIconChip icon={{ type: 'emoji', value: '📝' }} />,
49
+ icon: <SystemViewIcon kind="draft" />,
50
50
  keywords: ['draft', 'unsorted'],
51
51
  run: () => onSelectView(DRAFT_ID),
52
52
  },
@@ -63,7 +63,7 @@ export function HomeCommandMenu({
63
63
  {
64
64
  id: `view-${THEMES_ID}`,
65
65
  label: t.home.themes,
66
- icon: <Palette />,
66
+ icon: <SystemViewIcon kind="themes" />,
67
67
  keywords: ['themes', 'design'],
68
68
  run: () => onSelectView(THEMES_ID),
69
69
  },
@@ -72,7 +72,7 @@ export function HomeCommandMenu({
72
72
  navigation.push({
73
73
  id: `view-${ASSETS_ID}`,
74
74
  label: t.home.assets,
75
- icon: <ImageIcon />,
75
+ icon: <SystemViewIcon kind="assets" />,
76
76
  keywords: ['assets', 'images', 'files'],
77
77
  run: () => onSelectView(ASSETS_ID),
78
78
  });
@@ -1,10 +1,19 @@
1
+ import { Tooltip as TooltipPrimitive } from '@base-ui/react/tooltip';
1
2
  import type { ReactNode } from 'react';
2
- import { Tooltip, TooltipContent, TooltipProvider } from '~/components/ui/tooltip';
3
+ import { Tooltip, TooltipContent } from '~/components/ui/tooltip';
4
+
5
+ /**
6
+ * Tooltip trigger for a button that also opens a menu. The `ui/tooltip` wrapper
7
+ * is a plain function component, so on React 18 it swallows the ref that
8
+ * `DropdownMenuTrigger` hands to its `render` element and the menu never opens —
9
+ * the primitive forwards it.
10
+ */
11
+ export const MenuTooltipTrigger = TooltipPrimitive.Trigger;
3
12
 
4
13
  /**
5
14
  * Shared hover label for the toolbar icon buttons. `children` supplies the
6
15
  * `TooltipTrigger`; a button that also opens a menu has to compose it inward
7
- * (`<DropdownMenuTrigger render={<TooltipTrigger />}>`) — the other nesting
16
+ * (`<DropdownMenuTrigger render={<MenuTooltipTrigger />}>`) — the other nesting
8
17
  * order swallows the trigger ref and the tooltip never opens.
9
18
  */
10
19
  export function IconTooltip({
@@ -16,19 +25,19 @@ export function IconTooltip({
16
25
  shortcut?: string;
17
26
  children: ReactNode;
18
27
  }) {
28
+ // No local provider — tooltips join the app-level group so adjacent
29
+ // toolbar buttons open instantly once one tooltip is showing.
19
30
  return (
20
- <TooltipProvider delay={200}>
21
- <Tooltip>
22
- {children}
23
- <TooltipContent side="bottom" sideOffset={6} className="flex items-center gap-1.5">
24
- {label}
25
- {shortcut && (
26
- <kbd className="rounded-[3px] bg-background/18 px-1 py-0.5 font-mono text-[9.5px] tracking-[0.04em] text-background/85">
27
- {shortcut}
28
- </kbd>
29
- )}
30
- </TooltipContent>
31
- </Tooltip>
32
- </TooltipProvider>
31
+ <Tooltip>
32
+ {children}
33
+ <TooltipContent side="bottom" sideOffset={6} className="flex items-center gap-1.5">
34
+ {label}
35
+ {shortcut && (
36
+ <kbd className="rounded-[3px] bg-background/18 px-1 py-0.5 font-mono text-[9.5px] tracking-[0.04em] text-background/85">
37
+ {shortcut}
38
+ </kbd>
39
+ )}
40
+ </TooltipContent>
41
+ </Tooltip>
33
42
  );
34
43
  }
@@ -1,4 +1,13 @@
1
- import { MoreHorizontal, Pencil, Trash2 } from 'lucide-react';
1
+ import {
2
+ FolderOpen,
3
+ LayoutGrid,
4
+ type LucideIcon,
5
+ MoreHorizontal,
6
+ Palette,
7
+ Pencil,
8
+ PenLine,
9
+ Trash2,
10
+ } from 'lucide-react';
2
11
  import { useEffect, useRef, useState } from 'react';
3
12
  import {
4
13
  DropdownMenu,
@@ -33,6 +42,24 @@ function useDocDragActive() {
33
42
  return active;
34
43
  }
35
44
 
45
+ export type SystemViewKind = 'all' | 'draft' | 'themes' | 'assets';
46
+
47
+ const SYSTEM_VIEW_ICONS: Record<SystemViewKind, LucideIcon> = {
48
+ all: LayoutGrid,
49
+ draft: PenLine,
50
+ themes: Palette,
51
+ assets: FolderOpen,
52
+ };
53
+
54
+ export function SystemViewIcon({ kind, className }: { kind: SystemViewKind; className?: string }) {
55
+ const Icon = SYSTEM_VIEW_ICONS[kind];
56
+ return (
57
+ <span aria-hidden className={cn('flex size-5 shrink-0 items-center justify-center', className)}>
58
+ <Icon className="size-4" strokeWidth={1.75} />
59
+ </span>
60
+ );
61
+ }
62
+
36
63
  export function FolderIconChip({ icon, className }: { icon: FolderIcon; className?: string }) {
37
64
  if (icon.type === 'emoji') {
38
65
  return (
@@ -126,16 +153,12 @@ export function FolderItem({
126
153
  onDropDoc(pageId);
127
154
  };
128
155
 
129
- const icon: FolderIcon =
130
- row.kind === 'all'
131
- ? { type: 'emoji', value: '🎞️' }
132
- : row.kind === 'draft'
133
- ? { type: 'emoji', value: '📝' }
134
- : row.kind === 'themes'
135
- ? { type: 'emoji', value: '🎨' }
136
- : row.kind === 'assets'
137
- ? { type: 'emoji', value: '🗂️' }
138
- : row.folder.icon;
156
+ const chip =
157
+ row.kind === 'folder' ? (
158
+ <FolderIconChip icon={row.folder.icon} />
159
+ ) : (
160
+ <SystemViewIcon kind={row.kind} className={cn(!selected && 'text-muted-foreground')} />
161
+ );
139
162
  const label =
140
163
  row.kind === 'all'
141
164
  ? t.home.pages
@@ -158,13 +181,13 @@ export function FolderItem({
158
181
  // biome-ignore lint/a11y/noStaticElementInteractions: drag-and-drop target wraps interactive children
159
182
  <div
160
183
  className={cn(
161
- 'group relative flex items-center gap-2.5 rounded-[5px] px-2 py-[5px] text-[12.5px] transition-colors',
184
+ 'group relative flex items-center gap-2.5 rounded-[5px] px-2 py-[5px] text-[12.5px] transition-[background-color,color,scale] duration-150',
162
185
  selected
163
- ? 'bg-muted text-foreground before:absolute before:inset-y-1.5 before:-left-0.5 before:w-[2px] before:rounded-full before:bg-brand'
186
+ ? 'bg-background font-medium text-foreground shadow-edge ring-1 ring-foreground/[0.06]'
164
187
  : 'text-foreground/70 hover:bg-muted/60 hover:text-foreground',
165
188
  docDragActive && acceptsDocDrop && !dragOver && 'ring-1 ring-foreground/10',
166
189
  dragOver &&
167
- 'bg-brand/10 text-foreground ring-1 ring-brand ring-offset-1 ring-offset-sidebar motion-safe:scale-[1.01] motion-safe:transition-transform',
190
+ 'bg-brand/10 text-foreground ring-1 ring-brand ring-offset-1 ring-offset-sidebar motion-safe:scale-[1.01]',
168
191
  )}
169
192
  onDragEnter={handleDragEnter}
170
193
  onDragOver={handleDragOver}
@@ -177,11 +200,11 @@ export function FolderItem({
177
200
  render={
178
201
  <button
179
202
  type="button"
180
- className="flex size-5 shrink-0 items-center justify-center rounded transition-transform hover:scale-110"
203
+ className="flex size-5 shrink-0 items-center justify-center rounded outline-none motion-safe:transition-transform motion-safe:duration-150 hover:scale-110 active:scale-95 focus-visible:ring-1 focus-visible:ring-brand"
181
204
  aria-label={t.home.changeIcon}
182
205
  onClick={(e) => e.stopPropagation()}
183
206
  >
184
- <FolderIconChip icon={icon} />
207
+ {chip}
185
208
  </button>
186
209
  }
187
210
  />
@@ -196,7 +219,7 @@ export function FolderItem({
196
219
  aria-label={label}
197
220
  className="flex size-5 shrink-0 items-center justify-center"
198
221
  >
199
- <FolderIconChip icon={icon} />
222
+ {chip}
200
223
  </button>
201
224
  )}
202
225
 
@@ -217,7 +240,11 @@ export function FolderItem({
217
240
  className="min-w-0 flex-1 rounded-[3px] bg-card px-1 text-[12.5px] outline-none ring-1 ring-foreground/20"
218
241
  />
219
242
  ) : (
220
- <button type="button" onClick={onSelect} className="min-w-0 flex-1 truncate text-left">
243
+ <button
244
+ type="button"
245
+ onClick={onSelect}
246
+ className="min-w-0 flex-1 truncate rounded-[3px] text-left outline-none focus-visible:ring-1 focus-visible:ring-brand"
247
+ >
221
248
  {label}
222
249
  </button>
223
250
  )}
@@ -240,7 +267,7 @@ export function FolderItem({
240
267
  <button
241
268
  type="button"
242
269
  onClick={(e) => e.stopPropagation()}
243
- className="absolute right-2 top-1/2 size-5 -translate-y-1/2 rounded opacity-0 transition-opacity hover:bg-foreground/10 group-hover:opacity-100 aria-expanded:opacity-100"
270
+ className="absolute right-2 top-1/2 size-5 -translate-y-1/2 rounded opacity-0 outline-none transition-opacity duration-150 hover:bg-foreground/10 group-hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-1 focus-visible:ring-brand aria-expanded:opacity-100"
244
271
  aria-label={t.home.folderActions}
245
272
  >
246
273
  <MoreHorizontal className="mx-auto size-3.5" />
@@ -49,7 +49,7 @@ export function IconPicker({
49
49
  key={c}
50
50
  type="button"
51
51
  onClick={() => onChange({ type: 'color', value: c })}
52
- className="size-6 rounded-[4px] ring-1 ring-foreground/10 shadow-[inset_0_1px_0_oklch(1_0_0/0.18)] transition-transform hover:scale-110"
52
+ className="size-6 rounded-[4px] ring-1 ring-foreground/10 shadow-[inset_0_1px_0_oklch(1_0_0/0.18)] outline-none motion-safe:transition-transform motion-safe:duration-100 hover:scale-110 active:scale-95 focus-visible:ring-2 focus-visible:ring-brand"
53
53
  style={{ background: c }}
54
54
  aria-label={c}
55
55
  />
@@ -2,6 +2,7 @@ import config from 'virtual:open-pages/config';
2
2
  import { Loader2, RefreshCw, RotateCw } from 'lucide-react';
3
3
  import { useEffect, useState } from 'react';
4
4
  import { toast } from 'sonner';
5
+ import { ThemeToggle } from '~/components/theme-toggle';
5
6
  import { Button } from '~/components/ui/button';
6
7
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '~/components/ui/tooltip';
7
8
  import { format, useLocale } from '~/lib/use-locale';
@@ -53,15 +54,22 @@ export function SidebarFooter() {
53
54
  }
54
55
  }
55
56
 
56
- const versionRow = (
57
- <span className="inline-flex cursor-default items-center gap-1.5">
58
- {update?.latest && <span className="size-1.5 rounded-full bg-brand" aria-hidden />}
57
+ const versionRow = update?.latest ? (
58
+ // Real button: the tooltip holds actionable controls, so keyboard users
59
+ // must be able to reach and open it.
60
+ <button
61
+ type="button"
62
+ className="inline-flex cursor-default items-center gap-1.5 rounded-[3px] outline-none focus-visible:ring-1 focus-visible:ring-brand"
63
+ >
64
+ <span className="size-1.5 rounded-full bg-brand" aria-hidden />
59
65
  {label}
60
- </span>
66
+ </button>
67
+ ) : (
68
+ <span className="inline-flex cursor-default items-center gap-1.5">{label}</span>
61
69
  );
62
70
 
63
71
  return (
64
- <div className="px-4 py-3 text-[11px] text-muted-foreground/70 tabular-nums">
72
+ <div className="flex items-center justify-between gap-2 py-1.5 pr-2 pl-4 text-[11px] text-muted-foreground/70 tabular-nums">
65
73
  {update?.latest ? (
66
74
  <TooltipProvider delay={200}>
67
75
  <Tooltip
@@ -93,7 +101,7 @@ export function SidebarFooter() {
93
101
  onClick={restartServer}
94
102
  >
95
103
  {restarting ? (
96
- <Loader2 className="animate-spin" aria-hidden />
104
+ <Loader2 className="animate-spin motion-reduce:animate-none" aria-hidden />
97
105
  ) : (
98
106
  <RotateCw aria-hidden />
99
107
  )}
@@ -115,7 +123,7 @@ export function SidebarFooter() {
115
123
  onClick={updatePackage}
116
124
  >
117
125
  {isUpdating ? (
118
- <Loader2 className="animate-spin" aria-hidden />
126
+ <Loader2 className="animate-spin motion-reduce:animate-none" aria-hidden />
119
127
  ) : (
120
128
  <RefreshCw aria-hidden />
121
129
  )}
@@ -134,6 +142,9 @@ export function SidebarFooter() {
134
142
  ) : (
135
143
  versionRow
136
144
  )}
145
+ <div className="flex shrink-0 items-center">
146
+ <ThemeToggle />
147
+ </div>
137
148
  </div>
138
149
  );
139
150
  }
@@ -1,13 +1,12 @@
1
- import { Plus } from 'lucide-react';
1
+ import { Plus, Search } from 'lucide-react';
2
2
  import { useEffect, useRef, useState } from 'react';
3
3
  import { toast } from 'sonner';
4
- import { LanguageToggle } from '~/components/language-toggle';
5
- import { ThemeToggle } from '~/components/theme-toggle';
4
+ import logo from '~/assets/autono.svg';
6
5
  import { Popover, PopoverContent, PopoverTrigger } from '~/components/ui/popover';
7
6
  import type { Folder, FolderIcon } from '~/lib/sdk';
8
7
  import { format, useLocale } from '~/lib/use-locale';
9
8
  import { cn } from '~/lib/utils';
10
- import { CommandMenuTrigger } from '../command/command-menu';
9
+ import { COMMAND_MENU_SHORTCUT } from '../command/command-menu';
11
10
  import { FolderIconChip, FolderItem } from './folder-item';
12
11
  import { IconPicker, PRESET_COLORS } from './icon-picker';
13
12
  import { SidebarFooter } from './sidebar-footer';
@@ -129,17 +128,42 @@ export function Sidebar({
129
128
  }, [creating]);
130
129
 
131
130
  return (
132
- <aside className="relative flex h-full w-[16.5rem] shrink-0 flex-col border-r border-hairline bg-sidebar text-sidebar-foreground">
133
- <div className="flex items-center justify-between px-4 pt-5 pb-4">
134
- <h1 className="font-heading text-lg font-bold tracking-tight">{t.home.appTitle}</h1>
135
- <div className="-mr-1.5 flex items-center">
136
- <CommandMenuTrigger onClick={onOpenCommandMenu} />
137
- <LanguageToggle />
138
- <ThemeToggle />
139
- </div>
131
+ <aside className="group/side relative flex h-full w-[16.5rem] shrink-0 flex-col bg-sidebar text-sidebar-foreground">
132
+ <div className="flex items-center gap-2.5 px-4 pt-4 pb-3">
133
+ <img
134
+ src={logo}
135
+ alt=""
136
+ aria-hidden
137
+ draggable={false}
138
+ className="size-6 shrink-0 select-none rounded-[6px] ring-1 ring-foreground/10"
139
+ />
140
+ <h1 className="font-heading text-[13.5px] font-semibold tracking-tight">
141
+ {t.home.appTitle}
142
+ </h1>
143
+ </div>
144
+
145
+ <div className="px-2 pb-3">
146
+ <button
147
+ type="button"
148
+ onClick={onOpenCommandMenu}
149
+ aria-label={t.commandMenu.triggerAria}
150
+ aria-haspopup="dialog"
151
+ className={cn(
152
+ 'flex h-8 w-full items-center gap-2 rounded-[6px] border border-border bg-card px-2.5 text-[12.5px] text-muted-foreground shadow-edge outline-none',
153
+ 'transition-[border-color,color,background-color] duration-100',
154
+ 'hover:border-foreground/20 hover:text-foreground',
155
+ 'focus-visible:border-foreground/30 focus-visible:ring-2 focus-visible:ring-ring/30',
156
+ )}
157
+ >
158
+ <Search className="size-3.5 shrink-0" aria-hidden />
159
+ <span className="flex-1 truncate text-left">{t.commandMenu.triggerTooltip}</span>
160
+ <kbd className="rounded-[4px] border border-hairline bg-muted/60 px-1 py-px font-mono text-[9.5px] leading-none tracking-[0.04em] text-muted-foreground">
161
+ {COMMAND_MENU_SHORTCUT}
162
+ </kbd>
163
+ </button>
140
164
  </div>
141
165
 
142
- <div className="px-2">
166
+ <div className="space-y-0.5 px-2">
143
167
  <FolderItem
144
168
  row={{ kind: 'all' }}
145
169
  count={allCount}
@@ -165,12 +189,27 @@ export function Sidebar({
165
189
  )}
166
190
  </div>
167
191
 
168
- <div className="mt-5 flex items-center gap-2 px-4 pb-1.5">
192
+ <div className="mt-5 flex h-6 items-center justify-between pr-2.5 pl-4 pb-0.5">
169
193
  <span className="eyebrow">{t.home.folders}</span>
170
- <span className="h-px flex-1 bg-hairline" aria-hidden />
194
+ {import.meta.env.DEV && (
195
+ <button
196
+ type="button"
197
+ onClick={startCreating}
198
+ aria-label={t.home.newFolder}
199
+ title={t.home.newFolder}
200
+ className={cn(
201
+ 'flex size-5 items-center justify-center rounded-[4px] text-muted-foreground/70 opacity-0 outline-none',
202
+ 'transition-[opacity,background-color,color,scale] duration-100',
203
+ 'hover:bg-muted hover:text-foreground active:scale-90',
204
+ 'group-hover/side:opacity-100 focus-visible:opacity-100 focus-visible:ring-1 focus-visible:ring-brand',
205
+ )}
206
+ >
207
+ <Plus className="size-3.5" />
208
+ </button>
209
+ )}
171
210
  </div>
172
211
 
173
- <div className="flex-1 overflow-y-auto px-2 pb-2">
212
+ <div className="flex-1 space-y-0.5 overflow-y-auto px-2 pb-2">
174
213
  <FolderItem
175
214
  row={{ kind: 'draft' }}
176
215
  count={countFor(null)}
@@ -246,57 +285,45 @@ export function Sidebar({
246
285
  );
247
286
  })}
248
287
 
249
- {import.meta.env.DEV &&
250
- (creating ? (
251
- <div
252
- data-folder-create
253
- className="mt-1 flex items-center gap-2.5 rounded-[5px] border border-dashed border-foreground/30 bg-card px-2 py-[5px]"
254
- >
255
- <Popover open={iconOpen} onOpenChange={setIconOpen}>
256
- <PopoverTrigger
257
- render={
258
- <button
259
- type="button"
260
- className="flex size-5 shrink-0 items-center justify-center rounded transition-transform hover:scale-110"
261
- aria-label={t.home.pickIcon}
262
- >
263
- <FolderIconChip icon={newIcon} />
264
- </button>
265
- }
266
- />
267
- <PopoverContent side="right" align="start" className="w-auto p-2">
268
- <IconPicker value={newIcon} onChange={setNewIcon} />
269
- </PopoverContent>
270
- </Popover>
271
- <input
272
- ref={inputRef}
273
- value={newName}
274
- onChange={(e) => setNewName(e.target.value)}
275
- onKeyDown={(e) => {
276
- if (e.nativeEvent.isComposing) return;
277
- if (e.key === 'Enter') commitCreate();
278
- if (e.key === 'Escape') exitCreate();
279
- }}
280
- placeholder={t.home.folderName}
281
- maxLength={40}
282
- className="min-w-0 flex-1 bg-transparent text-[12.5px] outline-none placeholder:text-muted-foreground/60"
288
+ {import.meta.env.DEV && creating && (
289
+ <div
290
+ data-folder-create
291
+ className="mt-1 flex items-center gap-2.5 rounded-[5px] border border-dashed border-foreground/30 bg-card px-2 py-[5px]"
292
+ >
293
+ <Popover open={iconOpen} onOpenChange={setIconOpen}>
294
+ <PopoverTrigger
295
+ render={
296
+ <button
297
+ type="button"
298
+ className="flex size-5 shrink-0 items-center justify-center rounded outline-none motion-safe:transition-transform motion-safe:duration-150 hover:scale-110 active:scale-95 focus-visible:ring-1 focus-visible:ring-brand"
299
+ aria-label={t.home.pickIcon}
300
+ >
301
+ <FolderIconChip icon={newIcon} />
302
+ </button>
303
+ }
283
304
  />
284
- </div>
285
- ) : (
286
- <button
287
- type="button"
288
- onClick={startCreating}
289
- className="mt-1 flex w-full items-center gap-2 rounded-[5px] px-2 py-1.5 text-[12px] text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground"
290
- >
291
- <Plus className="size-3.5" />
292
- <span>{t.home.newFolder}</span>
293
- </button>
294
- ))}
305
+ <PopoverContent side="right" align="start" className="w-auto p-2">
306
+ <IconPicker value={newIcon} onChange={setNewIcon} />
307
+ </PopoverContent>
308
+ </Popover>
309
+ <input
310
+ ref={inputRef}
311
+ value={newName}
312
+ onChange={(e) => setNewName(e.target.value)}
313
+ onKeyDown={(e) => {
314
+ if (e.nativeEvent.isComposing) return;
315
+ if (e.key === 'Enter') commitCreate();
316
+ if (e.key === 'Escape') exitCreate();
317
+ }}
318
+ placeholder={t.home.folderName}
319
+ maxLength={40}
320
+ className="min-w-0 flex-1 bg-transparent text-[12.5px] outline-none placeholder:text-muted-foreground/60"
321
+ />
322
+ </div>
323
+ )}
295
324
  </div>
296
325
 
297
- <div className="border-t border-hairline">
298
- <SidebarFooter />
299
- </div>
326
+ <SidebarFooter />
300
327
  </aside>
301
328
  );
302
329
  }
@@ -1,7 +1,7 @@
1
1
  import { Monitor, Moon, Sun } from 'lucide-react';
2
2
  import { useTheme } from 'next-themes';
3
3
  import { useEffect, useState } from 'react';
4
- import { IconTooltip } from '~/components/icon-tooltip';
4
+ import { IconTooltip, MenuTooltipTrigger } from '~/components/icon-tooltip';
5
5
  import { buttonVariants } from '~/components/ui/button';
6
6
  import {
7
7
  DropdownMenu,
@@ -9,7 +9,6 @@ import {
9
9
  DropdownMenuItem,
10
10
  DropdownMenuTrigger,
11
11
  } from '~/components/ui/dropdown-menu';
12
- import { TooltipTrigger } from '~/components/ui/tooltip';
13
12
  import { useLocale } from '~/lib/use-locale';
14
13
  import { cn } from '~/lib/utils';
15
14
 
@@ -27,15 +26,15 @@ export function ThemeToggle() {
27
26
  <IconTooltip label={t.themeToggle.title}>
28
27
  <DropdownMenuTrigger
29
28
  render={
30
- <TooltipTrigger
29
+ <MenuTooltipTrigger
31
30
  type="button"
32
31
  aria-label={t.themeToggle.toggleAria}
33
32
  className={cn(buttonVariants({ variant: 'ghost', size: 'icon-sm' }), 'relative')}
34
33
  />
35
34
  }
36
35
  >
37
- <Sun className="size-3.5 scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
38
- <Moon className="absolute size-3.5 scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
36
+ <Sun className="size-3.5 scale-100 rotate-0 opacity-100 ease-swift motion-safe:transition-[opacity,scale,rotate] motion-safe:duration-200 dark:scale-75 dark:-rotate-90 dark:opacity-0" />
37
+ <Moon className="absolute size-3.5 scale-75 rotate-90 opacity-0 ease-swift motion-safe:transition-[opacity,scale,rotate] motion-safe:duration-200 dark:scale-100 dark:rotate-0 dark:opacity-100" />
39
38
  </DropdownMenuTrigger>
40
39
  </IconTooltip>
41
40
  <DropdownMenuContent align="end" className="min-w-[140px]">
@@ -48,7 +48,7 @@ export function ThemeDetail({ themeId, onBack }: { themeId: string; onBack: () =
48
48
  </div>
49
49
 
50
50
  <header className="flex flex-wrap items-baseline gap-3">
51
- <h2 className="font-heading text-[26px] font-semibold leading-[1.05] tracking-[-0.025em] md:text-[32px]">
51
+ <h2 className="font-heading text-[19px] font-semibold leading-none tracking-[-0.015em] md:text-[21px]">
52
52
  {theme.name}
53
53
  </h2>
54
54
  {theme.description ? (
@@ -156,8 +156,11 @@ function ThemeDocCard({ id }: { id: string }) {
156
156
  const displayTitle = page?.meta?.title ?? id;
157
157
 
158
158
  return (
159
- <Link to={`/p/${id}`} className="group block focus-visible:outline-none">
160
- <div className="relative aspect-video overflow-hidden rounded-[6px] border border-hairline bg-card shadow-edge ring-1 ring-foreground/[0.04] group-hover:shadow-floating group-hover:ring-foreground/20 motion-safe:transition-[box-shadow,--tw-ring-color] motion-safe:duration-200">
159
+ <Link
160
+ to={`/p/${id}`}
161
+ className="group block rounded-[6px] outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-2 focus-visible:ring-offset-background"
162
+ >
163
+ <div className="relative aspect-video overflow-hidden rounded-[6px] border border-hairline bg-card shadow-edge ring-1 ring-foreground/[0.04] group-hover:shadow-floating group-hover:ring-foreground/20 group-active:scale-[0.99] motion-safe:transition-[box-shadow,--tw-ring-color,scale] motion-safe:duration-200">
161
164
  <div className="h-full w-full motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-[1.03]">
162
165
  <PageThumb source={{ pageId: id, kind: pageKinds[id] }} title={displayTitle} />
163
166
  </div>
@@ -1,3 +1,4 @@
1
+ import { Palette } from 'lucide-react';
1
2
  import { format, useLocale } from '~/lib/use-locale';
2
3
  import { PageThumb } from '../../lib/page-thumb';
3
4
  import { type Theme, themes } from '../../lib/themes';
@@ -11,8 +12,12 @@ export function ThemesGallery({ onOpen }: { onOpen: (id: string) => void }) {
11
12
 
12
13
  return (
13
14
  <ul className="grid grid-cols-[repeat(auto-fill,minmax(min(240px,100%),1fr))] gap-x-6 gap-y-9 md:grid-cols-[repeat(auto-fill,minmax(340px,1fr))]">
14
- {themes.map((theme) => (
15
- <li key={theme.id}>
15
+ {themes.map((theme, i) => (
16
+ <li
17
+ key={theme.id}
18
+ className="rise-in"
19
+ style={{ animationDelay: `${Math.min(i, 11) * 30}ms` }}
20
+ >
16
21
  <ThemeCard
17
22
  theme={theme}
18
23
  onOpen={() => onOpen(theme.id)}
@@ -38,9 +43,9 @@ function ThemeCard({
38
43
  type="button"
39
44
  onClick={onOpen}
40
45
  aria-label={ariaLabel}
41
- className="group block w-full text-left focus-visible:outline-none"
46
+ className="group block w-full rounded-[6px] text-left outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-2 focus-visible:ring-offset-background"
42
47
  >
43
- <div className="relative aspect-video overflow-hidden rounded-[6px] border border-hairline bg-card shadow-edge ring-1 ring-foreground/[0.04] group-hover:shadow-floating group-hover:ring-foreground/20 motion-safe:transition-[box-shadow,--tw-ring-color] motion-safe:duration-200">
48
+ <div className="relative aspect-video overflow-hidden rounded-[6px] border border-hairline bg-card shadow-edge ring-1 ring-foreground/[0.04] group-hover:shadow-floating group-hover:ring-foreground/20 group-active:scale-[0.99] motion-safe:transition-[box-shadow,--tw-ring-color,scale] motion-safe:duration-200">
44
49
  <ThemePreview theme={theme} />
45
50
  </div>
46
51
  <div className="mt-3">
@@ -85,10 +90,10 @@ function NoDemoState() {
85
90
  function ThemesEmptyState() {
86
91
  const t = useLocale();
87
92
  return (
88
- <div className="rounded-[10px] border border-dashed border-border bg-card/60 px-8 py-20">
93
+ <div className="rounded-[8px] border border-dashed border-border px-8 py-20">
89
94
  <div className="mx-auto flex max-w-md flex-col items-center text-center">
90
- <div className="text-2xl">🎨</div>
91
- <p className="mt-3 font-heading text-[15px] font-semibold tracking-tight">
95
+ <Palette className="size-5 text-muted-foreground/60" aria-hidden />
96
+ <p className="mt-4 font-heading text-[14px] font-semibold tracking-tight">
92
97
  {t.themes.noThemesTitle}
93
98
  </p>
94
99
  <p className="mt-1.5 text-[13px] leading-relaxed text-muted-foreground">