@autono/open-pages 0.5.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 (125) hide show
  1. package/dist/{build-L50pau9v.js → build-CCoxDxL-.js} +11 -10
  2. package/dist/cli/bin.d.ts +1 -1
  3. package/dist/cli/bin.js +434 -28
  4. package/dist/{config-CDUdFE8N.js → config-D_U_aCQz.js} +484 -460
  5. package/dist/{config-C9or4m4J.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-CzJYXFOO.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-BzRiAGCK.js +132 -0
  16. package/dist/{types-C5NCdDT-.d.ts → types-DDdUFUIS.d.ts} +19 -6
  17. package/dist/{update-CV62NhBT.js → update-CWaCKlnl.js} +14 -5
  18. package/dist/{update-package-Cxt57pz7.js → update-package-GaNsVdr8.js} +50 -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 +24 -22
  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/components.json +25 -0
  55. package/workspace/deps.json +24 -0
  56. package/workspace/hooks/use-mobile.ts +19 -0
  57. package/workspace/lib/utils.ts +6 -0
  58. package/workspace/styles/globals.css +135 -0
  59. package/workspace/ui/accordion.tsx +79 -0
  60. package/workspace/ui/alert-dialog.tsx +197 -0
  61. package/workspace/ui/alert.tsx +76 -0
  62. package/workspace/ui/aspect-ratio.tsx +11 -0
  63. package/workspace/ui/attachment.tsx +204 -0
  64. package/workspace/ui/avatar.tsx +110 -0
  65. package/workspace/ui/badge.tsx +49 -0
  66. package/workspace/ui/breadcrumb.tsx +122 -0
  67. package/workspace/ui/bubble.tsx +125 -0
  68. package/workspace/ui/button-group.tsx +83 -0
  69. package/workspace/ui/button.tsx +67 -0
  70. package/workspace/ui/calendar.tsx +222 -0
  71. package/workspace/ui/card.tsx +103 -0
  72. package/workspace/ui/carousel.tsx +240 -0
  73. package/workspace/ui/chart.tsx +373 -0
  74. package/workspace/ui/checkbox.tsx +31 -0
  75. package/workspace/ui/collapsible.tsx +33 -0
  76. package/workspace/ui/combobox.tsx +297 -0
  77. package/workspace/ui/command.tsx +195 -0
  78. package/workspace/ui/context-menu.tsx +261 -0
  79. package/workspace/ui/dialog.tsx +168 -0
  80. package/workspace/ui/direction.tsx +22 -0
  81. package/workspace/ui/drawer.tsx +132 -0
  82. package/workspace/ui/dropdown-menu.tsx +269 -0
  83. package/workspace/ui/empty.tsx +104 -0
  84. package/workspace/ui/field.tsx +236 -0
  85. package/workspace/ui/hover-card.tsx +42 -0
  86. package/workspace/ui/input-group.tsx +156 -0
  87. package/workspace/ui/input-otp.tsx +87 -0
  88. package/workspace/ui/input.tsx +19 -0
  89. package/workspace/ui/item.tsx +196 -0
  90. package/workspace/ui/kbd.tsx +26 -0
  91. package/workspace/ui/label.tsx +22 -0
  92. package/workspace/ui/marker.tsx +69 -0
  93. package/workspace/ui/menubar.tsx +280 -0
  94. package/workspace/ui/message-scroller.tsx +129 -0
  95. package/workspace/ui/message.tsx +92 -0
  96. package/workspace/ui/native-select.tsx +61 -0
  97. package/workspace/ui/navigation-menu.tsx +164 -0
  98. package/workspace/ui/pagination.tsx +129 -0
  99. package/workspace/ui/popover.tsx +87 -0
  100. package/workspace/ui/progress.tsx +31 -0
  101. package/workspace/ui/radio-group.tsx +42 -0
  102. package/workspace/ui/resizable.tsx +50 -0
  103. package/workspace/ui/scroll-area.tsx +53 -0
  104. package/workspace/ui/select.tsx +192 -0
  105. package/workspace/ui/separator.tsx +26 -0
  106. package/workspace/ui/sheet.tsx +147 -0
  107. package/workspace/ui/sidebar.tsx +702 -0
  108. package/workspace/ui/skeleton.tsx +13 -0
  109. package/workspace/ui/slider.tsx +59 -0
  110. package/workspace/ui/sonner.tsx +47 -0
  111. package/workspace/ui/spinner.tsx +10 -0
  112. package/workspace/ui/switch.tsx +33 -0
  113. package/workspace/ui/table.tsx +114 -0
  114. package/workspace/ui/tabs.tsx +90 -0
  115. package/workspace/ui/textarea.tsx +18 -0
  116. package/workspace/ui/toggle-group.tsx +87 -0
  117. package/workspace/ui/toggle.tsx +45 -0
  118. package/workspace/ui/tooltip.tsx +57 -0
  119. package/dist/export-BsK6OCpc.js +0 -4
  120. package/dist/preview-CPufRmuj.js +0 -21
  121. package/dist/sync-Be_hxSOF.js +0 -139
  122. package/dist/sync-lKEXyPOh.js +0 -3
  123. package/dist/version-CeffgdfZ.js +0 -3
  124. package/src/app/components/language-toggle.tsx +0 -46
  125. package/src/app/favicon.ico +0 -0
@@ -2,9 +2,40 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <link rel="icon" href="./favicon.ico" />
5
+ <link rel="icon" type="image/svg+xml" href="./assets/autono.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>open-pages</title>
7
+ <title>Autono</title>
8
+ <meta name="description" content="Autono — the web page framework built for agents." />
9
+ <meta property="og:title" content="Autono" />
10
+ <meta property="og:site_name" content="Autono" />
11
+ <meta property="og:description" content="The web page framework built for agents." />
12
+ <meta property="og:type" content="website" />
13
+ <meta property="og:image" content="./assets/autono-social.png" />
14
+ <meta name="twitter:card" content="summary_large_image" />
15
+ <meta name="twitter:title" content="Autono" />
16
+ <meta name="twitter:description" content="The web page framework built for agents." />
17
+ <meta name="twitter:image" content="./assets/autono-social.png" />
18
+ <style>
19
+ html {
20
+ background-color: oklch(0.99 0 0);
21
+ }
22
+ html.dark {
23
+ background-color: oklch(0.145 0 0);
24
+ }
25
+ </style>
26
+ <script>
27
+ // Apply the stored theme before first paint; next-themes only runs
28
+ // after the bundle loads, which flashes white for dark-mode users.
29
+ let stored = null;
30
+ try {
31
+ stored = localStorage.getItem('theme');
32
+ } catch (_) {}
33
+ const dark =
34
+ stored === 'dark' ||
35
+ (stored !== 'light' && matchMedia('(prefers-color-scheme: dark)').matches);
36
+ document.documentElement.classList.toggle('dark', dark);
37
+ document.documentElement.style.colorScheme = dark ? 'dark' : 'light';
38
+ </script>
8
39
  </head>
9
40
  <body>
10
41
  <div id="root"></div>
@@ -156,6 +156,39 @@ export async function fetchSvgAsFile(routeUrl: string, filename: string): Promis
156
156
  return new File([blob], filename, { type: 'image/svg+xml' });
157
157
  }
158
158
 
159
+ export type GoogleFontItem = {
160
+ family: string;
161
+ category: string;
162
+ // Weight keys from the Google Fonts catalog, e.g. ['400', '400i', '700'].
163
+ variants: string[];
164
+ };
165
+
166
+ export async function searchGoogleFonts(
167
+ query: string,
168
+ signal?: AbortSignal,
169
+ ): Promise<GoogleFontItem[]> {
170
+ const q = query.trim();
171
+ const params = new URLSearchParams();
172
+ if (q) params.set('q', q);
173
+ else params.set('limit', '30');
174
+ const res = await fetch(`/__gfonts/search?${params.toString()}`, { signal });
175
+ if (!res.ok) throw new Error(`gfonts ${res.status}`);
176
+ return (await res.json()) as GoogleFontItem[];
177
+ }
178
+
179
+ export async function fetchFontAsFile(
180
+ family: string,
181
+ variant: string,
182
+ filenameBase: string,
183
+ ): Promise<File> {
184
+ const params = new URLSearchParams({ family, variant });
185
+ const res = await fetch(`/__gfonts/download?${params.toString()}`);
186
+ if (!res.ok) throw new Error(`gfonts download ${res.status}`);
187
+ const ext = res.headers.get('x-font-ext') || 'ttf';
188
+ const blob = await res.blob();
189
+ return new File([blob], `${filenameBase}.${ext}`, { type: blob.type || 'font/ttf' });
190
+ }
191
+
159
192
  export type UseAssetsResult = {
160
193
  assets: AssetEntry[];
161
194
  loading: boolean;
@@ -0,0 +1,18 @@
1
+ const LAST_HOME_LOCATION_KEY = 'open-pages:last-home-location';
2
+
3
+ export function rememberHomeLocation(pathname: string, search: string): void {
4
+ try {
5
+ sessionStorage.setItem(LAST_HOME_LOCATION_KEY, pathname + search);
6
+ } catch {
7
+ // sessionStorage can be unavailable (private browsing, quota); falling
8
+ // back to the default home route is fine.
9
+ }
10
+ }
11
+
12
+ export function readLastHomeLocation(): string {
13
+ try {
14
+ return sessionStorage.getItem(LAST_HOME_LOCATION_KEY) ?? '/';
15
+ } catch {
16
+ return '/';
17
+ }
18
+ }
package/src/app/main.tsx CHANGED
@@ -7,7 +7,16 @@ import './styles.css';
7
7
  // biome-ignore lint/style/noNonNullAssertion: #root is guaranteed by index.html
8
8
  createRoot(document.getElementById('root')!).render(
9
9
  <StrictMode>
10
- <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
10
+ {/* next-themes injects an anti-flash <script> meant for SSR; in this
11
+ client-only app it never runs, and React 19 warns about executable
12
+ script tags, so mark it as a data block. */}
13
+ <ThemeProvider
14
+ attribute="class"
15
+ defaultTheme="system"
16
+ enableSystem
17
+ disableTransitionOnChange
18
+ scriptProps={{ type: 'application/json' }}
19
+ >
11
20
  <App />
12
21
  </ThemeProvider>
13
22
  </StrictMode>,
@@ -1,8 +1,8 @@
1
1
  import { Menu } from 'lucide-react';
2
- import { useCallback, useMemo, useState } from 'react';
2
+ import { useCallback, useEffect, useMemo, useState } from 'react';
3
3
  import { Outlet, useLocation, useNavigate, useSearchParams } from 'react-router-dom';
4
4
  import { toast } from 'sonner';
5
- import { LanguageToggle } from '~/components/language-toggle';
5
+ import logo from '~/assets/autono.svg';
6
6
  import { ThemeToggle } from '~/components/theme-toggle';
7
7
  import {
8
8
  DropdownMenu,
@@ -12,11 +12,12 @@ import {
12
12
  } from '~/components/ui/dropdown-menu';
13
13
  import { useAssets } from '~/lib/assets';
14
14
  import { useFolders } from '~/lib/folders';
15
+ import { rememberHomeLocation } from '~/lib/last-home-location';
15
16
  import { format, useLocale } from '~/lib/use-locale';
16
17
  import { cn } from '~/lib/utils';
17
18
  import { CommandMenuTrigger } from '../components/command/command-menu';
18
19
  import { HomeCommandMenu } from '../components/command/home-command-menu';
19
- import { FolderIconChip } from '../components/sidebar/folder-item';
20
+ import { SystemViewIcon } from '../components/sidebar/folder-item';
20
21
  import { ALL_DOCS_ID, ASSETS_ID, Sidebar, THEMES_ID } from '../components/sidebar/sidebar';
21
22
  import { pageIds } from '../lib/pages';
22
23
  import type { FoldersManifest } from '../lib/sdk';
@@ -64,6 +65,10 @@ export function HomeShell() {
64
65
 
65
66
  const selectedId = pathToSelectedId(location.pathname, searchParams);
66
67
 
68
+ useEffect(() => {
69
+ rememberHomeLocation(location.pathname, location.search);
70
+ }, [location.pathname, location.search]);
71
+
67
72
  const [commandOpen, setCommandOpen] = useState(false);
68
73
  const openCommandMenu = useCallback(() => setCommandOpen(true), []);
69
74
 
@@ -138,7 +143,7 @@ export function HomeShell() {
138
143
  };
139
144
 
140
145
  return (
141
- <div className="flex h-dvh overflow-hidden bg-background text-foreground">
146
+ <div className="flex h-dvh overflow-hidden bg-sidebar text-foreground">
142
147
  <div className="hidden md:block">
143
148
  <Sidebar
144
149
  folders={manifest.folders}
@@ -174,69 +179,85 @@ export function HomeShell() {
174
179
  />
175
180
  </div>
176
181
 
177
- <div className="relative flex min-w-0 flex-1 flex-col overflow-y-auto bg-canvas">
178
- <div className="flex items-center justify-between border-b border-hairline bg-sidebar px-4 py-3 md:hidden">
179
- <h1 className="font-heading text-lg font-bold tracking-tight">{t.home.appTitle}</h1>
180
- <div className="-mr-1.5 flex items-center gap-0.5">
181
- <CommandMenuTrigger onClick={openCommandMenu} />
182
- <LanguageToggle />
183
- <ThemeToggle />
184
- <DropdownMenu>
185
- <DropdownMenuTrigger
186
- render={
187
- <button
188
- type="button"
189
- aria-label={t.home.menu}
190
- className="flex size-8 items-center justify-center rounded-[6px] text-muted-foreground hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground"
191
- >
192
- <Menu className="size-4" />
193
- </button>
194
- }
182
+ <div className="relative flex min-w-0 flex-1 flex-col md:py-2 md:pr-2">
183
+ <div className="relative flex min-w-0 flex-1 flex-col overflow-y-auto bg-background md:rounded-[10px] md:shadow-edge md:ring-1 md:ring-foreground/[0.06]">
184
+ <div className="flex items-center justify-between border-b border-hairline bg-sidebar px-4 py-3 md:hidden">
185
+ <div className="flex min-w-0 items-center gap-2.5">
186
+ <img
187
+ src={logo}
188
+ alt=""
189
+ aria-hidden
190
+ draggable={false}
191
+ className="size-6 shrink-0 select-none rounded-[6px] ring-1 ring-foreground/10"
195
192
  />
196
- <DropdownMenuContent align="end" className="min-w-[200px]">
197
- <DropdownMenuItem
198
- onClick={() => selectFolder(ALL_DOCS_ID)}
199
- className={cn(
200
- selectedId !== THEMES_ID &&
201
- selectedId !== ASSETS_ID &&
202
- 'bg-muted text-foreground',
203
- )}
204
- >
205
- <FolderIconChip icon={{ type: 'emoji', value: '🎞️' }} />
206
- <span className="flex-1 truncate">{t.home.pages}</span>
207
- <span className="folio">{pageIds.length.toString().padStart(2, '0')}</span>
208
- </DropdownMenuItem>
209
- <DropdownMenuItem
210
- onClick={() => selectFolder(THEMES_ID)}
211
- className={cn(selectedId === THEMES_ID && 'bg-muted text-foreground')}
212
- >
213
- <FolderIconChip icon={{ type: 'emoji', value: '🎨' }} />
214
- <span className="flex-1 truncate">{t.home.themes}</span>
215
- <span className="folio">{themeRegistry.length.toString().padStart(2, '0')}</span>
216
- </DropdownMenuItem>
217
- {import.meta.env.DEV && (
193
+ <h1 className="truncate font-heading text-lg font-bold tracking-tight">
194
+ {t.home.appTitle}
195
+ </h1>
196
+ </div>
197
+ <div className="-mr-1.5 flex items-center gap-0.5">
198
+ <CommandMenuTrigger onClick={openCommandMenu} />
199
+ <ThemeToggle />
200
+ <DropdownMenu>
201
+ <DropdownMenuTrigger
202
+ render={
203
+ <button
204
+ type="button"
205
+ aria-label={t.home.menu}
206
+ className="flex size-8 items-center justify-center rounded-[6px] text-muted-foreground outline-none transition-[background-color,color,scale] duration-100 hover:bg-muted hover:text-foreground active:scale-95 focus-visible:ring-2 focus-visible:ring-ring/30 aria-expanded:bg-muted aria-expanded:text-foreground"
207
+ >
208
+ <Menu className="size-4" />
209
+ </button>
210
+ }
211
+ />
212
+ <DropdownMenuContent align="end" className="min-w-[200px]">
218
213
  <DropdownMenuItem
219
- onClick={() => selectFolder(ASSETS_ID)}
220
- className={cn(selectedId === ASSETS_ID && 'bg-muted text-foreground')}
214
+ onClick={() => selectFolder(ALL_DOCS_ID)}
215
+ className={cn(
216
+ selectedId !== THEMES_ID &&
217
+ selectedId !== ASSETS_ID &&
218
+ 'bg-muted text-foreground',
219
+ )}
221
220
  >
222
- <FolderIconChip icon={{ type: 'emoji', value: '🗂️' }} />
223
- <span className="flex-1 truncate">{t.home.assets}</span>
224
- <span className="folio">{globalAssets.length.toString().padStart(2, '0')}</span>
221
+ <SystemViewIcon kind="all" className="text-muted-foreground" />
222
+ <span className="flex-1 truncate">{t.home.pages}</span>
223
+ <span className="folio">{pageIds.length.toString().padStart(2, '0')}</span>
225
224
  </DropdownMenuItem>
226
- )}
227
- </DropdownMenuContent>
228
- </DropdownMenu>
225
+ <DropdownMenuItem
226
+ onClick={() => selectFolder(THEMES_ID)}
227
+ className={cn(selectedId === THEMES_ID && 'bg-muted text-foreground')}
228
+ >
229
+ <SystemViewIcon kind="themes" className="text-muted-foreground" />
230
+ <span className="flex-1 truncate">{t.home.themes}</span>
231
+ <span className="folio">
232
+ {themeRegistry.length.toString().padStart(2, '0')}
233
+ </span>
234
+ </DropdownMenuItem>
235
+ {import.meta.env.DEV && (
236
+ <DropdownMenuItem
237
+ onClick={() => selectFolder(ASSETS_ID)}
238
+ className={cn(selectedId === ASSETS_ID && 'bg-muted text-foreground')}
239
+ >
240
+ <SystemViewIcon kind="assets" className="text-muted-foreground" />
241
+ <span className="flex-1 truncate">{t.home.assets}</span>
242
+ <span className="folio">
243
+ {globalAssets.length.toString().padStart(2, '0')}
244
+ </span>
245
+ </DropdownMenuItem>
246
+ )}
247
+ </DropdownMenuContent>
248
+ </DropdownMenu>
249
+ </div>
229
250
  </div>
230
- </div>
231
251
 
232
- <div
233
- className={cn(
234
- isAssetsRoute
235
- ? 'flex min-h-0 flex-1 flex-col'
236
- : 'mx-auto w-full max-w-[1180px] px-5 py-8 md:px-10 md:py-12',
237
- )}
238
- >
239
- <Outlet context={ctx} />
252
+ <div
253
+ className={cn(
254
+ isAssetsRoute
255
+ ? 'flex min-h-0 flex-1 flex-col'
256
+ : 'mx-auto w-full max-w-[1180px] px-5 py-8 md:px-10 md:py-12',
257
+ )}
258
+ >
259
+ <Outlet context={ctx} />
260
+ </div>
240
261
  </div>
241
262
  </div>
242
263
 
@@ -32,11 +32,11 @@ import {
32
32
  } from '~/components/ui/dropdown-menu';
33
33
  import { format, useLocale } from '~/lib/use-locale';
34
34
  import { cn } from '~/lib/utils';
35
- import { DOC_DND_MIME, FolderIconChip } from '../components/sidebar/folder-item';
35
+ import { DOC_DND_MIME, FolderIconChip, SystemViewIcon } from '../components/sidebar/folder-item';
36
36
  import { ALL_DOCS_ID, DRAFT_ID } from '../components/sidebar/sidebar';
37
37
  import { PageThumb } from '../lib/page-thumb';
38
38
  import { loadPage, pageCreatedAt, pageIds, pageKinds } from '../lib/pages';
39
- import type { Folder, FolderIcon, PageModule } from '../lib/sdk';
39
+ import type { Folder, PageModule } from '../lib/sdk';
40
40
  import type { HomeOutletContext } from './home-shell';
41
41
 
42
42
  type SortKey = 'created-desc' | 'created-asc' | 'title-asc' | 'title-desc';
@@ -92,10 +92,6 @@ export function Home() {
92
92
  const visibleDocs = isAll ? pageIds : isDraft ? draftDocs : (docsByFolder[selectedId] ?? []);
93
93
 
94
94
  const title = selectedFolder?.name ?? (isAll ? t.home.pages : t.home.draft);
95
- const headerIcon = selectedFolder?.icon ?? {
96
- type: 'emoji' as const,
97
- value: isAll ? '🎞️' : '📝',
98
- };
99
95
 
100
96
  const [query, setQuery] = useState('');
101
97
  const [sortKey, setSortKey] = useSortPref();
@@ -131,10 +127,14 @@ export function Home() {
131
127
 
132
128
  return (
133
129
  <>
134
- <header className="mb-8 md:mb-12">
135
- <div className="flex flex-wrap items-center gap-3">
136
- <FolderIconChip icon={headerIcon} className="size-7 text-2xl" />
137
- <h1 className="font-heading text-[32px] font-semibold leading-[1.05] tracking-[-0.025em] md:text-[44px]">
130
+ <header className="mb-6 md:mb-8">
131
+ <div className="flex flex-wrap items-center gap-2.5">
132
+ {selectedFolder ? (
133
+ <FolderIconChip icon={selectedFolder.icon} className="size-5 text-[16px]" />
134
+ ) : (
135
+ <SystemViewIcon kind={isAll ? 'all' : 'draft'} className="text-muted-foreground" />
136
+ )}
137
+ <h1 className="font-heading text-[19px] font-semibold leading-none tracking-[-0.015em] md:text-[21px]">
138
138
  {title}
139
139
  </h1>
140
140
  <DropdownMenu>
@@ -143,7 +143,7 @@ export function Home() {
143
143
  <button
144
144
  type="button"
145
145
  aria-label={t.home.folders}
146
- className="flex size-7 items-center justify-center rounded-[6px] border border-border bg-card text-muted-foreground hover:text-foreground aria-expanded:border-foreground/40 aria-expanded:text-foreground md:hidden"
146
+ className="flex size-7 items-center justify-center rounded-[6px] border border-border bg-card text-muted-foreground outline-none transition-[background-color,color,scale] duration-100 hover:bg-muted hover:text-foreground active:scale-95 focus-visible:ring-2 focus-visible:ring-ring/30 aria-expanded:border-foreground/40 aria-expanded:text-foreground md:hidden"
147
147
  >
148
148
  <ChevronDown className="size-4" />
149
149
  </button>
@@ -154,7 +154,7 @@ export function Home() {
154
154
  onClick={() => selectFolder(ALL_DOCS_ID)}
155
155
  className={cn(isAll && 'bg-muted text-foreground')}
156
156
  >
157
- <FolderIconChip icon={{ type: 'emoji', value: '🎞️' }} />
157
+ <SystemViewIcon kind="all" className="text-muted-foreground" />
158
158
  <span className="flex-1 truncate">{t.home.pages}</span>
159
159
  <span className="folio">{pageIds.length.toString().padStart(2, '0')}</span>
160
160
  </DropdownMenuItem>
@@ -162,7 +162,7 @@ export function Home() {
162
162
  onClick={() => selectFolder(DRAFT_ID)}
163
163
  className={cn(isDraft && 'bg-muted text-foreground')}
164
164
  >
165
- <FolderIconChip icon={{ type: 'emoji', value: '📝' }} />
165
+ <SystemViewIcon kind="draft" className="text-muted-foreground" />
166
166
  <span className="flex-1 truncate">{t.home.draft}</span>
167
167
  <span className="folio">{draftDocs.length.toString().padStart(2, '0')}</span>
168
168
  </DropdownMenuItem>
@@ -182,7 +182,7 @@ export function Home() {
182
182
  </DropdownMenuContent>
183
183
  </DropdownMenu>
184
184
  {!loading && (
185
- <span className="folio ml-1 self-end pb-2">
185
+ <span className="folio ml-0.5">
186
186
  {(isSearching ? filteredDocs.length : visibleDocs.length).toString().padStart(2, '0')}
187
187
  {isSearching && (
188
188
  <span className="opacity-40">
@@ -206,8 +206,12 @@ export function Home() {
206
206
  <NoResultsState query={query} onClear={() => setQuery('')} />
207
207
  ) : (
208
208
  <ul className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-x-6 gap-y-9 md:grid-cols-[repeat(auto-fill,minmax(300px,1fr))]">
209
- {sortedDocs.map((id) => (
210
- <li key={id}>
209
+ {sortedDocs.map((id, i) => (
210
+ <li
211
+ key={id}
212
+ className="rise-in"
213
+ style={{ animationDelay: `${Math.min(i, 11) * 30}ms` }}
214
+ >
211
215
  <DocCard
212
216
  id={id}
213
217
  folders={manifest.folders}
@@ -259,7 +263,7 @@ function SearchInput({ value, onChange }: { value: string; onChange: (value: str
259
263
  type="button"
260
264
  onClick={() => onChange('')}
261
265
  aria-label={t.home.clearSearch}
262
- className="absolute right-1.5 top-1/2 flex size-5 -translate-y-1/2 items-center justify-center rounded-[4px] text-muted-foreground hover:bg-muted hover:text-foreground"
266
+ className="absolute right-1.5 top-1/2 flex size-5 -translate-y-1/2 items-center justify-center rounded-[4px] text-muted-foreground outline-none transition-[background-color,color,scale] duration-100 hover:bg-muted hover:text-foreground active:scale-90 focus-visible:ring-2 focus-visible:ring-ring/30"
263
267
  >
264
268
  <X className="size-3" />
265
269
  </button>
@@ -289,7 +293,7 @@ function SortControl({ value, onChange }: { value: SortKey; onChange: (next: Sor
289
293
  <button
290
294
  type="button"
291
295
  aria-label={`${t.home.sortLabel}: ${labels[value]}`}
292
- className="flex h-8 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-[6px] border border-border bg-background pl-2 pr-1.5 text-[12.5px] font-medium text-foreground outline-none hover:bg-muted focus-visible:border-foreground/40 focus-visible:ring-2 focus-visible:ring-ring/30"
296
+ className="flex h-8 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-[6px] border border-border bg-background pl-2 pr-1.5 text-[12.5px] font-medium text-foreground outline-none transition-[background-color,translate] duration-100 hover:bg-muted active:translate-y-px focus-visible:border-foreground/40 focus-visible:ring-2 focus-visible:ring-ring/30"
293
297
  >
294
298
  <FieldIcon k={value} className="size-3.5 text-muted-foreground" />
295
299
  <span>{labels[value]}</span>
@@ -336,12 +340,10 @@ function HomeLoading() {
336
340
  function NoResultsState({ query, onClear }: { query: string; onClear: () => void }) {
337
341
  const t = useLocale();
338
342
  return (
339
- <div className="rounded-[10px] border border-dashed border-border bg-card/60 px-8 py-20">
343
+ <div className="rounded-[8px] border border-dashed border-border px-8 py-20">
340
344
  <div className="mx-auto flex max-w-md flex-col items-center text-center">
341
- <div className="flex size-12 items-center justify-center rounded-full border border-hairline bg-card text-muted-foreground">
342
- <Search className="size-5" />
343
- </div>
344
- <p className="mt-4 font-heading text-[15px] font-semibold tracking-tight">
345
+ <Search className="size-5 text-muted-foreground/60" aria-hidden />
346
+ <p className="mt-4 font-heading text-[14px] font-semibold tracking-tight">
345
347
  {t.home.noMatches}
346
348
  </p>
347
349
  <p className="mt-1.5 text-[13px] leading-relaxed text-muted-foreground">
@@ -364,14 +366,12 @@ function EmptyState({ isDraft, folderName }: { isDraft: boolean; folderName?: st
364
366
  folderName ?? t.home.folderEmptyTitle,
365
367
  );
366
368
  return (
367
- <div className="rounded-[10px] border border-dashed border-border bg-card/60 px-8 py-20">
369
+ <div className="rounded-[8px] border border-dashed border-border px-8 py-20">
368
370
  <div className="mx-auto flex max-w-md flex-col items-center text-center">
369
- <div className="flex size-12 items-center justify-center rounded-full border border-hairline bg-card text-muted-foreground">
370
- <FolderPlus className="size-5" />
371
- </div>
371
+ <FolderPlus className="size-5 text-muted-foreground/60" aria-hidden />
372
372
  {isDraft ? (
373
373
  <>
374
- <p className="mt-4 font-heading text-[15px] font-semibold tracking-tight">
374
+ <p className="mt-4 font-heading text-[14px] font-semibold tracking-tight">
375
375
  {t.home.noDocsYet}
376
376
  </p>
377
377
  <p className="mt-1.5 text-[13px] leading-relaxed text-muted-foreground">
@@ -384,7 +384,7 @@ function EmptyState({ isDraft, folderName }: { isDraft: boolean; folderName?: st
384
384
  </>
385
385
  ) : (
386
386
  <>
387
- <p className="mt-4 font-heading text-[15px] font-semibold tracking-tight">
387
+ <p className="mt-4 font-heading text-[14px] font-semibold tracking-tight">
388
388
  {folderEmptyTitle}
389
389
  </p>
390
390
  <p className="mt-1.5 text-[13px] leading-relaxed text-muted-foreground">
@@ -502,9 +502,12 @@ function DocCard({
502
502
  onDragEnd={() => setDragging(false)}
503
503
  className={cn('group relative motion-safe:transition-opacity', dragging && 'opacity-40')}
504
504
  >
505
- <Link to={`/p/${id}`} className="block focus-visible:outline-none">
505
+ <Link
506
+ to={`/p/${id}`}
507
+ className="block rounded-[6px] outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-2 focus-visible:ring-offset-background"
508
+ >
506
509
  {/* Page thumb — tight border, grey baseboard, no shadcn rounded-xl */}
507
- <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">
510
+ <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,scale] motion-safe:duration-200 group-active:scale-[0.99]">
508
511
  <div className="h-full w-full motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-[1.03]">
509
512
  <PageThumb source={{ pageId: id, kind: pageKinds[id] }} title={displayTitle} />
510
513
  </div>
@@ -528,7 +531,7 @@ function DocCard({
528
531
  </div>
529
532
 
530
533
  {import.meta.env.DEV && (
531
- <div className="absolute right-2 top-2">
534
+ <div className="absolute right-2 top-2 z-20">
532
535
  <DropdownMenu>
533
536
  <DropdownMenuTrigger
534
537
  render={
@@ -538,7 +541,7 @@ function DocCard({
538
541
  e.stopPropagation();
539
542
  e.preventDefault();
540
543
  }}
541
- className="flex size-7 items-center justify-center rounded-[5px] bg-card/90 text-foreground shadow-edge ring-1 ring-border opacity-0 backdrop-blur hover:bg-card group-hover:opacity-100 aria-expanded:opacity-100 motion-safe:transition-opacity"
544
+ className="flex size-7 items-center justify-center rounded-[5px] bg-card/90 text-foreground shadow-edge ring-1 ring-border opacity-0 outline-none backdrop-blur hover:bg-card group-hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-ring/40 aria-expanded:opacity-100 motion-safe:transition-opacity motion-safe:duration-150"
542
545
  aria-label={tCard.home.docActions}
543
546
  >
544
547
  <MoreHorizontal className="size-3.5" />
@@ -731,7 +734,7 @@ function MoveDialog({
731
734
  </DialogHeader>
732
735
  <div className="max-h-[320px] overflow-y-auto rounded-[6px] border border-border bg-background">
733
736
  <FolderOption
734
- icon={{ type: 'emoji', value: '📝' }}
737
+ chip={<SystemViewIcon kind="draft" className="text-muted-foreground" />}
735
738
  label={t.home.draft}
736
739
  active={selected === null}
737
740
  onClick={() => setSelected(null)}
@@ -739,7 +742,7 @@ function MoveDialog({
739
742
  {folders.map((f) => (
740
743
  <FolderOption
741
744
  key={f.id}
742
- icon={f.icon}
745
+ chip={<FolderIconChip icon={f.icon} />}
743
746
  label={f.name}
744
747
  active={selected === f.id}
745
748
  onClick={() => setSelected(f.id)}
@@ -760,12 +763,12 @@ function MoveDialog({
760
763
  }
761
764
 
762
765
  function FolderOption({
763
- icon,
766
+ chip,
764
767
  label,
765
768
  active,
766
769
  onClick,
767
770
  }: {
768
- icon: FolderIcon;
771
+ chip: React.ReactNode;
769
772
  label: string;
770
773
  active: boolean;
771
774
  onClick: () => void;
@@ -776,11 +779,11 @@ function FolderOption({
776
779
  type="button"
777
780
  onClick={onClick}
778
781
  className={cn(
779
- 'flex w-full items-center gap-2 border-b border-hairline px-3 py-2 text-left text-[13px] transition-colors last:border-b-0',
782
+ 'flex w-full items-center gap-2 border-b border-hairline px-3 py-2 text-left text-[13px] outline-none transition-colors duration-100 last:border-b-0 focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-brand active:bg-muted',
780
783
  active ? 'bg-muted text-foreground' : 'hover:bg-muted/60',
781
784
  )}
782
785
  >
783
- <FolderIconChip icon={icon} />
786
+ {chip}
784
787
  <span className="truncate">{label}</span>
785
788
  {active && (
786
789
  <span className="ml-auto inline-flex items-center gap-1 text-[10.5px] text-brand">
@@ -12,9 +12,10 @@ import {
12
12
  X,
13
13
  } from 'lucide-react';
14
14
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
15
- import { Link, useParams } from 'react-router-dom';
15
+ import { useNavigate, useParams } from 'react-router-dom';
16
16
  import { toast } from 'sonner';
17
17
  import { Button } from '~/components/ui/button';
18
+ import { readLastHomeLocation } from '~/lib/last-home-location';
18
19
  import { cn } from '~/lib/utils';
19
20
  import { type FrameMessage, frameUrl, isFrameMessage, type WorkspaceMessage } from '../lib/frame';
20
21
  import { pageChangeIncludes, pageIds, pageKinds } from '../lib/pages';
@@ -53,8 +54,22 @@ export function PageView() {
53
54
 
54
55
  const title = pageModule?.meta?.title ?? pageId;
55
56
  useEffect(() => {
56
- document.title = `${title} — open-pages`;
57
+ document.title = `${title} — Autono`;
58
+ return () => {
59
+ document.title = 'Autono';
60
+ };
57
61
  }, [title]);
62
+ const navigate = useNavigate();
63
+ // react-router records its entry index in history.state; going back is only
64
+ // safe when we actually pushed an entry, otherwise land on the last home view.
65
+ const goBack = useCallback(() => {
66
+ const idx = (window.history.state as { idx?: number } | null)?.idx ?? 0;
67
+ if (idx > 0) {
68
+ navigate(-1);
69
+ } else {
70
+ navigate(readLastHomeLocation(), { replace: true });
71
+ }
72
+ }, [navigate]);
58
73
 
59
74
  const frameRef = useRef<HTMLIFrameElement | null>(null);
60
75
  const [frameKey, setFrameKey] = useState(0);
@@ -178,7 +193,7 @@ export function PageView() {
178
193
  {showPageUi && (
179
194
  <header className="flex h-12 shrink-0 items-center gap-2 border-b bg-background px-3">
180
195
  {showPageBrowser && (
181
- <Button variant="ghost" size="icon" aria-label="Back to pages" render={<Link to="/" />}>
196
+ <Button variant="ghost" size="icon" aria-label="Back to pages" onClick={goBack}>
182
197
  <ChevronLeft className="size-4" />
183
198
  </Button>
184
199
  )}
@@ -1,6 +1,6 @@
1
1
  import { useNavigate, useParams } from 'react-router-dom';
2
2
  import { useLocale } from '~/lib/use-locale';
3
- import { FolderIconChip } from '../components/sidebar/folder-item';
3
+ import { SystemViewIcon } from '../components/sidebar/folder-item';
4
4
  import { ThemeDetail } from '../components/themes/theme-detail';
5
5
  import { ThemesGallery } from '../components/themes/themes-gallery';
6
6
  import { themes as themeRegistry } from '../lib/themes';
@@ -10,15 +10,13 @@ export function ThemesGalleryPage() {
10
10
  const t = useLocale();
11
11
  return (
12
12
  <>
13
- <header className="mb-8 md:mb-12">
14
- <div className="flex flex-wrap items-center gap-3">
15
- <FolderIconChip icon={{ type: 'emoji', value: '🎨' }} className="size-7 text-2xl" />
16
- <h1 className="font-heading text-[32px] font-semibold leading-[1.05] tracking-[-0.025em] md:text-[44px]">
13
+ <header className="mb-6 md:mb-8">
14
+ <div className="flex flex-wrap items-center gap-2.5">
15
+ <SystemViewIcon kind="themes" className="text-muted-foreground" />
16
+ <h1 className="font-heading text-[19px] font-semibold leading-none tracking-[-0.015em] md:text-[21px]">
17
17
  {t.themes.title}
18
18
  </h1>
19
- <span className="folio ml-1 self-end pb-2">
20
- {themeRegistry.length.toString().padStart(2, '0')}
21
- </span>
19
+ <span className="folio ml-0.5">{themeRegistry.length.toString().padStart(2, '0')}</span>
22
20
  </div>
23
21
  </header>
24
22
  <ThemesGallery onOpen={(id) => navigate(`/themes/${encodeURIComponent(id)}`)} />