@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,160 @@
1
+ import 'maplibre-gl/dist/maplibre-gl.css';
2
+ import { Map as MapLibre, Marker, setWorkerUrl } from 'maplibre-gl';
3
+ import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?url';
4
+ import { useEffect, useRef, useState } from 'react';
5
+ import type { LatLng } from './geo.tsx';
6
+
7
+ /*
8
+ * The geo field's map (DECISIONS.md: MapLibre with OpenFreeMap tiles). Loaded
9
+ * on demand. The pin is a real button: Tab reaches it, and the arrow keys
10
+ * move it (Shift moves ten times as far), with each move announced.
11
+ */
12
+
13
+ setWorkerUrl(workerUrl);
14
+
15
+ const STYLE = 'https://tiles.openfreemap.org/styles/liberty';
16
+ const WORLD: LatLng = { lat: 20, lng: 0 };
17
+
18
+ export default function GeoMap({
19
+ label,
20
+ point,
21
+ center,
22
+ zoom,
23
+ disabled,
24
+ onChange,
25
+ }: {
26
+ label: string;
27
+ point: LatLng | null;
28
+ center: LatLng | undefined;
29
+ zoom: number | undefined;
30
+ disabled: boolean;
31
+ onChange: (point: LatLng) => void;
32
+ }) {
33
+ const container = useRef<HTMLDivElement>(null);
34
+ const map = useRef<MapLibre | null>(null);
35
+ const marker = useRef<Marker | null>(null);
36
+ const onChangeRef = useRef(onChange);
37
+ const [failed, setFailed] = useState(false);
38
+ const [announcement, setAnnouncement] = useState('');
39
+
40
+ useEffect(() => {
41
+ onChangeRef.current = onChange;
42
+ });
43
+
44
+ useEffect(() => {
45
+ if (!container.current) return;
46
+ const start = point ?? center ?? WORLD;
47
+ let instance: MapLibre;
48
+ try {
49
+ instance = new MapLibre({
50
+ container: container.current,
51
+ style: STYLE,
52
+ center: [start.lng, start.lat],
53
+ zoom: zoom ?? (point || center ? 13 : 1),
54
+ attributionControl: { compact: true },
55
+ // The map itself isn't a tab stop; the pin and the number fields are.
56
+ keyboard: false,
57
+ });
58
+ } catch {
59
+ // No WebGL, most likely. Reported after this effect, not during it.
60
+ void Promise.resolve().then(() => {
61
+ setFailed(true);
62
+ });
63
+ return;
64
+ }
65
+ instance.on('error', () => {
66
+ setFailed(true);
67
+ });
68
+ instance.getCanvas().setAttribute('tabindex', '-1');
69
+ instance.getCanvas().setAttribute('aria-hidden', 'true');
70
+ map.current = instance;
71
+
72
+ const pin = document.createElement('button');
73
+ pin.type = 'button';
74
+ pin.className = 'vesper-map-pin';
75
+ pin.setAttribute('aria-label', `${label} pin. Use the arrow keys to move it.`);
76
+ const created = new Marker({ element: pin, draggable: !disabled })
77
+ .setLngLat([start.lng, start.lat])
78
+ .addTo(instance);
79
+ marker.current = created;
80
+
81
+ const commit = (next: LatLng, announce: boolean) => {
82
+ const rounded = { lat: round(next.lat), lng: round(next.lng) };
83
+ created.setLngLat([rounded.lng, rounded.lat]);
84
+ onChangeRef.current(rounded);
85
+ if (announce)
86
+ setAnnouncement(`Latitude ${String(rounded.lat)}, longitude ${String(rounded.lng)}.`);
87
+ };
88
+ created.on('dragend', () => {
89
+ const { lat, lng } = created.getLngLat();
90
+ commit({ lat, lng }, true);
91
+ });
92
+ instance.on('click', (event) => {
93
+ if (!disabled) commit({ lat: event.lngLat.lat, lng: event.lngLat.lng }, true);
94
+ });
95
+ pin.addEventListener('keydown', (event) => {
96
+ const steps: Record<string, [number, number]> = {
97
+ ArrowUp: [1, 0],
98
+ ArrowDown: [-1, 0],
99
+ ArrowRight: [0, 1],
100
+ ArrowLeft: [0, -1],
101
+ };
102
+ const step = steps[event.key];
103
+ if (!step || disabled) return;
104
+ event.preventDefault();
105
+ // About 20 screen pixels at the current zoom; Shift moves ten times as far.
106
+ const degrees = (360 / 2 ** instance.getZoom() / 512) * 20 * (event.shiftKey ? 10 : 1);
107
+ const { lat, lng } = created.getLngLat();
108
+ const next = {
109
+ lat: Math.max(-85, Math.min(85, lat + step[0] * degrees)),
110
+ lng: ((lng + step[1] * degrees + 540) % 360) - 180,
111
+ };
112
+ commit(next, true);
113
+ instance.panTo([next.lng, next.lat]);
114
+ });
115
+
116
+ return () => {
117
+ created.remove();
118
+ instance.remove();
119
+ map.current = null;
120
+ marker.current = null;
121
+ };
122
+ // The map is created once; later point changes are applied below.
123
+ // eslint-disable-next-line react-hooks/exhaustive-deps
124
+ }, []);
125
+
126
+ // Values typed into the number fields move the pin.
127
+ useEffect(() => {
128
+ if (!point || !marker.current) return;
129
+ const current = marker.current.getLngLat();
130
+ if (current.lat === point.lat && current.lng === point.lng) return;
131
+ marker.current.setLngLat([point.lng, point.lat]);
132
+ map.current?.panTo([point.lng, point.lat]);
133
+ }, [point]);
134
+
135
+ return (
136
+ <div className="flex flex-col gap-2">
137
+ <div
138
+ ref={container}
139
+ className="h-64 w-full overflow-hidden rounded-md border border-border-strong"
140
+ />
141
+ {failed && (
142
+ <p className="text-small text-content-muted">
143
+ The map couldn’t load (it needs an internet connection). The number fields above work
144
+ without it.
145
+ </p>
146
+ )}
147
+ <p className="text-small text-content-muted">
148
+ Drag the pin, click the map, or focus the pin and use the arrow keys.
149
+ </p>
150
+ <p role="status" className="sr-only">
151
+ {announcement}
152
+ </p>
153
+ </div>
154
+ );
155
+ }
156
+
157
+ /** Six decimal places is about 10 cm: more is noise in a content file. */
158
+ function round(n: number): number {
159
+ return Math.round(n * 1e6) / 1e6;
160
+ }
@@ -0,0 +1,170 @@
1
+ import { isPlainObject, type DataValue } from '@vespercms/core';
2
+ import { Button, cx } from '@vespercms/ui';
3
+ import { Suspense, lazy, useState } from 'react';
4
+ import type { ValuePath } from '../editor/values.ts';
5
+ import type { WidgetProps } from '../types.ts';
6
+ import { inputClass } from './basic.tsx';
7
+
8
+ const GeoMap = lazy(() => import('./geo-map.tsx'));
9
+
10
+ export type LatLng = { lat: number; lng: number };
11
+
12
+ function pointOf(value: DataValue): Partial<LatLng> {
13
+ if (!isPlainObject(value)) return {};
14
+ const { lat, lng } = value;
15
+ return {
16
+ ...(typeof lat === 'number' ? { lat } : {}),
17
+ ...(typeof lng === 'number' ? { lng } : {}),
18
+ };
19
+ }
20
+
21
+ /**
22
+ * A place (spec §6.3.2): latitude and longitude as two number fields, which
23
+ * are the real, always-visible controls. A map with a draggable,
24
+ * keyboard-movable pin loads only when asked for: it fetches map tiles from
25
+ * the internet, and it's a convenience, not the way in.
26
+ */
27
+ export function GeoWidget({
28
+ value,
29
+ onChange,
30
+ field,
31
+ id,
32
+ disabled,
33
+ invalid,
34
+ describedBy,
35
+ }: WidgetProps<DataValue> & { path: ValuePath }) {
36
+ const point = pointOf(value);
37
+ const [showMap, setShowMap] = useState(false);
38
+ const set = (key: 'lat' | 'lng', next: number | undefined) => {
39
+ const base = isPlainObject(value) ? value : {};
40
+ const merged: Record<string, DataValue> = { ...base, [key]: next };
41
+ if (next === undefined) Reflect.deleteProperty(merged, key);
42
+ onChange(Object.keys(merged).length === 0 && !field.required ? undefined : merged);
43
+ };
44
+ const options = field.widgetOptions as { defaultCenter?: LatLng; zoom?: number } | undefined;
45
+
46
+ return (
47
+ <div className="flex flex-col gap-3">
48
+ <div className="flex flex-wrap gap-4">
49
+ <Coordinate
50
+ id={id}
51
+ label="Latitude"
52
+ hint="−90 to 90, north is positive"
53
+ value={point.lat}
54
+ min={-90}
55
+ max={90}
56
+ onChange={(next) => {
57
+ set('lat', next);
58
+ }}
59
+ disabled={disabled}
60
+ invalid={invalid}
61
+ describedBy={describedBy}
62
+ />
63
+ <Coordinate
64
+ id={`${id}-lng`}
65
+ label="Longitude"
66
+ hint="−180 to 180, east is positive"
67
+ value={point.lng}
68
+ min={-180}
69
+ max={180}
70
+ onChange={(next) => {
71
+ set('lng', next);
72
+ }}
73
+ disabled={disabled}
74
+ invalid={invalid}
75
+ describedBy={describedBy}
76
+ />
77
+ </div>
78
+ {showMap ? (
79
+ <Suspense fallback={<p className="text-small text-content-muted">Loading the map…</p>}>
80
+ <GeoMap
81
+ label={field.label}
82
+ point={point.lat !== undefined && point.lng !== undefined ? (point as LatLng) : null}
83
+ center={options?.defaultCenter}
84
+ zoom={options?.zoom}
85
+ disabled={disabled}
86
+ onChange={(next) => {
87
+ const base = isPlainObject(value) ? value : {};
88
+ onChange({ ...base, lat: next.lat, lng: next.lng });
89
+ }}
90
+ />
91
+ </Suspense>
92
+ ) : (
93
+ <div>
94
+ <Button
95
+ variant="quiet"
96
+ onPress={() => {
97
+ setShowMap(true);
98
+ }}
99
+ >
100
+ Show on a map
101
+ </Button>
102
+ </div>
103
+ )}
104
+ </div>
105
+ );
106
+ }
107
+
108
+ function Coordinate({
109
+ id,
110
+ label,
111
+ hint,
112
+ value,
113
+ min,
114
+ max,
115
+ onChange,
116
+ disabled,
117
+ invalid,
118
+ describedBy,
119
+ }: {
120
+ id: string;
121
+ label: string;
122
+ hint: string;
123
+ value: number | undefined;
124
+ min: number;
125
+ max: number;
126
+ onChange: (value: number | undefined) => void;
127
+ disabled: boolean;
128
+ invalid: boolean;
129
+ describedBy: string | undefined;
130
+ }) {
131
+ // Text state lets "-8" or "36." be typed on the way to a number. A value
132
+ // that changes from outside (the map) replaces it.
133
+ const [text, setText] = useState(value === undefined ? '' : String(value));
134
+ const [shown, setShown] = useState(value);
135
+ if (value !== shown) {
136
+ setShown(value);
137
+ setText(value === undefined ? '' : String(value));
138
+ }
139
+ return (
140
+ <div className="flex flex-col gap-1">
141
+ <label htmlFor={id} className="text-small text-content">
142
+ {label}
143
+ </label>
144
+ <input
145
+ id={id}
146
+ type="number"
147
+ inputMode="decimal"
148
+ step="any"
149
+ min={min}
150
+ max={max}
151
+ value={text}
152
+ onChange={(event) => {
153
+ setText(event.target.value);
154
+ const next = event.target.value === '' ? undefined : event.target.valueAsNumber;
155
+ if (next === undefined || Number.isFinite(next)) {
156
+ setShown(next);
157
+ onChange(next);
158
+ }
159
+ }}
160
+ aria-describedby={[describedBy, `${id}-hint`].filter(Boolean).join(' ')}
161
+ aria-invalid={invalid || undefined}
162
+ disabled={disabled}
163
+ className={cx(inputClass, 'w-44')}
164
+ />
165
+ <p id={`${id}-hint`} className="text-small text-content-muted">
166
+ {hint}
167
+ </p>
168
+ </div>
169
+ );
170
+ }