@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.
Files changed (110) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +108 -0
  3. package/bin.js +2 -0
  4. package/dist/build-TP72kiw7.js +35 -0
  5. package/dist/cli/bin.d.ts +1 -0
  6. package/dist/cli/bin.js +100 -0
  7. package/dist/config-C9or4m4J.d.ts +23 -0
  8. package/dist/config-L0IbktJ0.js +3135 -0
  9. package/dist/design-C13iz9_4.js +35 -0
  10. package/dist/dev-I2IyEh5W.js +97 -0
  11. package/dist/export-B-kBRWB1.js +4 -0
  12. package/dist/export-ic28osQP.js +131 -0
  13. package/dist/index.d.ts +56 -0
  14. package/dist/index.js +3 -0
  15. package/dist/locale/index.d.ts +24 -0
  16. package/dist/locale/index.js +881 -0
  17. package/dist/open-pages-plugin-D_DN_zjh.js +632 -0
  18. package/dist/preview-BrDuvgAN.js +21 -0
  19. package/dist/sync-Be_hxSOF.js +139 -0
  20. package/dist/sync-lKEXyPOh.js +3 -0
  21. package/dist/types-C5NCdDT-.d.ts +219 -0
  22. package/dist/update-CV62NhBT.js +37 -0
  23. package/dist/update-package-Cxt57pz7.js +152 -0
  24. package/dist/version-B6AiiGvT.js +26 -0
  25. package/dist/version-CeffgdfZ.js +3 -0
  26. package/dist/vite/index.d.ts +14 -0
  27. package/dist/vite/index.js +7 -0
  28. package/env.d.ts +59 -0
  29. package/package.json +106 -0
  30. package/skills/apply-comments/SKILL.md +86 -0
  31. package/skills/create-page/SKILL.md +92 -0
  32. package/skills/create-theme/SKILL.md +223 -0
  33. package/skills/current-page/SKILL.md +106 -0
  34. package/skills/page-authoring/SKILL.md +148 -0
  35. package/skills/page-authoring/references/assets-and-fonts.md +85 -0
  36. package/skills/page-authoring/references/html-pages.md +65 -0
  37. package/skills/page-authoring/references/interactivity.md +77 -0
  38. package/skills/page-authoring/references/layout-and-responsive.md +71 -0
  39. package/skills/page-authoring/references/typography-and-color.md +60 -0
  40. package/src/app/app.tsx +45 -0
  41. package/src/app/components/asset-view.tsx +1665 -0
  42. package/src/app/components/command/command-menu.tsx +227 -0
  43. package/src/app/components/command/command.tsx +142 -0
  44. package/src/app/components/command/home-command-menu.tsx +96 -0
  45. package/src/app/components/icon-tooltip.tsx +34 -0
  46. package/src/app/components/language-toggle.tsx +46 -0
  47. package/src/app/components/sidebar/folder-item.tsx +269 -0
  48. package/src/app/components/sidebar/icon-picker.tsx +61 -0
  49. package/src/app/components/sidebar/sidebar-footer.tsx +139 -0
  50. package/src/app/components/sidebar/sidebar.tsx +302 -0
  51. package/src/app/components/theme-toggle.tsx +66 -0
  52. package/src/app/components/themes/theme-detail.tsx +226 -0
  53. package/src/app/components/themes/themes-gallery.tsx +104 -0
  54. package/src/app/components/ui/badge.tsx +48 -0
  55. package/src/app/components/ui/button.tsx +93 -0
  56. package/src/app/components/ui/card.tsx +92 -0
  57. package/src/app/components/ui/context-menu.tsx +247 -0
  58. package/src/app/components/ui/dialog.tsx +155 -0
  59. package/src/app/components/ui/dropdown-menu.tsx +267 -0
  60. package/src/app/components/ui/input.tsx +25 -0
  61. package/src/app/components/ui/label.tsx +21 -0
  62. package/src/app/components/ui/popover.tsx +82 -0
  63. package/src/app/components/ui/progress.tsx +33 -0
  64. package/src/app/components/ui/scroll-area.tsx +53 -0
  65. package/src/app/components/ui/select.tsx +195 -0
  66. package/src/app/components/ui/separator.tsx +26 -0
  67. package/src/app/components/ui/slider.tsx +68 -0
  68. package/src/app/components/ui/sonner.tsx +48 -0
  69. package/src/app/components/ui/tabs.tsx +79 -0
  70. package/src/app/components/ui/textarea.tsx +22 -0
  71. package/src/app/components/ui/toggle-group.tsx +84 -0
  72. package/src/app/components/ui/toggle.tsx +45 -0
  73. package/src/app/components/ui/tooltip.tsx +75 -0
  74. package/src/app/favicon.ico +0 -0
  75. package/src/app/frame/inspect.ts +140 -0
  76. package/src/app/frame/main.tsx +91 -0
  77. package/src/app/frame.html +12 -0
  78. package/src/app/index.html +13 -0
  79. package/src/app/lib/asset-filter.test.ts +176 -0
  80. package/src/app/lib/asset-filter.ts +80 -0
  81. package/src/app/lib/assets.ts +257 -0
  82. package/src/app/lib/design.ts +58 -0
  83. package/src/app/lib/folders.ts +239 -0
  84. package/src/app/lib/frame.ts +38 -0
  85. package/src/app/lib/locale-store.ts +67 -0
  86. package/src/app/lib/page-thumb.tsx +46 -0
  87. package/src/app/lib/pages.ts +28 -0
  88. package/src/app/lib/sdk.ts +43 -0
  89. package/src/app/lib/themes.ts +22 -0
  90. package/src/app/lib/use-locale.ts +8 -0
  91. package/src/app/lib/use-page-module.ts +48 -0
  92. package/src/app/lib/use-page-titles.ts +29 -0
  93. package/src/app/lib/use-restart-server.ts +78 -0
  94. package/src/app/lib/utils.test.ts +25 -0
  95. package/src/app/lib/utils.ts +6 -0
  96. package/src/app/main.tsx +14 -0
  97. package/src/app/routes/assets.tsx +9 -0
  98. package/src/app/routes/home-shell.tsx +252 -0
  99. package/src/app/routes/home.tsx +846 -0
  100. package/src/app/routes/page.tsx +329 -0
  101. package/src/app/routes/themes.tsx +34 -0
  102. package/src/app/styles.css +405 -0
  103. package/src/app/virtual.d.ts +53 -0
  104. package/src/locale/en.ts +218 -0
  105. package/src/locale/format.ts +12 -0
  106. package/src/locale/index.ts +6 -0
  107. package/src/locale/ja.ts +221 -0
  108. package/src/locale/types.ts +215 -0
  109. package/src/locale/zh-cn.ts +218 -0
  110. 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
+ }