@vespercms/admin 0.0.0-stage → 0.1.0-next.1

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 (207) hide show
  1. package/README.md +119 -2
  2. package/dist/BodyEditor-_mkh0BSQ.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-4fVeej3c.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 +35 -0
  79. package/dist/screens/EntryEditor.d.ts +13 -0
  80. package/dist/screens/EntryList.d.ts +16 -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/screens/status.d.ts +36 -0
  89. package/dist/shortcuts-CarKeDqp.js +4666 -0
  90. package/dist/studio.css +3 -0
  91. package/dist/studio.js +6449 -0
  92. package/dist/types.d.ts +76 -0
  93. package/dist/widgets/basic.d.ts +25 -0
  94. package/dist/widgets/blank-date.d.ts +8 -0
  95. package/dist/widgets/crops.d.ts +43 -0
  96. package/dist/widgets/focal-point.d.ts +15 -0
  97. package/dist/widgets/geo-map.d.ts +10 -0
  98. package/dist/widgets/geo.d.ts +16 -0
  99. package/dist/widgets/group.d.ts +41 -0
  100. package/dist/widgets/icon.d.ts +9 -0
  101. package/dist/widgets/image.d.ts +16 -0
  102. package/dist/widgets/markdown-lazy.d.ts +2 -0
  103. package/dist/widgets/markdown.d.ts +2 -0
  104. package/dist/widgets/reference.d.ts +12 -0
  105. package/dist/widgets/registry.d.ts +32 -0
  106. package/dist/widgets/special.d.ts +20 -0
  107. package/dist/widgets/union.d.ts +36 -0
  108. package/package.json +63 -4
  109. package/src/a11y/FieldNotes.tsx +56 -0
  110. package/src/a11y/NotesDrawer.tsx +81 -0
  111. package/src/a11y/pdf.ts +45 -0
  112. package/src/a11y/run.ts +12 -0
  113. package/src/a11y/useNotes.ts +139 -0
  114. package/src/app/App.tsx +156 -0
  115. package/src/app/Screen.tsx +416 -0
  116. package/src/app/UserMenu.tsx +95 -0
  117. package/src/app/api.ts +176 -0
  118. package/src/app/appearance.ts +54 -0
  119. package/src/app/browser-api.ts +173 -0
  120. package/src/app/collection-icon.ts +15 -0
  121. package/src/app/context.tsx +32 -0
  122. package/src/app/entries-changed.ts +48 -0
  123. package/src/app/file-url.tsx +44 -0
  124. package/src/app/format.ts +39 -0
  125. package/src/app/idb.ts +82 -0
  126. package/src/app/load.ts +40 -0
  127. package/src/app/media.ts +20 -0
  128. package/src/app/navigation.ts +4 -0
  129. package/src/app/pane-context.ts +25 -0
  130. package/src/app/panes.tsx +513 -0
  131. package/src/app/router.ts +158 -0
  132. package/src/body/BodyEditor.tsx +1139 -0
  133. package/src/body/TableView.tsx +700 -0
  134. package/src/body/catalog.ts +198 -0
  135. package/src/body/checks.ts +83 -0
  136. package/src/body/chips.ts +150 -0
  137. package/src/body/commands.ts +191 -0
  138. package/src/body/document.ts +388 -0
  139. package/src/body/insert.tsx +301 -0
  140. package/src/body/nodes.ts +287 -0
  141. package/src/body/reveal.ts +93 -0
  142. package/src/body/scroll.ts +53 -0
  143. package/src/body/settings.tsx +510 -0
  144. package/src/body/shortcuts.ts +31 -0
  145. package/src/body/table.ts +192 -0
  146. package/src/body/ui.ts +77 -0
  147. package/src/body/views.tsx +268 -0
  148. package/src/build.ts +6 -0
  149. package/src/cloud/CloudGate.tsx +245 -0
  150. package/src/cloud/access.ts +52 -0
  151. package/src/cloud/changes.tsx +474 -0
  152. package/src/cloud/connection.ts +281 -0
  153. package/src/cloud/editor.tsx +250 -0
  154. package/src/cloud/session.ts +289 -0
  155. package/src/cloud/storage.ts +212 -0
  156. package/src/editor/Field.tsx +182 -0
  157. package/src/editor/PagePicker.tsx +102 -0
  158. package/src/editor/arrange.ts +98 -0
  159. package/src/editor/context.tsx +31 -0
  160. package/src/editor/copy.ts +30 -0
  161. package/src/editor/reveal.ts +38 -0
  162. package/src/editor/target.ts +104 -0
  163. package/src/editor/values.ts +71 -0
  164. package/src/github/GitHubGate.tsx +298 -0
  165. package/src/github/api.ts +81 -0
  166. package/src/github/cache.ts +64 -0
  167. package/src/github/session.ts +146 -0
  168. package/src/index.ts +10 -0
  169. package/src/media/MediaPicker.tsx +302 -0
  170. package/src/media/file-icon.ts +17 -0
  171. package/src/media/formats.ts +262 -0
  172. package/src/media/hygiene.ts +220 -0
  173. package/src/media/upload.ts +47 -0
  174. package/src/mount.tsx +20 -0
  175. package/src/schedule/ScheduleDialog.tsx +303 -0
  176. package/src/schedule/ScheduleSummary.tsx +59 -0
  177. package/src/schedule/ScheduledView.tsx +294 -0
  178. package/src/schedule/cloud.ts +66 -0
  179. package/src/schedule/now.ts +18 -0
  180. package/src/schedule/times.ts +122 -0
  181. package/src/screens/EntryEditor.tsx +1817 -0
  182. package/src/screens/EntryList.tsx +272 -0
  183. package/src/screens/Home.tsx +131 -0
  184. package/src/screens/ListPane.tsx +477 -0
  185. package/src/screens/MediaBrowser.tsx +253 -0
  186. package/src/screens/NotFound.tsx +19 -0
  187. package/src/screens/PageTree.tsx +738 -0
  188. package/src/screens/Redirects.tsx +522 -0
  189. package/src/screens/Unavailable.tsx +27 -0
  190. package/src/screens/status.tsx +104 -0
  191. package/src/studio.css +168 -0
  192. package/src/types.ts +82 -0
  193. package/src/widgets/basic.tsx +315 -0
  194. package/src/widgets/blank-date.ts +32 -0
  195. package/src/widgets/crops.tsx +399 -0
  196. package/src/widgets/focal-point.tsx +240 -0
  197. package/src/widgets/geo-map.tsx +160 -0
  198. package/src/widgets/geo.tsx +170 -0
  199. package/src/widgets/group.tsx +827 -0
  200. package/src/widgets/icon.tsx +47 -0
  201. package/src/widgets/image.tsx +407 -0
  202. package/src/widgets/markdown-lazy.tsx +22 -0
  203. package/src/widgets/markdown.tsx +267 -0
  204. package/src/widgets/reference.tsx +196 -0
  205. package/src/widgets/registry.ts +104 -0
  206. package/src/widgets/special.tsx +290 -0
  207. package/src/widgets/union.tsx +233 -0
@@ -0,0 +1,245 @@
1
+ import { Button } from '@vespercms/ui';
2
+ import { useCallback, useEffect, useMemo, useState } from 'react';
3
+ import { App } from '../app/App.tsx';
4
+ import { startSignIn as startGitHubSignIn, type GitHubStudioSettings } from '../github/session.ts';
5
+ import type { Account, StudioOptions } from '../types.ts';
6
+ import { connect } from './connection.ts';
7
+ import {
8
+ endCloudSession,
9
+ finishCloudSignIn,
10
+ resumeCloudSession,
11
+ startCloudSignIn,
12
+ type CloudSettings,
13
+ type CloudSignedIn,
14
+ type StoredSession,
15
+ } from './session.ts';
16
+ import type { KeyValueStore } from '../app/idb.ts';
17
+
18
+ /*
19
+ * The cloud's way in (spec §6.9): finish a sign-in the app sent back here,
20
+ * or pick up one this browser already has, then the studio connected to
21
+ * the cloud. When the site also has GitHub mode, signing in with GitHub is
22
+ * the second way in. One primary action per screen (spec §10.4).
23
+ */
24
+
25
+ type GateOptions = Omit<StudioOptions, 'api' | 'github' | 'account' | 'cloud' | 'connection'>;
26
+
27
+ type State =
28
+ | { kind: 'checking' }
29
+ | { kind: 'signed-out'; message?: string }
30
+ | { kind: 'unreachable' }
31
+ | ({ kind: 'ready' } & CloudSignedIn);
32
+
33
+ const noProps = () => Promise.resolve({});
34
+
35
+ /** How often a visible studio asks for drafts and reviews: three small requests, the tree cached by commit in the cloud. */
36
+ const CHECK_EVERY = 60_000;
37
+
38
+ export function CloudGate({
39
+ settings,
40
+ github,
41
+ options,
42
+ fetcher,
43
+ store,
44
+ }: {
45
+ settings: CloudSettings;
46
+ github?: GitHubStudioSettings | null;
47
+ options: GateOptions;
48
+ fetcher?: typeof fetch;
49
+ /** Where the key and tokens are kept: IndexedDB unless a test passes its own. */
50
+ store?: KeyValueStore<StoredSession>;
51
+ }) {
52
+ const [state, setState] = useState<State>({ kind: 'checking' });
53
+ const [expired, setExpired] = useState(false);
54
+ const [attempt, setAttempt] = useState(0);
55
+ const reach = useMemo(
56
+ () => ({ ...(fetcher ? { fetch: fetcher } : {}), ...(store ? { store } : {}) }),
57
+ [fetcher, store],
58
+ );
59
+
60
+ useEffect(() => {
61
+ let live = true;
62
+ const set = (next: State) => {
63
+ if (live) setState(next);
64
+ };
65
+ void (async () => {
66
+ const finished = await finishCloudSignIn(settings, reach);
67
+ if (finished) {
68
+ set(
69
+ finished.ok
70
+ ? { kind: 'ready', ...finished }
71
+ : { kind: 'signed-out', message: finished.message },
72
+ );
73
+ return;
74
+ }
75
+ const resumed = await resumeCloudSession(settings, reach);
76
+ if (resumed.kind === 'signed-in') set({ ...resumed, kind: 'ready' });
77
+ else set({ kind: resumed.kind });
78
+ })();
79
+ return () => {
80
+ live = false;
81
+ };
82
+ // `attempt` reruns the check after "Try again".
83
+ }, [settings, reach, attempt]);
84
+
85
+ const signIn = useCallback(() => {
86
+ void startCloudSignIn(settings, window.location.hash, store);
87
+ }, [settings, store]);
88
+ const client = state.kind === 'ready' ? state.client : null;
89
+ const leave = useCallback(() => {
90
+ void endCloudSession(settings, client, store).then(() => {
91
+ setExpired(false);
92
+ setState({ kind: 'signed-out' });
93
+ });
94
+ }, [settings, client, store]);
95
+
96
+ const ready = state.kind === 'ready' ? state : null;
97
+ // One connection per sign-in, so its caches and drafts last the whole visit.
98
+ const connected = useMemo(
99
+ () =>
100
+ ready
101
+ ? connect({
102
+ settings,
103
+ client: ready.client,
104
+ user: ready.user,
105
+ member: ready.member,
106
+ siteName: ready.siteName,
107
+ manifest: options.manifest,
108
+ loadMdxProps: options.loadMdxProps ?? noProps,
109
+ ...(options.loadUrlFunctions ? { loadUrlFunctions: options.loadUrlFunctions } : {}),
110
+ ...(options.routes ? { routes: options.routes } : {}),
111
+ onExpired: () => {
112
+ setExpired(true);
113
+ },
114
+ })
115
+ : null,
116
+ [ready, settings, options],
117
+ );
118
+ useEffect(() => {
119
+ if (!connected) return;
120
+ const drafts = connected.connection.drafts;
121
+ // One check at a time: coming back into view and the timer can coincide.
122
+ let checking = false;
123
+ const check = () => {
124
+ if (checking) return;
125
+ checking = true;
126
+ void drafts
127
+ .refresh()
128
+ .catch(() => undefined)
129
+ .finally(() => {
130
+ checking = false;
131
+ });
132
+ };
133
+ check();
134
+ // Other people's drafts and reviews arrive while this tab is open: an
135
+ // approver's studio, open before a review was requested, showed nothing
136
+ // of it until reloaded. A hidden tab doesn't ask.
137
+ const whenSeen = () => {
138
+ if (document.visibilityState === 'visible') check();
139
+ };
140
+ document.addEventListener('visibilitychange', whenSeen);
141
+ const timer = window.setInterval(whenSeen, CHECK_EVERY);
142
+ return () => {
143
+ document.removeEventListener('visibilitychange', whenSeen);
144
+ window.clearInterval(timer);
145
+ };
146
+ }, [connected]);
147
+
148
+ const account = useMemo<Account | null>(
149
+ () =>
150
+ connected
151
+ ? {
152
+ label: connected.connection.user.name,
153
+ name: connected.connection.user.name,
154
+ ...(connected.connection.user.avatar
155
+ ? { picture: connected.connection.user.avatar }
156
+ : {}),
157
+ description: `Signed in to Vesper as ${connected.connection.user.name}`,
158
+ signOutLabel: 'Sign out of Vesper',
159
+ dashboardUrl: connected.connection.dashboardUrl,
160
+ signOut: leave,
161
+ signInAgain: signIn,
162
+ expired,
163
+ }
164
+ : null,
165
+ [connected, leave, signIn, expired],
166
+ );
167
+
168
+ useEffect(() => {
169
+ // Signed in, each screen names the page itself.
170
+ if (state.kind !== 'ready')
171
+ document.title = `${titleOf(state)} · ${options.siteName ?? 'Vesper'}`;
172
+ }, [state, options.siteName]);
173
+
174
+ if (connected && account) {
175
+ return (
176
+ <App {...options} api={connected.api} account={account} connection={connected.connection} />
177
+ );
178
+ }
179
+
180
+ return (
181
+ <div className="afterglow min-h-screen">
182
+ <main id="main" className="mx-auto flex max-w-2xl flex-col gap-4 px-4 py-16">
183
+ <h1 className="text-heading-1 text-content">{titleOf(state)}</h1>
184
+ {state.kind === 'checking' && (
185
+ <p role="status" className="text-content-muted">
186
+ Opening the studio…
187
+ </p>
188
+ )}
189
+ {state.kind === 'signed-out' && (
190
+ <>
191
+ {state.message && (
192
+ <p role="alert" className="rounded-md border border-danger bg-surface-raised p-4">
193
+ {state.message}
194
+ </p>
195
+ )}
196
+ <p>
197
+ Sign in with your Vesper account. Your changes are kept as drafts until they’re
198
+ published.
199
+ </p>
200
+ <div className="flex flex-wrap gap-3">
201
+ <Button variant="primary" onPress={signIn}>
202
+ Sign in with Vesper
203
+ </Button>
204
+ {github && (
205
+ <Button onPress={() => void startGitHubSignIn(github, window.location.hash)}>
206
+ Sign in with GitHub
207
+ </Button>
208
+ )}
209
+ </div>
210
+ </>
211
+ )}
212
+ {state.kind === 'unreachable' && (
213
+ <>
214
+ <p role="alert">
215
+ Vesper can’t be reached. Check the connection and try again. Anything you were editing
216
+ is kept on this device.
217
+ </p>
218
+ <div>
219
+ <Button
220
+ variant="primary"
221
+ onPress={() => {
222
+ setState({ kind: 'checking' });
223
+ setAttempt((n) => n + 1);
224
+ }}
225
+ >
226
+ Try again
227
+ </Button>
228
+ </div>
229
+ </>
230
+ )}
231
+ </main>
232
+ </div>
233
+ );
234
+ }
235
+
236
+ function titleOf(state: State): string {
237
+ switch (state.kind) {
238
+ case 'checking':
239
+ return 'Opening the studio';
240
+ case 'unreachable':
241
+ return 'Vesper couldn’t be reached';
242
+ default:
243
+ return 'Sign in to edit this site';
244
+ }
245
+ }
@@ -0,0 +1,52 @@
1
+ import { editsInCollection, grantMatches, type CollectionManifest } from '@vespercms/core';
2
+ import type { CloudConnection } from './connection.ts';
3
+
4
+ /*
5
+ * What the studio shows a member of a cloud site (spec §7.5): it hides what
6
+ * they can't do instead of showing it disabled. Viewers see everything,
7
+ * read-only. Without the cloud, everything is shown.
8
+ */
9
+
10
+ /** True when a collection belongs on this member's menus and home screen. */
11
+ export function shownTo(cloud: CloudConnection | undefined, collection: string): boolean {
12
+ if (!cloud || cloud.member.role === 'viewer') return true;
13
+ return editsInCollection(cloud.member, collection);
14
+ }
15
+
16
+ /** True when an entry file belongs in this member's lists. */
17
+ export function entryShownTo(cloud: CloudConnection | undefined, path: string): boolean {
18
+ if (!cloud || cloud.member.role === 'viewer' || cloud.wholeSite) return true;
19
+ return cloud.access(path).edit;
20
+ }
21
+
22
+ /** True when the member may start a new entry at the top of a collection (or in `parent`, in a tree). */
23
+ export function mayCreateIn(
24
+ cloud: CloudConnection | undefined,
25
+ collection: CollectionManifest,
26
+ parent?: string,
27
+ ): boolean {
28
+ if (!cloud) return true;
29
+ const source = collection.source;
30
+ if (source.loader === 'file') return cloud.access(source.path).edit;
31
+ if (source.loader !== 'glob') return false;
32
+ const folder = [source.base === '.' ? '' : source.base, parent ?? ''].filter(Boolean).join('/');
33
+ return cloud.access(`${folder ? `${folder}/` : ''}new-entry.${source.formats[0] ?? 'md'}`).edit;
34
+ }
35
+
36
+ /**
37
+ * True when the member may change a page at this place in a tree (spec
38
+ * §6.15.2, §7.5). A tree path is a grant key: `admissions` is the
39
+ * Admissions section page, `admissions/apply` a page under it.
40
+ */
41
+ export function treeCovered(
42
+ cloud: CloudConnection | undefined,
43
+ collection: string,
44
+ path: string,
45
+ ): boolean {
46
+ if (!cloud) return true;
47
+ if (cloud.member.role === 'viewer') return false;
48
+ if (cloud.wholeSite) return true;
49
+ return (cloud.member.grants ?? []).some(
50
+ (grant) => grant.collection === collection && grantMatches(grant.match, path),
51
+ );
52
+ }