@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,139 @@
1
+ import {
2
+ resolveMediaPath,
3
+ type A11yNote,
4
+ type BodyFormat,
5
+ type CollectionManifest,
6
+ type DataObject,
7
+ type Manifest,
8
+ type PdfFacts,
9
+ type PdfTarget,
10
+ } from '@vespercms/core';
11
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
12
+ import type { a11yChecker } from './run.ts';
13
+
14
+ type Checks = typeof import('./run.ts');
15
+ type Checker = ReturnType<typeof a11yChecker>;
16
+
17
+ export type EntryNotes = {
18
+ /** Field notes in form order, then the body's, then the PDFs'. */
19
+ notes: A11yNote[];
20
+ /** PDFs the entry points at. */
21
+ pdfs: PdfTarget[];
22
+ };
23
+
24
+ const NONE: EntryNotes = { notes: [], pdfs: [] };
25
+
26
+ export type EntryNotesResult = EntryNotes & {
27
+ /** Checks the current values now, without the pause (for a save). PDFs keep their last result. */
28
+ recheck: () => Promise<A11yNote[]>;
29
+ };
30
+
31
+ /**
32
+ * What each PDF says about itself, by the URL the studio reads it from, for
33
+ * as long as the studio is open: a PDF is read once, however often its entry
34
+ * is checked. A file that can't be read gets no notes rather than wrong ones.
35
+ */
36
+ const pdfFacts = new Map<string, Promise<PdfFacts | null>>();
37
+
38
+ function factsFor(url: string): Promise<PdfFacts | null> {
39
+ let facts = pdfFacts.get(url);
40
+ if (!facts) {
41
+ facts = (async () => {
42
+ try {
43
+ const response = await fetch(url);
44
+ if (!response.ok) return null;
45
+ const bytes = await response.arrayBuffer();
46
+ const { readPdfFacts } = await import('./pdf.ts');
47
+ return await readPdfFacts(bytes);
48
+ } catch {
49
+ return null;
50
+ }
51
+ })();
52
+ pdfFacts.set(url, facts);
53
+ }
54
+ return facts;
55
+ }
56
+
57
+ /**
58
+ * The entry's accessibility notes (spec §6.18, layer 1), checked a moment
59
+ * after typing stops, the same pause as the body's validation. PDFs are read
60
+ * with PDF.js, and their notes join when they have been.
61
+ */
62
+ export function useEntryNotes(options: {
63
+ manifest: Manifest;
64
+ collection: CollectionManifest;
65
+ values: DataObject;
66
+ /** The body without its leading line breaks, as the body editor has it. */
67
+ body: string | undefined;
68
+ format: BodyFormat;
69
+ /** The entry's file, which relative PDF paths are resolved against. */
70
+ entryPath: string;
71
+ /** Where the studio reads a site file from. */
72
+ fileUrl: (path: string) => Promise<string>;
73
+ }): EntryNotesResult {
74
+ const { manifest, collection, values, body, format, entryPath, fileUrl } = options;
75
+ const load = useMemo(() => {
76
+ let loaded: Promise<{ checks: Checks; check: Checker }> | null = null;
77
+ return () =>
78
+ (loaded ??= import('./run.ts').then((checks) => ({
79
+ checks,
80
+ check: checks.a11yChecker(manifest),
81
+ })));
82
+ }, [manifest]);
83
+
84
+ const [found, setFound] = useState<EntryNotes>(NONE);
85
+ useEffect(() => {
86
+ let live = true;
87
+ const timer = setTimeout(() => {
88
+ void load().then(({ check }) => {
89
+ if (live)
90
+ setFound(check(collection, { values, ...(body !== undefined ? { body } : {}), format }));
91
+ });
92
+ }, 300);
93
+ return () => {
94
+ live = false;
95
+ clearTimeout(timer);
96
+ };
97
+ }, [load, collection, values, body, format]);
98
+
99
+ const [pdfNotes, setPdfNotes] = useState<A11yNote[]>([]);
100
+ useEffect(() => {
101
+ let live = true;
102
+ void load()
103
+ .then(({ checks }) =>
104
+ Promise.all(
105
+ found.pdfs.map(async (pdf) => {
106
+ const path = resolveMediaPath(pdf.href, entryPath);
107
+ const facts = path === null ? null : await factsFor(await fileUrl(path));
108
+ return facts ? checks.pdfNotes(pdf, facts) : [];
109
+ }),
110
+ ),
111
+ )
112
+ .then((notes) => {
113
+ if (live) setPdfNotes(notes.flat());
114
+ });
115
+ return () => {
116
+ live = false;
117
+ };
118
+ }, [load, found.pdfs, entryPath, fileUrl]);
119
+
120
+ const latest = useRef({ collection, values, body, format, pdfNotes });
121
+ useEffect(() => {
122
+ latest.current = { collection, values, body, format, pdfNotes };
123
+ });
124
+ const recheck = useCallback(async () => {
125
+ const { check } = await load();
126
+ const now = latest.current;
127
+ const fresh = check(now.collection, {
128
+ values: now.values,
129
+ ...(now.body !== undefined ? { body: now.body } : {}),
130
+ format: now.format,
131
+ });
132
+ return [...fresh.notes, ...now.pdfNotes];
133
+ }, [load]);
134
+
135
+ return useMemo(
136
+ () => ({ notes: [...found.notes, ...pdfNotes], pdfs: found.pdfs, recheck }),
137
+ [found, pdfNotes, recheck],
138
+ );
139
+ }
@@ -0,0 +1,156 @@
1
+ import type { CollectionManifest } from '@vespercms/core';
2
+ import { Button, Dialog } from '@vespercms/ui';
3
+ import { lazy, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { Unavailable } from '../screens/Unavailable.tsx';
5
+ import { ChangesProvider } from '../cloud/changes.tsx';
6
+ import { CloudGate } from '../cloud/CloudGate.tsx';
7
+ import { GitHubGate } from '../github/GitHubGate.tsx';
8
+ import { readSession } from '../github/session.ts';
9
+ import type { StudioOptions, WidgetComponent, WidgetLoader } from '../types.ts';
10
+ import { StudioContext, VESPER_NAME, type Studio } from './context.tsx';
11
+ import { useMediaQuery } from './media.ts';
12
+ import { NavigationContext } from './navigation.ts';
13
+ import { PANES, PaneShell } from './panes.tsx';
14
+ import { useRouter, type Route } from './router.ts';
15
+
16
+ const NO_WIDGETS: Record<string, WidgetLoader> = {};
17
+ const NO_PROPS = (): Promise<Record<string, unknown>> => Promise.resolve({});
18
+
19
+ export function App({
20
+ manifest,
21
+ api,
22
+ widgets = NO_WIDGETS,
23
+ loadMdxProps = NO_PROPS,
24
+ siteName = VESPER_NAME,
25
+ siteUrl,
26
+ github,
27
+ routes,
28
+ loadUrlFunctions,
29
+ account,
30
+ cloud,
31
+ connection,
32
+ }: StudioOptions) {
33
+ const { route, pending, leave, stay } = useRouter();
34
+ const panes = useMediaQuery(PANES);
35
+ const navigation = useNavigationCount(route);
36
+
37
+ const studio = useMemo<Studio | null>(() => {
38
+ if (!api) return null;
39
+ const byName = new Map<string, CollectionManifest>(
40
+ manifest.collections.map((c) => [c.name, c]),
41
+ );
42
+ return {
43
+ manifest,
44
+ api,
45
+ widgets: lazyWidgets(widgets),
46
+ loadMdxProps,
47
+ siteName,
48
+ siteUrl,
49
+ collection: (name) => byName.get(name),
50
+ ...(account ? { account } : {}),
51
+ ...(connection ? { cloud: connection } : {}),
52
+ };
53
+ }, [manifest, api, widgets, loadMdxProps, siteName, siteUrl, account, connection]);
54
+
55
+ // Stable, so the gate keeps one GitHub API for the whole visit.
56
+ const gateOptions = useMemo(
57
+ () => ({
58
+ manifest,
59
+ widgets,
60
+ loadMdxProps,
61
+ siteName,
62
+ ...(siteUrl !== undefined ? { siteUrl } : {}),
63
+ ...(routes ? { routes } : {}),
64
+ ...(loadUrlFunctions ? { loadUrlFunctions } : {}),
65
+ }),
66
+ [manifest, widgets, loadMdxProps, siteName, siteUrl, routes, loadUrlFunctions],
67
+ );
68
+
69
+ if (!studio) {
70
+ // Sign in first, then this same App with the cloud or GitHub as its back end.
71
+ // The cloud's gate offers GitHub too; once GitHub is chosen, its own gate takes over.
72
+ if (cloud && !(github && gitHubChosen()))
73
+ return <CloudGate settings={cloud} github={github ?? null} options={gateOptions} />;
74
+ if (github) return <GitHubGate settings={github} options={gateOptions} />;
75
+ return <Unavailable siteName={siteName} />;
76
+ }
77
+
78
+ // Wide: panes, full width; narrow: one screen at a time (app/panes.tsx).
79
+ const screen = <PaneShell route={route} studio={studio} wide={panes} />;
80
+ return (
81
+ <StudioContext.Provider value={studio}>
82
+ <NavigationContext.Provider value={navigation}>
83
+ {/* The cloud's Changes drawer, reachable from every screen (spec §7.4). */}
84
+ {connection ? (
85
+ <ChangesProvider linked={route.screen === 'changes'}>{screen}</ChangesProvider>
86
+ ) : (
87
+ screen
88
+ )}
89
+ <Dialog
90
+ title="Leave without saving?"
91
+ role="alertdialog"
92
+ isOpen={pending !== null}
93
+ onOpenChange={(open) => {
94
+ if (!open) stay();
95
+ }}
96
+ >
97
+ <p className="mb-6">Your changes to this entry haven’t been saved. They’ll be lost.</p>
98
+ <div className="flex flex-wrap justify-end gap-3">
99
+ <Button onPress={stay}>Keep editing</Button>
100
+ <Button variant="danger" onPress={leave}>
101
+ Leave without saving
102
+ </Button>
103
+ </div>
104
+ </Dialog>
105
+ </NavigationContext.Provider>
106
+ </StudioContext.Provider>
107
+ );
108
+ }
109
+
110
+ const lazyCache = new WeakMap<WidgetLoader, WidgetComponent>();
111
+
112
+ /** Custom widget loaders as lazy components, made once per loader. */
113
+ function lazyWidgets(loaders: Record<string, WidgetLoader>): Record<string, WidgetComponent> {
114
+ return Object.fromEntries(
115
+ Object.entries(loaders).map(([name, loader]) => {
116
+ let component = lazyCache.get(loader);
117
+ if (!component) {
118
+ component = lazy(loader);
119
+ lazyCache.set(loader, component);
120
+ }
121
+ return [name, component];
122
+ }),
123
+ );
124
+ }
125
+
126
+ /**
127
+ * Each route gets a fresh screen, so no state leaks from one entry to the
128
+ * next. Changes is Home under the drawer, so closing it keeps the same Home.
129
+ */
130
+ function routeKey(route: Route): string {
131
+ return JSON.stringify(route.screen === 'changes' ? { screen: 'home' } : route);
132
+ }
133
+
134
+ /**
135
+ * Counts in-app navigations, so a new screen can take focus. It compares the
136
+ * route with the last one counted rather than skipping a "first run": React's
137
+ * StrictMode runs effects twice on mount in development, and a first-run flag
138
+ * then counted the load as a navigation and moved focus past the top bar.
139
+ */
140
+ function useNavigationCount(route: Route): { count: number } {
141
+ const [count, setCount] = useState(0);
142
+ const key = routeKey(route);
143
+ const counted = useRef(key);
144
+ useEffect(() => {
145
+ if (counted.current === key) return;
146
+ counted.current = key;
147
+ setCount((n) => n + 1);
148
+ }, [key]);
149
+ return useMemo(() => ({ count }), [count]);
150
+ }
151
+
152
+ /** True when the editor signed in with GitHub, or is on the way back from doing so. */
153
+ function gitHubChosen(): boolean {
154
+ const params = new URLSearchParams(window.location.search);
155
+ return params.has('github') || params.has('github_error') || readSession() !== null;
156
+ }