@autono/open-pages 0.1.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/LICENSE +23 -0
- package/README.md +108 -0
- package/bin.js +2 -0
- package/dist/build-TP72kiw7.js +35 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +100 -0
- package/dist/config-C9or4m4J.d.ts +23 -0
- package/dist/config-L0IbktJ0.js +3135 -0
- package/dist/design-C13iz9_4.js +35 -0
- package/dist/dev-I2IyEh5W.js +97 -0
- package/dist/export-B-kBRWB1.js +4 -0
- package/dist/export-ic28osQP.js +131 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +3 -0
- package/dist/locale/index.d.ts +24 -0
- package/dist/locale/index.js +881 -0
- package/dist/open-pages-plugin-D_DN_zjh.js +632 -0
- package/dist/preview-BrDuvgAN.js +21 -0
- package/dist/sync-Be_hxSOF.js +139 -0
- package/dist/sync-lKEXyPOh.js +3 -0
- package/dist/types-C5NCdDT-.d.ts +219 -0
- package/dist/update-CV62NhBT.js +37 -0
- package/dist/update-package-Cxt57pz7.js +152 -0
- package/dist/version-B6AiiGvT.js +26 -0
- package/dist/version-CeffgdfZ.js +3 -0
- package/dist/vite/index.d.ts +14 -0
- package/dist/vite/index.js +7 -0
- package/env.d.ts +59 -0
- package/package.json +106 -0
- package/skills/apply-comments/SKILL.md +86 -0
- package/skills/create-page/SKILL.md +92 -0
- package/skills/create-theme/SKILL.md +223 -0
- package/skills/current-page/SKILL.md +106 -0
- package/skills/page-authoring/SKILL.md +148 -0
- package/skills/page-authoring/references/assets-and-fonts.md +85 -0
- package/skills/page-authoring/references/html-pages.md +65 -0
- package/skills/page-authoring/references/interactivity.md +77 -0
- package/skills/page-authoring/references/layout-and-responsive.md +71 -0
- package/skills/page-authoring/references/typography-and-color.md +60 -0
- package/src/app/app.tsx +45 -0
- package/src/app/components/asset-view.tsx +1665 -0
- package/src/app/components/command/command-menu.tsx +227 -0
- package/src/app/components/command/command.tsx +142 -0
- package/src/app/components/command/home-command-menu.tsx +96 -0
- package/src/app/components/icon-tooltip.tsx +34 -0
- package/src/app/components/language-toggle.tsx +46 -0
- package/src/app/components/sidebar/folder-item.tsx +269 -0
- package/src/app/components/sidebar/icon-picker.tsx +61 -0
- package/src/app/components/sidebar/sidebar-footer.tsx +139 -0
- package/src/app/components/sidebar/sidebar.tsx +302 -0
- package/src/app/components/theme-toggle.tsx +66 -0
- package/src/app/components/themes/theme-detail.tsx +226 -0
- package/src/app/components/themes/themes-gallery.tsx +104 -0
- package/src/app/components/ui/badge.tsx +48 -0
- package/src/app/components/ui/button.tsx +93 -0
- package/src/app/components/ui/card.tsx +92 -0
- package/src/app/components/ui/context-menu.tsx +247 -0
- package/src/app/components/ui/dialog.tsx +155 -0
- package/src/app/components/ui/dropdown-menu.tsx +267 -0
- package/src/app/components/ui/input.tsx +25 -0
- package/src/app/components/ui/label.tsx +21 -0
- package/src/app/components/ui/popover.tsx +82 -0
- package/src/app/components/ui/progress.tsx +33 -0
- package/src/app/components/ui/scroll-area.tsx +53 -0
- package/src/app/components/ui/select.tsx +195 -0
- package/src/app/components/ui/separator.tsx +26 -0
- package/src/app/components/ui/slider.tsx +68 -0
- package/src/app/components/ui/sonner.tsx +48 -0
- package/src/app/components/ui/tabs.tsx +79 -0
- package/src/app/components/ui/textarea.tsx +22 -0
- package/src/app/components/ui/toggle-group.tsx +84 -0
- package/src/app/components/ui/toggle.tsx +45 -0
- package/src/app/components/ui/tooltip.tsx +75 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/frame/inspect.ts +140 -0
- package/src/app/frame/main.tsx +91 -0
- package/src/app/frame.html +12 -0
- package/src/app/index.html +13 -0
- package/src/app/lib/asset-filter.test.ts +176 -0
- package/src/app/lib/asset-filter.ts +80 -0
- package/src/app/lib/assets.ts +257 -0
- package/src/app/lib/design.ts +58 -0
- package/src/app/lib/folders.ts +239 -0
- package/src/app/lib/frame.ts +38 -0
- package/src/app/lib/locale-store.ts +67 -0
- package/src/app/lib/page-thumb.tsx +46 -0
- package/src/app/lib/pages.ts +28 -0
- package/src/app/lib/sdk.ts +43 -0
- package/src/app/lib/themes.ts +22 -0
- package/src/app/lib/use-locale.ts +8 -0
- package/src/app/lib/use-page-module.ts +48 -0
- package/src/app/lib/use-page-titles.ts +29 -0
- package/src/app/lib/use-restart-server.ts +78 -0
- package/src/app/lib/utils.test.ts +25 -0
- package/src/app/lib/utils.ts +6 -0
- package/src/app/main.tsx +14 -0
- package/src/app/routes/assets.tsx +9 -0
- package/src/app/routes/home-shell.tsx +252 -0
- package/src/app/routes/home.tsx +846 -0
- package/src/app/routes/page.tsx +329 -0
- package/src/app/routes/themes.tsx +34 -0
- package/src/app/styles.css +405 -0
- package/src/app/virtual.d.ts +53 -0
- package/src/locale/en.ts +218 -0
- package/src/locale/format.ts +12 -0
- package/src/locale/index.ts +6 -0
- package/src/locale/ja.ts +221 -0
- package/src/locale/types.ts +215 -0
- package/src/locale/zh-cn.ts +218 -0
- package/src/locale/zh-tw.ts +218 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import config from 'virtual:open-pages/config';
|
|
2
|
+
import {
|
|
3
|
+
ChevronLeft,
|
|
4
|
+
Crosshair,
|
|
5
|
+
ExternalLink,
|
|
6
|
+
Loader2,
|
|
7
|
+
MessageSquarePlus,
|
|
8
|
+
Monitor,
|
|
9
|
+
RotateCw,
|
|
10
|
+
Smartphone,
|
|
11
|
+
Tablet,
|
|
12
|
+
X,
|
|
13
|
+
} from 'lucide-react';
|
|
14
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
15
|
+
import { Link, useParams } from 'react-router-dom';
|
|
16
|
+
import { toast } from 'sonner';
|
|
17
|
+
import { Button } from '@/components/ui/button';
|
|
18
|
+
import { cn } from '@/lib/utils';
|
|
19
|
+
import { type FrameMessage, frameUrl, isFrameMessage, type WorkspaceMessage } from '../lib/frame';
|
|
20
|
+
import { pageChangeIncludes, pageIds, pageKinds } from '../lib/pages';
|
|
21
|
+
import { usePageModule } from '../lib/use-page-module';
|
|
22
|
+
|
|
23
|
+
const { showPageUi, showPageBrowser } = config.build;
|
|
24
|
+
|
|
25
|
+
type Viewport = { id: 'desktop' | 'tablet' | 'mobile'; width: number | null; label: string };
|
|
26
|
+
|
|
27
|
+
const VIEWPORTS: Viewport[] = [
|
|
28
|
+
{ id: 'desktop', width: null, label: 'Desktop' },
|
|
29
|
+
{ id: 'tablet', width: 820, label: 'Tablet' },
|
|
30
|
+
{ id: 'mobile', width: 390, label: 'Mobile' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const VIEWPORT_STORAGE_KEY = 'open-pages:viewport';
|
|
34
|
+
|
|
35
|
+
function readViewportPref(): Viewport {
|
|
36
|
+
try {
|
|
37
|
+
const raw = window.localStorage.getItem(VIEWPORT_STORAGE_KEY);
|
|
38
|
+
return VIEWPORTS.find((v) => v.id === raw) ?? VIEWPORTS[0];
|
|
39
|
+
} catch {
|
|
40
|
+
return VIEWPORTS[0];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const VIEWPORT_ICONS = { desktop: Monitor, tablet: Tablet, mobile: Smartphone } as const;
|
|
45
|
+
|
|
46
|
+
type Selection = { loc: string; tag: string; text: string };
|
|
47
|
+
|
|
48
|
+
export function PageView() {
|
|
49
|
+
const { pageId = '' } = useParams();
|
|
50
|
+
const known = pageIds.includes(pageId);
|
|
51
|
+
const kind = pageKinds[pageId] ?? 'react';
|
|
52
|
+
const { page: pageModule } = usePageModule(pageId);
|
|
53
|
+
|
|
54
|
+
const title = pageModule?.meta?.title ?? pageId;
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
document.title = `${title} — open-pages`;
|
|
57
|
+
}, [title]);
|
|
58
|
+
|
|
59
|
+
const frameRef = useRef<HTMLIFrameElement | null>(null);
|
|
60
|
+
const [frameKey, setFrameKey] = useState(0);
|
|
61
|
+
const [ready, setReady] = useState(false);
|
|
62
|
+
const [error, setError] = useState<string | null>(null);
|
|
63
|
+
const [viewport, setViewport] = useState<Viewport>(readViewportPref);
|
|
64
|
+
const pickViewport = (v: Viewport) => {
|
|
65
|
+
setViewport(v);
|
|
66
|
+
try {
|
|
67
|
+
window.localStorage.setItem(VIEWPORT_STORAGE_KEY, v.id);
|
|
68
|
+
} catch {}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const [inspecting, setInspecting] = useState(false);
|
|
72
|
+
const [selection, setSelection] = useState<Selection | null>(null);
|
|
73
|
+
const [note, setNote] = useState('');
|
|
74
|
+
const [savingNote, setSavingNote] = useState(false);
|
|
75
|
+
|
|
76
|
+
const send = useCallback((msg: WorkspaceMessage) => {
|
|
77
|
+
frameRef.current?.contentWindow?.postMessage(msg, '*');
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
send({ type: 'op:inspect', on: inspecting });
|
|
82
|
+
if (!inspecting) {
|
|
83
|
+
setSelection(null);
|
|
84
|
+
send({ type: 'op:select', loc: null });
|
|
85
|
+
}
|
|
86
|
+
}, [inspecting, send]);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const onMessage = (event: MessageEvent) => {
|
|
90
|
+
if (event.source !== frameRef.current?.contentWindow) return;
|
|
91
|
+
if (!isFrameMessage(event.data)) return;
|
|
92
|
+
const msg: FrameMessage = event.data;
|
|
93
|
+
if (msg.type === 'op:ready') {
|
|
94
|
+
setReady(true);
|
|
95
|
+
setError(null);
|
|
96
|
+
send({ type: 'op:inspect', on: inspecting });
|
|
97
|
+
} else if (msg.type === 'op:error') {
|
|
98
|
+
setReady(true);
|
|
99
|
+
setError(msg.message);
|
|
100
|
+
} else if (msg.type === 'op:select') {
|
|
101
|
+
setSelection({ loc: msg.loc, tag: msg.tag, text: msg.text });
|
|
102
|
+
setNote('');
|
|
103
|
+
} else if (msg.type === 'op:key') {
|
|
104
|
+
if (msg.key === 'i') setInspecting((v) => !v);
|
|
105
|
+
if (msg.key === 'Escape') setSelection(null);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
window.addEventListener('message', onMessage);
|
|
109
|
+
return () => window.removeEventListener('message', onMessage);
|
|
110
|
+
}, [inspecting, send]);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const onKey = (e: KeyboardEvent) => {
|
|
114
|
+
const tag = (e.target as HTMLElement | null)?.tagName;
|
|
115
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
|
116
|
+
if (e.key === 'i') setInspecting((v) => !v);
|
|
117
|
+
if (e.key === 'Escape') setSelection(null);
|
|
118
|
+
};
|
|
119
|
+
window.addEventListener('keydown', onKey);
|
|
120
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (kind !== 'html' || !import.meta.hot) return;
|
|
125
|
+
const handler = (data: unknown) => {
|
|
126
|
+
if (pageChangeIncludes(data, pageId)) setFrameKey((k) => k + 1);
|
|
127
|
+
};
|
|
128
|
+
import.meta.hot.on('open-pages:page-changed', handler);
|
|
129
|
+
return () => import.meta.hot?.off('open-pages:page-changed', handler);
|
|
130
|
+
}, [kind, pageId]);
|
|
131
|
+
|
|
132
|
+
// Agent cursor: which page is open, and what is selected.
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (!import.meta.hot || !pageId || !known) return;
|
|
135
|
+
import.meta.hot.send('open-pages:current', { pageId, pageTitle: title, view: 'pages' });
|
|
136
|
+
}, [pageId, known, title]);
|
|
137
|
+
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!import.meta.hot) return;
|
|
140
|
+
const [line, column] = selection
|
|
141
|
+
? selection.loc.split(':').map(Number)
|
|
142
|
+
: [undefined, undefined];
|
|
143
|
+
import.meta.hot.send('open-pages:current', {
|
|
144
|
+
selection: selection ? { line, column, tagName: selection.tag, text: selection.text } : null,
|
|
145
|
+
});
|
|
146
|
+
send({ type: 'op:select', loc: selection?.loc ?? null });
|
|
147
|
+
}, [selection, send]);
|
|
148
|
+
|
|
149
|
+
const submitComment = useCallback(async () => {
|
|
150
|
+
if (!selection || !note.trim()) return;
|
|
151
|
+
const [line, column] = selection.loc.split(':').map(Number);
|
|
152
|
+
setSavingNote(true);
|
|
153
|
+
try {
|
|
154
|
+
const res = await fetch('/__comments/add', {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: { 'Content-Type': 'application/json' },
|
|
157
|
+
body: JSON.stringify({ pageId, line, column, text: note.trim() }),
|
|
158
|
+
});
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
161
|
+
throw new Error(body.error ?? `POST /__comments/add → ${res.status}`);
|
|
162
|
+
}
|
|
163
|
+
toast.success('Comment saved — run /apply-comments to apply it');
|
|
164
|
+
setSelection(null);
|
|
165
|
+
setNote('');
|
|
166
|
+
} catch (e) {
|
|
167
|
+
toast.error(e instanceof Error ? e.message : String(e));
|
|
168
|
+
} finally {
|
|
169
|
+
setSavingNote(false);
|
|
170
|
+
}
|
|
171
|
+
}, [selection, note, pageId]);
|
|
172
|
+
|
|
173
|
+
const src = useMemo(() => (known ? frameUrl({ pageId, kind }) : ''), [known, pageId, kind]);
|
|
174
|
+
const canInspect = kind === 'react' && import.meta.env.DEV;
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div className="flex h-screen flex-col bg-muted/40 text-foreground">
|
|
178
|
+
{showPageUi && (
|
|
179
|
+
<header className="flex h-12 shrink-0 items-center gap-2 border-b bg-background px-3">
|
|
180
|
+
{showPageBrowser && (
|
|
181
|
+
<Button variant="ghost" size="icon" aria-label="Back to pages" render={<Link to="/" />}>
|
|
182
|
+
<ChevronLeft className="size-4" />
|
|
183
|
+
</Button>
|
|
184
|
+
)}
|
|
185
|
+
<h1 className="min-w-0 truncate text-sm font-medium">{title}</h1>
|
|
186
|
+
{kind === 'html' && (
|
|
187
|
+
<span className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
|
|
188
|
+
html
|
|
189
|
+
</span>
|
|
190
|
+
)}
|
|
191
|
+
<div className="ml-auto flex items-center gap-2">
|
|
192
|
+
{!ready && known && (
|
|
193
|
+
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
194
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
195
|
+
Loading
|
|
196
|
+
</span>
|
|
197
|
+
)}
|
|
198
|
+
<fieldset className="flex items-center rounded-md border p-0.5" aria-label="Viewport">
|
|
199
|
+
{VIEWPORTS.map((v) => {
|
|
200
|
+
const Icon = VIEWPORT_ICONS[v.id];
|
|
201
|
+
return (
|
|
202
|
+
<Button
|
|
203
|
+
key={v.id}
|
|
204
|
+
variant={viewport.id === v.id ? 'secondary' : 'ghost'}
|
|
205
|
+
size="icon-sm"
|
|
206
|
+
aria-label={v.label}
|
|
207
|
+
aria-pressed={viewport.id === v.id}
|
|
208
|
+
title={v.width ? `${v.label} · ${v.width}px` : v.label}
|
|
209
|
+
onClick={() => pickViewport(v)}
|
|
210
|
+
>
|
|
211
|
+
<Icon className="size-3.5" />
|
|
212
|
+
</Button>
|
|
213
|
+
);
|
|
214
|
+
})}
|
|
215
|
+
</fieldset>
|
|
216
|
+
<Button
|
|
217
|
+
variant="outline"
|
|
218
|
+
size="icon-sm"
|
|
219
|
+
aria-label="Reload page"
|
|
220
|
+
title="Reload the page"
|
|
221
|
+
onClick={() => {
|
|
222
|
+
setReady(false);
|
|
223
|
+
setFrameKey((k) => k + 1);
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
<RotateCw className="size-3.5" />
|
|
227
|
+
</Button>
|
|
228
|
+
{canInspect && (
|
|
229
|
+
<Button
|
|
230
|
+
variant={inspecting ? 'default' : 'outline'}
|
|
231
|
+
size="sm"
|
|
232
|
+
onClick={() => setInspecting((v) => !v)}
|
|
233
|
+
disabled={!ready || !!error}
|
|
234
|
+
aria-pressed={inspecting}
|
|
235
|
+
title="Inspect elements (i) — click any element to select it or leave a comment"
|
|
236
|
+
>
|
|
237
|
+
<Crosshair className="size-4" />
|
|
238
|
+
Inspect
|
|
239
|
+
</Button>
|
|
240
|
+
)}
|
|
241
|
+
<Button
|
|
242
|
+
variant="outline"
|
|
243
|
+
size="sm"
|
|
244
|
+
disabled={!known}
|
|
245
|
+
render={<a href={src} target="_blank" rel="noreferrer" />}
|
|
246
|
+
aria-label="Open page in a new tab"
|
|
247
|
+
title="Open the page by itself in a new tab"
|
|
248
|
+
>
|
|
249
|
+
<ExternalLink className="size-4" />
|
|
250
|
+
Open
|
|
251
|
+
</Button>
|
|
252
|
+
</div>
|
|
253
|
+
</header>
|
|
254
|
+
)}
|
|
255
|
+
|
|
256
|
+
{(error || !known) && (
|
|
257
|
+
<div className="border-b border-destructive/30 bg-destructive/10 px-4 py-2 font-mono text-xs text-destructive whitespace-pre-wrap">
|
|
258
|
+
{known ? error : `Page not found: ${pageId}`}
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
|
|
262
|
+
<main className="relative flex min-h-0 flex-1 justify-center overflow-hidden">
|
|
263
|
+
{known && (
|
|
264
|
+
<iframe
|
|
265
|
+
key={frameKey}
|
|
266
|
+
ref={frameRef}
|
|
267
|
+
title={title}
|
|
268
|
+
src={src}
|
|
269
|
+
className={cn(
|
|
270
|
+
'h-full border-0 bg-white',
|
|
271
|
+
viewport.width !== null && 'border-x shadow-md',
|
|
272
|
+
inspecting && 'cursor-crosshair',
|
|
273
|
+
)}
|
|
274
|
+
style={{ width: viewport.width ?? '100%', maxWidth: '100%' }}
|
|
275
|
+
/>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
{selection && (
|
|
279
|
+
<div className="fixed bottom-6 right-6 z-20 w-[340px] rounded-lg border bg-background p-3 shadow-xl">
|
|
280
|
+
<div className="flex items-center gap-2">
|
|
281
|
+
<span className="rounded bg-muted px-1.5 py-0.5 font-mono text-[11px]">
|
|
282
|
+
{selection.tag}
|
|
283
|
+
</span>
|
|
284
|
+
<span className="font-mono text-[11px] text-muted-foreground">
|
|
285
|
+
line {selection.loc.replace(':', ', col ')}
|
|
286
|
+
</span>
|
|
287
|
+
<Button
|
|
288
|
+
variant="ghost"
|
|
289
|
+
size="icon"
|
|
290
|
+
className="ml-auto size-6"
|
|
291
|
+
aria-label="Clear selection"
|
|
292
|
+
onClick={() => setSelection(null)}
|
|
293
|
+
>
|
|
294
|
+
<X className="size-3.5" />
|
|
295
|
+
</Button>
|
|
296
|
+
</div>
|
|
297
|
+
{selection.text && (
|
|
298
|
+
<p className="mt-1.5 line-clamp-2 text-xs text-muted-foreground">
|
|
299
|
+
“{selection.text}”
|
|
300
|
+
</p>
|
|
301
|
+
)}
|
|
302
|
+
<textarea
|
|
303
|
+
value={note}
|
|
304
|
+
onChange={(e) => setNote(e.target.value)}
|
|
305
|
+
placeholder="Leave a note for your agent — e.g. “make this bold”"
|
|
306
|
+
rows={2}
|
|
307
|
+
className="mt-2 w-full resize-none rounded-md border bg-transparent px-2 py-1.5 text-sm outline-none focus:ring-1 focus:ring-ring"
|
|
308
|
+
/>
|
|
309
|
+
<div className="mt-2 flex justify-end gap-2">
|
|
310
|
+
<Button
|
|
311
|
+
size="sm"
|
|
312
|
+
onClick={submitComment}
|
|
313
|
+
disabled={!note.trim() || savingNote}
|
|
314
|
+
aria-label="Save comment"
|
|
315
|
+
>
|
|
316
|
+
{savingNote ? (
|
|
317
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
318
|
+
) : (
|
|
319
|
+
<MessageSquarePlus className="size-3.5" />
|
|
320
|
+
)}
|
|
321
|
+
Comment
|
|
322
|
+
</Button>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
</main>
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useNavigate, useParams } from 'react-router-dom';
|
|
2
|
+
import { useLocale } from '@/lib/use-locale';
|
|
3
|
+
import { FolderIconChip } from '../components/sidebar/folder-item';
|
|
4
|
+
import { ThemeDetail } from '../components/themes/theme-detail';
|
|
5
|
+
import { ThemesGallery } from '../components/themes/themes-gallery';
|
|
6
|
+
import { themes as themeRegistry } from '../lib/themes';
|
|
7
|
+
|
|
8
|
+
export function ThemesGalleryPage() {
|
|
9
|
+
const navigate = useNavigate();
|
|
10
|
+
const t = useLocale();
|
|
11
|
+
return (
|
|
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]">
|
|
17
|
+
{t.themes.title}
|
|
18
|
+
</h1>
|
|
19
|
+
<span className="folio ml-1 self-end pb-2">
|
|
20
|
+
{themeRegistry.length.toString().padStart(2, '0')}
|
|
21
|
+
</span>
|
|
22
|
+
</div>
|
|
23
|
+
</header>
|
|
24
|
+
<ThemesGallery onOpen={(id) => navigate(`/themes/${encodeURIComponent(id)}`)} />
|
|
25
|
+
</>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ThemeDetailPage() {
|
|
30
|
+
const { themeId } = useParams<{ themeId: string }>();
|
|
31
|
+
const navigate = useNavigate();
|
|
32
|
+
if (!themeId) return null;
|
|
33
|
+
return <ThemeDetail themeId={themeId} onBack={() => navigate('/themes')} />;
|
|
34
|
+
}
|