@vespercms/admin 0.0.0-stage → 0.1.0-next.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 (203) hide show
  1. package/README.md +119 -2
  2. package/dist/BodyEditor-CAGNfofv.js +3047 -0
  3. package/dist/a11y/FieldNotes.d.ts +23 -0
  4. package/dist/a11y/NotesDrawer.d.ts +23 -0
  5. package/dist/a11y/pdf.d.ts +3 -0
  6. package/dist/a11y/run.d.ts +3 -0
  7. package/dist/a11y/useNotes.d.ts +28 -0
  8. package/dist/app/App.d.ts +2 -0
  9. package/dist/app/Screen.d.ts +88 -0
  10. package/dist/app/UserMenu.d.ts +7 -0
  11. package/dist/app/api.d.ts +75 -0
  12. package/dist/app/appearance.d.ts +10 -0
  13. package/dist/app/browser-api.d.ts +17 -0
  14. package/dist/app/collection-icon.d.ts +12 -0
  15. package/dist/app/context.d.ts +23 -0
  16. package/dist/app/entries-changed.d.ts +11 -0
  17. package/dist/app/file-url.d.ts +13 -0
  18. package/dist/app/format.d.ts +4 -0
  19. package/dist/app/idb.d.ts +7 -0
  20. package/dist/app/load.d.ts +16 -0
  21. package/dist/app/media.d.ts +5 -0
  22. package/dist/app/navigation.d.ts +4 -0
  23. package/dist/app/pane-context.d.ts +19 -0
  24. package/dist/app/panes.d.ts +14 -0
  25. package/dist/app/router.d.ts +62 -0
  26. package/dist/body/BodyEditor.d.ts +42 -0
  27. package/dist/body/TableView.d.ts +2 -0
  28. package/dist/body/catalog.d.ts +47 -0
  29. package/dist/body/checks.d.ts +15 -0
  30. package/dist/body/chips.d.ts +31 -0
  31. package/dist/body/commands.d.ts +31 -0
  32. package/dist/body/document.d.ts +42 -0
  33. package/dist/body/insert.d.ts +69 -0
  34. package/dist/body/nodes.d.ts +38 -0
  35. package/dist/body/reveal.d.ts +13 -0
  36. package/dist/body/scroll.d.ts +16 -0
  37. package/dist/body/settings.d.ts +19 -0
  38. package/dist/body/shortcuts.d.ts +13 -0
  39. package/dist/body/table.d.ts +31 -0
  40. package/dist/body/ui.d.ts +30 -0
  41. package/dist/body/views.d.ts +9 -0
  42. package/dist/checks-C-0xDu-I.js +57 -0
  43. package/dist/cloud/CloudGate.d.ts +14 -0
  44. package/dist/cloud/access.d.ts +14 -0
  45. package/dist/cloud/changes.d.ts +26 -0
  46. package/dist/cloud/connection.d.ts +100 -0
  47. package/dist/cloud/editor.d.ts +44 -0
  48. package/dist/cloud/session.d.ts +62 -0
  49. package/dist/cloud/storage.d.ts +45 -0
  50. package/dist/editor/Field.d.ts +24 -0
  51. package/dist/editor/PagePicker.d.ts +20 -0
  52. package/dist/editor/arrange.d.ts +13 -0
  53. package/dist/editor/context.d.ts +23 -0
  54. package/dist/editor/copy.d.ts +9 -0
  55. package/dist/editor/reveal.d.ts +7 -0
  56. package/dist/editor/target.d.ts +21 -0
  57. package/dist/editor/values.d.ts +18 -0
  58. package/dist/geo-map-CT3OGD7O.js +110 -0
  59. package/dist/github/GitHubGate.d.ts +15 -0
  60. package/dist/github/api.d.ts +20 -0
  61. package/dist/github/cache.d.ts +2 -0
  62. package/dist/github/session.d.ts +35 -0
  63. package/dist/index.d.ts +3 -0
  64. package/dist/markdown-KUkv11EB.js +211 -0
  65. package/dist/media/MediaPicker.d.ts +28 -0
  66. package/dist/media/file-icon.d.ts +3 -0
  67. package/dist/media/formats.d.ts +23 -0
  68. package/dist/media/hygiene.d.ts +37 -0
  69. package/dist/media/upload.d.ts +31 -0
  70. package/dist/mount.d.ts +3 -0
  71. package/dist/pdf-m5s_uXJ3.js +24 -0
  72. package/dist/run-y4XRkg17.js +7 -0
  73. package/dist/schedule/ScheduleDialog.d.ts +28 -0
  74. package/dist/schedule/ScheduleSummary.d.ts +12 -0
  75. package/dist/schedule/ScheduledView.d.ts +8 -0
  76. package/dist/schedule/cloud.d.ts +21 -0
  77. package/dist/schedule/now.d.ts +5 -0
  78. package/dist/schedule/times.d.ts +37 -0
  79. package/dist/screens/EntryEditor.d.ts +13 -0
  80. package/dist/screens/EntryList.d.ts +18 -0
  81. package/dist/screens/Home.d.ts +6 -0
  82. package/dist/screens/ListPane.d.ts +6 -0
  83. package/dist/screens/MediaBrowser.d.ts +7 -0
  84. package/dist/screens/NotFound.d.ts +3 -0
  85. package/dist/screens/PageTree.d.ts +10 -0
  86. package/dist/screens/Redirects.d.ts +1 -0
  87. package/dist/screens/Unavailable.d.ts +8 -0
  88. package/dist/shortcuts-B_JMAvF9.js +4630 -0
  89. package/dist/studio.css +3 -0
  90. package/dist/studio.js +6312 -0
  91. package/dist/types.d.ts +76 -0
  92. package/dist/widgets/basic.d.ts +25 -0
  93. package/dist/widgets/crops.d.ts +43 -0
  94. package/dist/widgets/focal-point.d.ts +15 -0
  95. package/dist/widgets/geo-map.d.ts +10 -0
  96. package/dist/widgets/geo.d.ts +16 -0
  97. package/dist/widgets/group.d.ts +41 -0
  98. package/dist/widgets/icon.d.ts +9 -0
  99. package/dist/widgets/image.d.ts +16 -0
  100. package/dist/widgets/markdown-lazy.d.ts +2 -0
  101. package/dist/widgets/markdown.d.ts +2 -0
  102. package/dist/widgets/reference.d.ts +12 -0
  103. package/dist/widgets/registry.d.ts +32 -0
  104. package/dist/widgets/special.d.ts +20 -0
  105. package/dist/widgets/union.d.ts +36 -0
  106. package/package.json +63 -4
  107. package/src/a11y/FieldNotes.tsx +56 -0
  108. package/src/a11y/NotesDrawer.tsx +81 -0
  109. package/src/a11y/pdf.ts +45 -0
  110. package/src/a11y/run.ts +12 -0
  111. package/src/a11y/useNotes.ts +139 -0
  112. package/src/app/App.tsx +156 -0
  113. package/src/app/Screen.tsx +416 -0
  114. package/src/app/UserMenu.tsx +95 -0
  115. package/src/app/api.ts +176 -0
  116. package/src/app/appearance.ts +54 -0
  117. package/src/app/browser-api.ts +173 -0
  118. package/src/app/collection-icon.ts +15 -0
  119. package/src/app/context.tsx +32 -0
  120. package/src/app/entries-changed.ts +48 -0
  121. package/src/app/file-url.tsx +44 -0
  122. package/src/app/format.ts +39 -0
  123. package/src/app/idb.ts +82 -0
  124. package/src/app/load.ts +40 -0
  125. package/src/app/media.ts +20 -0
  126. package/src/app/navigation.ts +4 -0
  127. package/src/app/pane-context.ts +25 -0
  128. package/src/app/panes.tsx +513 -0
  129. package/src/app/router.ts +158 -0
  130. package/src/body/BodyEditor.tsx +1139 -0
  131. package/src/body/TableView.tsx +700 -0
  132. package/src/body/catalog.ts +198 -0
  133. package/src/body/checks.ts +83 -0
  134. package/src/body/chips.ts +150 -0
  135. package/src/body/commands.ts +191 -0
  136. package/src/body/document.ts +388 -0
  137. package/src/body/insert.tsx +301 -0
  138. package/src/body/nodes.ts +287 -0
  139. package/src/body/reveal.ts +93 -0
  140. package/src/body/scroll.ts +53 -0
  141. package/src/body/settings.tsx +510 -0
  142. package/src/body/shortcuts.ts +31 -0
  143. package/src/body/table.ts +192 -0
  144. package/src/body/ui.ts +77 -0
  145. package/src/body/views.tsx +268 -0
  146. package/src/build.ts +6 -0
  147. package/src/cloud/CloudGate.tsx +245 -0
  148. package/src/cloud/access.ts +52 -0
  149. package/src/cloud/changes.tsx +474 -0
  150. package/src/cloud/connection.ts +281 -0
  151. package/src/cloud/editor.tsx +250 -0
  152. package/src/cloud/session.ts +289 -0
  153. package/src/cloud/storage.ts +212 -0
  154. package/src/editor/Field.tsx +182 -0
  155. package/src/editor/PagePicker.tsx +102 -0
  156. package/src/editor/arrange.ts +98 -0
  157. package/src/editor/context.tsx +31 -0
  158. package/src/editor/copy.ts +30 -0
  159. package/src/editor/reveal.ts +38 -0
  160. package/src/editor/target.ts +104 -0
  161. package/src/editor/values.ts +71 -0
  162. package/src/github/GitHubGate.tsx +261 -0
  163. package/src/github/api.ts +81 -0
  164. package/src/github/cache.ts +64 -0
  165. package/src/github/session.ts +146 -0
  166. package/src/index.ts +10 -0
  167. package/src/media/MediaPicker.tsx +302 -0
  168. package/src/media/file-icon.ts +17 -0
  169. package/src/media/formats.ts +262 -0
  170. package/src/media/hygiene.ts +220 -0
  171. package/src/media/upload.ts +47 -0
  172. package/src/mount.tsx +20 -0
  173. package/src/schedule/ScheduleDialog.tsx +300 -0
  174. package/src/schedule/ScheduleSummary.tsx +59 -0
  175. package/src/schedule/ScheduledView.tsx +294 -0
  176. package/src/schedule/cloud.ts +66 -0
  177. package/src/schedule/now.ts +18 -0
  178. package/src/schedule/times.ts +133 -0
  179. package/src/screens/EntryEditor.tsx +1803 -0
  180. package/src/screens/EntryList.tsx +274 -0
  181. package/src/screens/Home.tsx +131 -0
  182. package/src/screens/ListPane.tsx +459 -0
  183. package/src/screens/MediaBrowser.tsx +253 -0
  184. package/src/screens/NotFound.tsx +19 -0
  185. package/src/screens/PageTree.tsx +718 -0
  186. package/src/screens/Redirects.tsx +522 -0
  187. package/src/screens/Unavailable.tsx +27 -0
  188. package/src/studio.css +163 -0
  189. package/src/types.ts +82 -0
  190. package/src/widgets/basic.tsx +311 -0
  191. package/src/widgets/crops.tsx +399 -0
  192. package/src/widgets/focal-point.tsx +240 -0
  193. package/src/widgets/geo-map.tsx +160 -0
  194. package/src/widgets/geo.tsx +170 -0
  195. package/src/widgets/group.tsx +827 -0
  196. package/src/widgets/icon.tsx +47 -0
  197. package/src/widgets/image.tsx +407 -0
  198. package/src/widgets/markdown-lazy.tsx +22 -0
  199. package/src/widgets/markdown.tsx +267 -0
  200. package/src/widgets/reference.tsx +196 -0
  201. package/src/widgets/registry.ts +104 -0
  202. package/src/widgets/special.tsx +286 -0
  203. package/src/widgets/union.tsx +233 -0
@@ -0,0 +1,253 @@
1
+ import { dirOf, extensionOf, isImagePath, type MediaItem } from '@vespercms/core';
2
+ import { Button, Dialog, Icon, Select, TextField } from '@vespercms/ui';
3
+ import { useMemo, useRef, useState } from 'react';
4
+ import { FileImage } from '../app/file-url.tsx';
5
+ import { useStudio } from '../app/context.tsx';
6
+ import { useLoad } from '../app/load.ts';
7
+ import { hrefFor } from '../app/router.ts';
8
+ import { Screen } from '../app/Screen.tsx';
9
+ import { fileSize } from '../media/hygiene.ts';
10
+ import { fileIcon } from '../media/file-icon.ts';
11
+ import { UploadStatus } from '../media/MediaPicker.tsx';
12
+ import { checkUpload, sendUpload, type UploadStep } from '../media/upload.ts';
13
+
14
+ type Show = 'all' | 'unused';
15
+
16
+ /**
17
+ * The media browser (spec §6.6.6): every image and file in the site's
18
+ * assets folders, with where each is used. Search by name, filter by
19
+ * folder, and an "Unused" view for cleaning up. Deleting is confirmed, and
20
+ * only possible for files nothing uses.
21
+ */
22
+ export function MediaBrowser() {
23
+ const { api, manifest } = useStudio();
24
+ const maxEdge = manifest.site.images?.maxUploadEdge;
25
+ const [load, reload] = useLoad(() => api.listMedia(), 'media');
26
+ const [query, setQuery] = useState('');
27
+ const [folder, setFolder] = useState('all');
28
+ const [show, setShow] = useState<Show>('all');
29
+ const [deleting, setDeleting] = useState<MediaItem | null>(null);
30
+ const [step, setStep] = useState<UploadStep | { step: 'working'; text: string } | null>(null);
31
+ const [message, setMessage] = useState('');
32
+ const input = useRef<HTMLInputElement>(null);
33
+
34
+ const items = useMemo(() => (load.state === 'ready' ? load.value : []), [load]);
35
+ const folders = useMemo(() => [...new Set(items.map((i) => dirOf(i.path)))].sort(), [items]);
36
+ const shown = items.filter(
37
+ (item) =>
38
+ (folder === 'all' || dirOf(item.path) === folder) &&
39
+ (show === 'all' || item.usedBy.length === 0) &&
40
+ (query.trim() === '' || item.path.toLowerCase().includes(query.trim().toLowerCase())),
41
+ );
42
+ const uploadFolder = folder === 'all' ? `${manifest.site.srcDir}/assets` : folder;
43
+
44
+ const upload = async (ready: Extract<UploadStep, { upload: unknown }>) => {
45
+ setStep({ step: 'working', text: 'Uploading…' });
46
+ const result = await sendUpload(api, ready.upload, uploadFolder);
47
+ if (result.ok) {
48
+ setStep(null);
49
+ setMessage(`Added ${result.path}. ${ready.upload.notes.join('. ')}`.trim());
50
+ reload();
51
+ } else {
52
+ setStep({ step: 'refused', message: result.message });
53
+ }
54
+ };
55
+
56
+ return (
57
+ <Screen title="Media" afterglow={load.state === 'ready' && items.length === 0}>
58
+ <div className="mb-6 flex flex-wrap items-end gap-4">
59
+ <TextField
60
+ label="Search by file name"
61
+ type="search"
62
+ icon="search"
63
+ value={query}
64
+ onChange={setQuery}
65
+ className="w-full max-w-xs"
66
+ />
67
+ <Select
68
+ label="Folder"
69
+ value={folder}
70
+ onChange={(value) => {
71
+ setFolder(value ?? 'all');
72
+ }}
73
+ options={[
74
+ { value: 'all', label: 'All folders' },
75
+ ...folders.map((f) => ({ value: f, label: f })),
76
+ ]}
77
+ className="w-56"
78
+ />
79
+ <Select
80
+ label="Show"
81
+ value={show}
82
+ onChange={(value) => {
83
+ setShow((value ?? 'all') as Show);
84
+ }}
85
+ options={[
86
+ { value: 'all', label: 'All files' },
87
+ { value: 'unused', label: 'Unused files' },
88
+ ]}
89
+ className="w-44"
90
+ />
91
+ <input
92
+ ref={input}
93
+ type="file"
94
+ tabIndex={-1}
95
+ aria-hidden="true"
96
+ className="sr-only"
97
+ onChange={(event) => {
98
+ const file = event.target.files?.[0];
99
+ event.target.value = '';
100
+ if (!file) return;
101
+ setMessage('');
102
+ setStep({ step: 'working', text: `Preparing ${file.name}…` });
103
+ void checkUpload(file, items, { accept: 'any', ...(maxEdge ? { maxEdge } : {}) }).then(
104
+ (next) => {
105
+ if (next.step === 'ready') void upload(next);
106
+ else setStep(next);
107
+ },
108
+ );
109
+ }}
110
+ />
111
+ <Button
112
+ variant="primary"
113
+ icon="upload"
114
+ className="ml-auto"
115
+ onPress={() => {
116
+ input.current?.click();
117
+ }}
118
+ >
119
+ Upload
120
+ </Button>
121
+ </div>
122
+ <div className="mb-6 flex flex-col gap-2">
123
+ <UploadStatus
124
+ step={step}
125
+ onUpload={(ready) => void upload(ready)}
126
+ onReuse={(path) => {
127
+ setStep(null);
128
+ setMessage(`${path} is already in the site, so nothing was added.`);
129
+ }}
130
+ onCancel={() => {
131
+ setStep(null);
132
+ }}
133
+ />
134
+ <p role="status" className="text-small text-content-muted">
135
+ {message}
136
+ </p>
137
+ </div>
138
+
139
+ {load.state === 'loading' && <p className="text-small text-content-muted">Loading files…</p>}
140
+ {load.state === 'failed' && (
141
+ <p role="alert" className="text-danger">
142
+ Couldn’t list files: {load.message}
143
+ </p>
144
+ )}
145
+ {load.state === 'ready' && shown.length === 0 && (
146
+ <p className="text-content-muted">
147
+ {items.length === 0 ? 'This site has no images or files yet.' : 'No files match.'}
148
+ </p>
149
+ )}
150
+ <ul className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
151
+ {shown.map((item) => (
152
+ <li key={item.path} className="flex flex-col gap-2 rounded-lg border border-border p-3">
153
+ {isImagePath(item.path) ? (
154
+ <FileImage
155
+ path={item.path}
156
+ loading="lazy"
157
+ className="h-36 w-full rounded-md bg-surface-sunken object-contain"
158
+ />
159
+ ) : (
160
+ <span
161
+ aria-hidden="true"
162
+ className="flex h-36 flex-col items-center justify-center gap-2 rounded-md bg-surface-sunken text-caption text-content-muted uppercase"
163
+ >
164
+ <Icon name={fileIcon(extensionOf(item.path))} size={32} />
165
+ {extensionOf(item.path)}
166
+ </span>
167
+ )}
168
+ <h2 className="text-label break-all text-content">
169
+ {item.path.slice(item.path.lastIndexOf('/') + 1)}
170
+ </h2>
171
+ <p className="font-mono text-caption break-all text-content-muted">
172
+ {dirOf(item.path)} · {fileSize(item.size)}
173
+ </p>
174
+ <UsedOn item={item} />
175
+ {item.usedBy.length === 0 && (
176
+ <div>
177
+ <Button
178
+ variant="quiet"
179
+ onPress={() => {
180
+ setDeleting(item);
181
+ }}
182
+ aria-label={`Delete ${item.path}`}
183
+ >
184
+ Delete
185
+ </Button>
186
+ </div>
187
+ )}
188
+ </li>
189
+ ))}
190
+ </ul>
191
+
192
+ <Dialog
193
+ title="Delete this file?"
194
+ role="alertdialog"
195
+ isOpen={deleting !== null}
196
+ onOpenChange={(open) => {
197
+ if (!open) setDeleting(null);
198
+ }}
199
+ >
200
+ <p className="mb-6">
201
+ {deleting?.path} isn’t used on any page. Deleting removes it from the site; git can bring
202
+ it back.
203
+ </p>
204
+ <div className="flex flex-wrap justify-end gap-3">
205
+ <Button
206
+ onPress={() => {
207
+ setDeleting(null);
208
+ }}
209
+ >
210
+ Cancel
211
+ </Button>
212
+ <Button
213
+ variant="danger"
214
+ onPress={() => {
215
+ const target = deleting;
216
+ setDeleting(null);
217
+ if (!target) return;
218
+ void api.deleteMedia(target.path).then((result) => {
219
+ setMessage(
220
+ result.ok ? `Deleted ${target.path}.` : 'message' in result ? result.message : '',
221
+ );
222
+ reload();
223
+ });
224
+ }}
225
+ >
226
+ Delete
227
+ </Button>
228
+ </div>
229
+ </Dialog>
230
+ </Screen>
231
+ );
232
+ }
233
+
234
+ function UsedOn({ item }: { item: MediaItem }) {
235
+ if (item.usedBy.length === 0)
236
+ return <p className="text-small text-content-muted">Not used on any page</p>;
237
+ return (
238
+ <p className="text-small">
239
+ Used on{' '}
240
+ {item.usedBy.map((use, index) => (
241
+ <span key={`${use.collection}/${use.id}`}>
242
+ {index > 0 && ', '}
243
+ <a
244
+ href={hrefFor({ screen: 'edit', collection: use.collection, id: use.id })}
245
+ className="text-accent underline underline-offset-2"
246
+ >
247
+ {use.title}
248
+ </a>
249
+ </span>
250
+ ))}
251
+ </p>
252
+ );
253
+ }
@@ -0,0 +1,19 @@
1
+ import { Link } from '@vespercms/ui';
2
+ import { hrefFor } from '../app/router.ts';
3
+ import { Screen } from '../app/Screen.tsx';
4
+
5
+ export function NotFound({
6
+ message = 'This page doesn’t exist in the studio.',
7
+ }: {
8
+ message?: string;
9
+ }) {
10
+ return (
11
+ <Screen title="Not found" afterglow>
12
+ <p className="mb-4 max-w-2xl">{message}</p>
13
+
14
+ <Link icon="back" href={hrefFor({ screen: 'home' })}>
15
+ Back to content
16
+ </Link>
17
+ </Screen>
18
+ );
19
+ }