@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,76 @@
1
+ import type { Manifest, UrlEntry, WidgetProps } from '@vespercms/core';
2
+ import type { CloudConnection } from './cloud/connection.ts';
3
+ import type { CloudSettings } from './cloud/session.ts';
4
+ import type { GitHubStudioSettings } from './github/session.ts';
5
+ import type { ComponentType } from 'react';
6
+ import type { StudioApi } from './app/api.ts';
7
+ /** What a widget receives (spec §6.3.3); defined in core so sites can type widgets without the studio. */
8
+ export type { WidgetProps };
9
+ export type WidgetComponent = ComponentType<WidgetProps<any>>;
10
+ /** A custom widget from vesper.config.ts, loaded only when a field uses it. */
11
+ export type WidgetLoader = () => Promise<{
12
+ default: WidgetComponent;
13
+ }>;
14
+ export type StudioOptions = {
15
+ manifest: Manifest;
16
+ /**
17
+ * The storage back end, or `null` when this build has none yet: a deployed
18
+ * site before GitHub mode (Phase 3). The studio then explains where to edit.
19
+ */
20
+ api: StudioApi | null;
21
+ /** Custom widgets by name (spec §6.3.3). */
22
+ widgets?: Record<string, WidgetLoader>;
23
+ /**
24
+ * Loads declared MDX components' Zod props schemas from vesper.config.ts,
25
+ * so props are checked exactly as the site checks them (spec §6.5.2). On
26
+ * first use: Zod stays out of the studio's first load.
27
+ */
28
+ loadMdxProps?: () => Promise<Record<string, unknown>>;
29
+ /** Shown in the top bar and the page title. */
30
+ siteName?: string;
31
+ /** The site's home page, for "View site". */
32
+ siteUrl?: string;
33
+ /**
34
+ * GitHub mode in a built site (spec §6.8): with no `api`, the studio
35
+ * signs the editor in with GitHub and saves to this repository.
36
+ */
37
+ github?: GitHubStudioSettings | null;
38
+ /** The site's static routes at build, for GitHub mode's collision checks. */
39
+ routes?: readonly (readonly [string, string])[];
40
+ /** Function-form URL patterns from vesper.config.ts, for GitHub mode's moves and links. */
41
+ loadUrlFunctions?: () => Promise<Readonly<Record<string, (entry: UrlEntry) => unknown>>>;
42
+ /** Who is signed in, set by GitHub mode and the cloud. */
43
+ account?: Account;
44
+ /**
45
+ * The cloud in a built site (spec §6.9): with no `api`, the studio signs
46
+ * the editor in to Vesper, and saves become drafts.
47
+ */
48
+ cloud?: CloudSettings | null;
49
+ /** The studio connected to the cloud, set by the cloud's gate once signed in. */
50
+ connection?: CloudConnection;
51
+ };
52
+ /** The signed-in editor, in GitHub mode or the cloud: the user menu shows it. */
53
+ export type Account = {
54
+ /** The user menu's button: `@login` on GitHub, the person's name on Vesper. */
55
+ label: string;
56
+ /** The person's name, at the top of the open menu and for their initials. */
57
+ name: string;
58
+ /** Their picture's address, beside the name; without one, their initials show. */
59
+ picture?: string;
60
+ /** The menu's name: "Signed in to GitHub as @login". */
61
+ description: string;
62
+ /** The menu's way out: "Sign out of GitHub", "Sign out of Vesper". */
63
+ signOutLabel: string;
64
+ /** The cloud's dashboard, for "Open dashboard". */
65
+ dashboardUrl?: string;
66
+ /**
67
+ * GitHub mode: the repository is public, so a change scheduled there can
68
+ * be read before it goes live (spec §6.17). The Schedule dialog says so.
69
+ */
70
+ publicRepository?: boolean;
71
+ signOut: () => void;
72
+ /** Signs in again after the session expired, coming back to the same screen. */
73
+ signInAgain: () => void;
74
+ /** True once GitHub has said the session expired. */
75
+ expired: boolean;
76
+ };
@@ -0,0 +1,25 @@
1
+ import { type DataValue } from '@vespercms/core';
2
+ import type { WidgetProps } from '../types.ts';
3
+ /** Native inputs share the ui kit's control look. */
4
+ export declare const inputClass: string;
5
+ export declare function TextWidget(props: WidgetProps<string>): import("react").JSX.Element;
6
+ export declare function EmailWidget(props: WidgetProps<string>): import("react").JSX.Element;
7
+ export declare function TextAreaWidget(props: WidgetProps<string>): import("react").JSX.Element;
8
+ export declare function NumberWidget({ value, onChange, field, id, describedBy, invalid, disabled, }: WidgetProps<number>): import("react").JSX.Element;
9
+ export declare function SwitchWidget({ value, onChange, id, describedBy, invalid, disabled, }: WidgetProps<boolean>): import("react").JSX.Element;
10
+ /**
11
+ * A calendar date. The value keeps its type: a Date stays a Date (written
12
+ * back as `2026-10-17`), and a quoted string stays a string.
13
+ */
14
+ export declare function DateWidget({ value, onChange, field, id, describedBy, invalid, disabled, }: WidgetProps<Date | string>): import("react").JSX.Element;
15
+ export declare function SelectWidget({ value, onChange, field, id, describedBy, invalid, disabled, }: WidgetProps<string>): import("react").JSX.Element;
16
+ /** Up to four options, all visible. Rendered inside the field's fieldset, which names it. */
17
+ export declare function RadioWidget({ value, onChange, field, id, describedBy, invalid, disabled, }: WidgetProps<string>): import("react").JSX.Element;
18
+ /** A list of strings: type and press Enter to add, each with a named remove button. */
19
+ export declare function TagsWidget({ value, onChange, field, id, describedBy, invalid, disabled, }: WidgetProps<string[]>): import("react").JSX.Element;
20
+ /**
21
+ * The raw field (spec §6.3 step 4): any value Vesper can't map to a control,
22
+ * edited as a small YAML snippet and parsed the way Astro parses it. The
23
+ * text is kept while it's invalid, so nothing typed is lost.
24
+ */
25
+ export declare function RawWidget({ value, onChange, field, id, describedBy, invalid, disabled, }: WidgetProps<DataValue>): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { type FocusEvent, type KeyboardEvent } from 'react';
2
+ /** Props for a `date` or `datetime-local` input whose value is `value`. */
3
+ export declare function useBlankWhenEmpty(value: string): {
4
+ 'data-blank': string | undefined;
5
+ onPointerDown: () => void;
6
+ onKeyDown: (event: KeyboardEvent) => void;
7
+ onBlur: (event: FocusEvent<HTMLInputElement>) => void;
8
+ };
@@ -0,0 +1,43 @@
1
+ import { type CropRect, type ImageSize, type ImageValue } from '@vespercms/core';
2
+ export type Ratio = {
3
+ name: string;
4
+ ratio: number;
5
+ };
6
+ /** A ratio's name for people: "Wide", or "Social image" for the built-in og. */
7
+ export declare function ratioName(name: string): string;
8
+ /** The image positioned inside a box of the crop's shape so only the crop shows. */
9
+ export declare function cropStyle(rect: CropRect): {
10
+ width: string;
11
+ height: string;
12
+ left: string;
13
+ top: string;
14
+ };
15
+ export type CropPreviewsProps = {
16
+ id: string;
17
+ src: string;
18
+ /** The image's natural size; previews wait for it. */
19
+ size: ImageSize | null;
20
+ value: ImageValue;
21
+ ratios: Ratio[];
22
+ /** The field's crops after a change: one applied, or one reset to the focal point. */
23
+ onChange: (crops: Record<string, CropRect>) => void;
24
+ disabled: boolean;
25
+ };
26
+ export declare function CropPreviews({ id, src, size, value, ratios, onChange, disabled, }: CropPreviewsProps): import("react").JSX.Element;
27
+ /** "40% from the left, 10% from the top, 45% of the width", for the crop box. */
28
+ export declare function describeCrop(rect: CropRect): string;
29
+ export type CropDialogProps = {
30
+ src: string;
31
+ size: ImageSize;
32
+ name: string;
33
+ ratio: number;
34
+ /** Where the box starts: the custom crop, or the focal point's. */
35
+ start: {
36
+ rect: CropRect;
37
+ custom: boolean;
38
+ };
39
+ onClose: () => void;
40
+ onApply: (rect: CropRect) => void;
41
+ onReset: () => void;
42
+ };
43
+ export declare function CropDialog({ src, size, name, ratio, start, onClose, onApply, onReset, }: CropDialogProps): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { type FocalPoint, type ImageSize } from '@vespercms/core';
2
+ /** "62% across, 35% down", the words the control and the announcements use. */
3
+ export declare function describeFocus(focus: FocalPoint | undefined): string;
4
+ export type FocalPointControlProps = {
5
+ id: string;
6
+ /** The image's URL. */
7
+ src: string;
8
+ focus: FocalPoint | undefined;
9
+ /** A new focal point, or undefined to go back to the centre. */
10
+ onChange: (focus: FocalPoint | undefined) => void;
11
+ /** The image's natural size, once it has loaded (for the crop previews). */
12
+ onImageSize?: (size: ImageSize) => void;
13
+ disabled: boolean;
14
+ };
15
+ export declare function FocalPointControl({ id, src, focus, onChange, onImageSize, disabled, }: FocalPointControlProps): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import 'maplibre-gl/dist/maplibre-gl.css';
2
+ import type { LatLng } from './geo.tsx';
3
+ export default function GeoMap({ label, point, center, zoom, disabled, onChange, }: {
4
+ label: string;
5
+ point: LatLng | null;
6
+ center: LatLng | undefined;
7
+ zoom: number | undefined;
8
+ disabled: boolean;
9
+ onChange: (point: LatLng) => void;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { type DataValue } from '@vespercms/core';
2
+ import type { ValuePath } from '../editor/values.ts';
3
+ import type { WidgetProps } from '../types.ts';
4
+ export type LatLng = {
5
+ lat: number;
6
+ lng: number;
7
+ };
8
+ /**
9
+ * A place (spec §6.3.2): latitude and longitude as two number fields, which
10
+ * are the real, always-visible controls. A map with a draggable,
11
+ * keyboard-movable pin loads only when asked for: it fetches map tiles from
12
+ * the internet, and it's a convenience, not the way in.
13
+ */
14
+ export declare function GeoWidget({ value, onChange, field, id, disabled, invalid, describedBy, }: WidgetProps<DataValue> & {
15
+ path: ValuePath;
16
+ }): import("react").JSX.Element;
@@ -0,0 +1,41 @@
1
+ import { type DataValue, type FieldModel } from '@vespercms/core';
2
+ import { type ValuePath } from '../editor/values.ts';
3
+ import type { WidgetProps } from '../types.ts';
4
+ type GroupProps = WidgetProps<DataValue> & {
5
+ path: ValuePath;
6
+ };
7
+ /** An object: its fields, in schema order, inside the field's fieldset. */
8
+ export declare function GroupWidget({ field, path }: GroupProps): import("react").JSX.Element;
9
+ /** What one item and several are called: "point" and "points", from the list's own label. */
10
+ export declare function itemNames(list: FieldModel): {
11
+ one: string;
12
+ many: string;
13
+ };
14
+ /**
15
+ * A list item's name, from its own content: an image's alt text or file
16
+ * name; otherwise the `summary` hint's field, a title-like field, or its
17
+ * first text, in that order; else "Point 3".
18
+ */
19
+ export declare function itemSummary(value: DataValue, index: number, list: FieldModel): {
20
+ name: string;
21
+ key: string | undefined;
22
+ };
23
+ /**
24
+ * The repeater (spec §6.3.2). A list of objects or images shows each item as
25
+ * a row named by its content, which opens in place (DECISIONS.md, "Top-level
26
+ * groups are sections, and list items are rows that open in place"). Rows
27
+ * are closed when the entry opens, except a new item and any holding a field
28
+ * that a refused save names. Order never depends on drag-and-drop (spec
29
+ * §10.6): each item's "…" menu moves it, and Alt+↑/↓ on a row does too. A
30
+ * row's handle drags it as well, for the pointer only (DECISIONS.md, "List
31
+ * rows drag by a handle"). Closed items stay in the page
32
+ * (`hidden="until-found"`), so the browser's find, the error summary and Go
33
+ * to field still reach every field.
34
+ *
35
+ * A list of kinds (a discriminated union) names each row by its kind. Add
36
+ * offers the kinds, each row's "…" menu adds one below it or changes its
37
+ * kind, and a change that would remove content asks first (DECISIONS.md,
38
+ * "Blocks are a discriminated union of objects").
39
+ */
40
+ export declare function RepeaterWidget({ value, onChange, field, path, disabled, id }: GroupProps): import("react").JSX.Element;
41
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { WidgetProps } from '../types.ts';
2
+ /**
3
+ * An icon field (`fields.icon`): a searchable list of the names it offers,
4
+ * each with its icon before it, and the chosen one's icon in the field. The
5
+ * drawings come from the manifest, which holds only the icons the site's
6
+ * fields offer, so the studio carries no icon set of its own. The file
7
+ * stores the name.
8
+ */
9
+ export declare function IconWidget({ value, onChange, field, id, describedBy, invalid, disabled, }: WidgetProps<string>): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { type DataValue, type FieldModel } from '@vespercms/core';
2
+ import { type ValuePath } from '../editor/values.ts';
3
+ import type { WidgetProps } from '../types.ts';
4
+ import { type Ratio } from './crops.tsx';
5
+ /** The ratios a field previews: the ones it lists, else every ratio the site has (spec §6.6.3). */
6
+ export declare function fieldRatios(field: FieldModel, site: Record<string, number>): Ratio[];
7
+ /**
8
+ * An image: preview, choose or replace, alt text written for this page, and
9
+ * a "Decorative image" option that stores `alt: ""` and `decorative: true`.
10
+ * A plain `image()` field stores just the path, with no alt text here.
11
+ */
12
+ export declare function ImageWidget({ value, onChange, field, id, describedBy, invalid, disabled, path, }: WidgetProps<DataValue> & {
13
+ path: ValuePath;
14
+ }): import("react").JSX.Element;
15
+ /** A file (PDF and so on): its name, type and size, with choose, replace and remove. */
16
+ export declare function FileWidget({ value, onChange, field, id, describedBy, disabled, }: WidgetProps<string>): import("react").JSX.Element;
@@ -0,0 +1,2 @@
1
+ import type { WidgetProps } from '../types.ts';
2
+ export declare function LazyMarkdownWidget(props: WidgetProps<string>): import("react").JSX.Element;
@@ -0,0 +1,2 @@
1
+ import type { WidgetProps } from '../types.ts';
2
+ export declare function MarkdownWidget(props: WidgetProps<string>): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { type DataValue } from '@vespercms/core';
2
+ import type { WidgetProps } from '../types.ts';
3
+ /** The ID a stored reference points at. */
4
+ export declare function referenceId(value: DataValue): string | undefined;
5
+ /** One reference: a searchable list of the other collection's entries. */
6
+ export declare function ReferenceWidget({ value, onChange, field, id, describedBy, invalid, disabled, }: WidgetProps<DataValue>): import("react").JSX.Element;
7
+ /**
8
+ * Several references, in order. Order usually matters (related posts), so
9
+ * each has Move up, Move down and Remove buttons, and new ones are added
10
+ * from a search box at the end.
11
+ */
12
+ export declare function ReferencesWidget({ value, onChange, field, id, describedBy, invalid, disabled, }: WidgetProps<DataValue[]>): import("react").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import type { FieldModel } from '@vespercms/core';
2
+ import type { ComponentType } from 'react';
3
+ import type { ValuePath } from '../editor/values.ts';
4
+ import type { WidgetProps } from '../types.ts';
5
+ export type BuiltinWidgetProps = WidgetProps<any> & {
6
+ path: ValuePath;
7
+ /** The field is a list item in its own row, which already names it (a union's kind). */
8
+ inRow?: boolean;
9
+ };
10
+ export type BuiltinWidget = {
11
+ component: ComponentType<BuiltinWidgetProps>;
12
+ /**
13
+ * Renders as a fieldset whose legend is the label (objects, lists, radio
14
+ * groups); `'boxed'` also draws a hairline around it (an image, a map).
15
+ */
16
+ group?: true | 'boxed';
17
+ /** Renders each child (or item) as its own field, so their errors show there. */
18
+ rendersChildren?: true;
19
+ /**
20
+ * At the form's first level it's a section: a hairline above, its own
21
+ * heading, more space (DECISIONS.md, "Top-level groups are sections").
22
+ * Deeper, an object is set off by an indent guide instead of a frame.
23
+ */
24
+ section?: true;
25
+ /** Ids of controls it renders for some of its children, as suffixes of its own id (`alt` → `-alt`). */
26
+ childControls?: Readonly<Record<string, string>>;
27
+ };
28
+ /**
29
+ * The built-in control for a field: its resolved widget, else the one its
30
+ * type implies (spec §6.3 step 3), else the raw YAML field (step 4).
31
+ */
32
+ export declare function builtinWidget(field: FieldModel): BuiltinWidget;
@@ -0,0 +1,20 @@
1
+ import type { WidgetProps } from '../types.ts';
2
+ /**
3
+ * Colour (spec §6.3.2). The hex text field is the primary control; the
4
+ * native picker is a convenience beside it. With a palette, it's a radio
5
+ * group of named swatches instead, each named in words.
6
+ */
7
+ export declare function ColorWidget({ value, onChange, field, id, describedBy, invalid, disabled, }: WidgetProps<string>): import("react").JSX.Element;
8
+ /**
9
+ * Slug (spec §6.3.2): follows its source field (usually the title) until
10
+ * someone edits it by hand. It follows only on entries that haven't been
11
+ * saved yet, so a published page's URL never changes behind anyone's back.
12
+ */
13
+ export declare function SlugWidget({ value, onChange, field, id, describedBy, invalid, disabled, }: WidgetProps<string>): import("react").JSX.Element;
14
+ /** URL, with a link that opens it (spec §6.3.2). */
15
+ export declare function UrlWidget({ value, onChange, field, id, describedBy, invalid, disabled, }: WidgetProps<string>): import("react").JSX.Element;
16
+ /**
17
+ * Date and time in the site's time zone, named in text (spec §6.17). The
18
+ * value keeps its type: a Date stays a Date, written with that day's offset.
19
+ */
20
+ export declare function DateTimeWidget({ value, onChange, field, id, describedBy, invalid, disabled, }: WidgetProps<Date | string>): import("react").JSX.Element;
@@ -0,0 +1,36 @@
1
+ import { type DataObject, type DataValue, type FieldModel, type FieldVariant } from '@vespercms/core';
2
+ import { type ValuePath } from '../editor/values.ts';
3
+ import type { WidgetProps } from '../types.ts';
4
+ /** A new value of a kind: its key first, then its fields' own defaults, as a new entry gets them. */
5
+ export declare function kindValue(union: FieldModel, kind: FieldVariant): DataObject;
6
+ export type KindChange = {
7
+ value: DataObject;
8
+ /** Labels of the fields both kinds have, whose values carried over. */
9
+ kept: string[];
10
+ /** Labels of the fields with content the new kind has no place for. */
11
+ dropped: string[];
12
+ };
13
+ /**
14
+ * A value moved to another kind. A field the new kind has under the same key
15
+ * and of the same kind keeps its value; the rest are dropped. `setAside` is
16
+ * what this value last held as the new kind, in this visit: changing back
17
+ * before saving brings it back.
18
+ */
19
+ export declare function changeKind(union: FieldModel, value: DataValue, to: FieldVariant, setAside?: DataObject): KindChange;
20
+ /** "Heading kept. Cards and Layout removed." for the status line after a change of kind. */
21
+ export declare function changeSummary(change: KindChange): string;
22
+ /**
23
+ * How a list names an item of a kind: by its kind, with its own words after
24
+ * it ("Card grid", "This industry has rules"). On a page, the order of kinds
25
+ * is what someone scans for, and kinds are short names.
26
+ */
27
+ export declare function kindRow(value: DataValue, union: FieldModel): {
28
+ name: string;
29
+ detail: string;
30
+ };
31
+ type UnionProps = WidgetProps<DataValue> & {
32
+ path: ValuePath;
33
+ inRow?: boolean;
34
+ };
35
+ export declare function UnionWidget(props: UnionProps): import("react").JSX.Element | null;
36
+ export {};
package/package.json CHANGED
@@ -1,6 +1,65 @@
1
1
  {
2
2
  "name": "@vespercms/admin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-next.1",
4
+ "description": "The Vesper studio: the editor UI served at /admin, one bundle for every storage mode.",
5
+ "license": "MIT",
6
+ "homepage": "https://vespercms.com",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/vespercms/vespercms.git",
10
+ "directory": "packages/admin"
11
+ },
12
+ "type": "module",
13
+ "sideEffects": [
14
+ "*.css"
15
+ ],
16
+ "//exports": "Source in this workspace (@vesper/source); dist/ elsewhere. dist/studio.css is precompiled, so sites need no Tailwind (DECISIONS.md, Studio).",
17
+ "exports": {
18
+ ".": {
19
+ "@vesper/source": "./src/index.ts",
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/studio.js"
22
+ },
23
+ "./studio.css": {
24
+ "@vesper/source": "./src/studio.css",
25
+ "default": "./dist/studio.css"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "src",
31
+ "!src/**/*.test.ts",
32
+ "!src/**/*.test.tsx",
33
+ "!src/**/__snapshots__",
34
+ "!src/test",
35
+ "!src/test-env.d.ts"
36
+ ],
37
+ "scripts": {
38
+ "build": "vite build && tsc -p tsconfig.build.json",
39
+ "prepack": "npm run build"
40
+ },
41
+ "dependencies": {
42
+ "@tiptap/core": "^3.32.0",
43
+ "@tiptap/extension-image": "^3.32.0",
44
+ "@tiptap/markdown": "^3.32.0",
45
+ "@tiptap/pm": "^3.32.0",
46
+ "@tiptap/react": "^3.32.0",
47
+ "@tiptap/starter-kit": "^3.32.0",
48
+ "@vespercms/core": "0.1.0-next.1",
49
+ "dompurify": "^3.4.17",
50
+ "maplibre-gl": "^6.14.0",
51
+ "pdfjs-dist": "^6.4.299",
52
+ "react-aria-components": "^1.22.1"
53
+ },
54
+ "//peerDependencies": "One React for the studio and the site's custom widgets, which render inside it (spec §6.3.3).",
55
+ "peerDependencies": {
56
+ "react": "^19.0.0",
57
+ "react-dom": "^19.0.0"
58
+ },
59
+ "devDependencies": {
60
+ "@fontsource/figtree": "^5.3.0",
61
+ "@tailwindcss/vite": "^4.3.3",
62
+ "@vespercms/ui": "0.0.0",
63
+ "tailwindcss": "^4.3.3"
64
+ }
65
+ }
@@ -0,0 +1,56 @@
1
+ import type { A11yNote } from '@vespercms/core';
2
+ import { Icon } from '@vespercms/ui';
3
+ import type { ReactNode } from 'react';
4
+
5
+ /** A note's words: what it's about, the problem, and why it matters (spec §6.18). */
6
+ export function NoteText({ note }: { note: A11yNote }) {
7
+ return (
8
+ <>
9
+ {note.subject ? `${note.subject}: ` : ''}
10
+ {note.message} <span className="text-content-muted">{note.why}</span>
11
+ </>
12
+ );
13
+ }
14
+
15
+ /**
16
+ * Accessibility notes under the field they're about. Text, never a coloured
17
+ * badge (spec §10.4): the small warning mark is the only colour, and the
18
+ * words say what it is. The list is part of the control's description.
19
+ */
20
+ export function FieldNotes({ id, notes }: { id: string; notes: readonly A11yNote[] }) {
21
+ return (
22
+ <ul id={id} className="m-0 flex list-none flex-col gap-1 p-0">
23
+ {notes.map((note, index) => (
24
+ <li key={`${note.rule}:${String(index)}`} className="flex gap-2 text-small text-content">
25
+ <NoteLine>
26
+ <NoteText note={note} />
27
+ </NoteLine>
28
+ </li>
29
+ ))}
30
+ </ul>
31
+ );
32
+ }
33
+
34
+ /**
35
+ * One note the studio writes itself, not from a rule (a replaced image's alt
36
+ * text), in the same words and mark as the rules' notes beside a field.
37
+ */
38
+ export function FieldNote({ id, children }: { id: string; children: ReactNode }) {
39
+ return (
40
+ <p id={id} className="flex gap-2 text-small text-content">
41
+ <NoteLine>{children}</NoteLine>
42
+ </p>
43
+ );
44
+ }
45
+
46
+ function NoteLine({ children }: { children: ReactNode }) {
47
+ return (
48
+ <>
49
+ <Icon name="warning" size={16} className="mt-0.5 text-warning" />
50
+ <span>
51
+ <span className="font-semibold">Accessibility note: </span>
52
+ {children}
53
+ </span>
54
+ </>
55
+ );
56
+ }
@@ -0,0 +1,81 @@
1
+ import type { A11yNote } from '@vespercms/core';
2
+ import { Button, Drawer, Icon } from '@vespercms/ui';
3
+ import { NoteText } from './FieldNotes.tsx';
4
+
5
+ /** The notes about one field (or the body), under its name. */
6
+ export type NoteGroup = { key: string; label: string; notes: A11yNote[] };
7
+
8
+ /** "2 accessibility notes", and how many refuse a save under the site's gate. */
9
+ export function notesCount(notes: number, blocking: number): string {
10
+ const text = `${String(notes)} accessibility ${notes === 1 ? 'note' : 'notes'}`;
11
+ if (blocking === 0) return text;
12
+ return `${text}, ${String(blocking)} ${blocking === 1 ? 'blocks' : 'block'} saving`;
13
+ }
14
+
15
+ /**
16
+ * Every accessibility note on the entry (spec §6.18), opened from the count
17
+ * in the top bar: grouped under each field's name in form order, each with
18
+ * why it matters and "Go to field". A modal drawer: focus moves in, Escape
19
+ * closes it, and focus returns to the count.
20
+ */
21
+ export function NotesDrawer({
22
+ isOpen,
23
+ onOpenChange,
24
+ groups,
25
+ isBlocking,
26
+ onGo,
27
+ }: {
28
+ isOpen: boolean;
29
+ onOpenChange: (open: boolean) => void;
30
+ groups: readonly NoteGroup[];
31
+ isBlocking: (note: A11yNote) => boolean;
32
+ /** Closes the drawer and takes focus to the note's field. */
33
+ onGo: (note: A11yNote) => void;
34
+ }) {
35
+ return (
36
+ <Drawer title="Accessibility notes" isOpen={isOpen} onOpenChange={onOpenChange}>
37
+ <div className="flex flex-col gap-6">
38
+ <p className="text-content-muted">
39
+ Automated checks find some problems, not all. Check the page by hand too, with a keyboard
40
+ and a screen reader.
41
+ </p>
42
+ {groups.map((group) => (
43
+ <section key={group.key} aria-labelledby={`a11y-group-${group.key}`}>
44
+ <h3 id={`a11y-group-${group.key}`} className="mb-2 text-heading-3 text-content">
45
+ {group.label}
46
+ </h3>
47
+ <ul className="m-0 flex list-none flex-col gap-4 p-0">
48
+ {group.notes.map((note, index) => {
49
+ const id = `a11y-note-${group.key}-${String(index)}`;
50
+ return (
51
+ <li key={id} className="flex flex-col items-start gap-2">
52
+ <p id={id} className="m-0 flex gap-2">
53
+ <Icon name="warning" size={16} className="mt-1 text-warning" />
54
+ <span>
55
+ <NoteText note={note} />
56
+ {isBlocking(note) && (
57
+ <span className="font-semibold">
58
+ {' '}
59
+ This site doesn’t save until it’s fixed.
60
+ </span>
61
+ )}
62
+ </span>
63
+ </p>
64
+ <Button
65
+ aria-describedby={id}
66
+ onPress={() => {
67
+ onGo(note);
68
+ }}
69
+ >
70
+ Go to field
71
+ </Button>
72
+ </li>
73
+ );
74
+ })}
75
+ </ul>
76
+ </section>
77
+ ))}
78
+ </div>
79
+ </Drawer>
80
+ );
81
+ }
@@ -0,0 +1,45 @@
1
+ import type { PdfFacts } from '@vespercms/core';
2
+ import { GlobalWorkerOptions, getDocument } from 'pdfjs-dist/legacy/build/pdf.mjs';
3
+ import workerUrl from 'pdfjs-dist/legacy/build/pdf.worker.min.mjs?url';
4
+
5
+ /*
6
+ * What a PDF says about its own accessibility (spec §6.18): whether it's
7
+ * tagged, its title and its language, read with PDF.js in the browser, so it
8
+ * works in every storage mode. This module and PDF.js load only when an entry
9
+ * has a PDF to check. The legacy build runs in browsers older than the
10
+ * modern build's floor (Chrome 140, Firefox 133, Safari 18.2).
11
+ */
12
+
13
+ GlobalWorkerOptions.workerSrc = workerUrl;
14
+
15
+ /** Reads the facts the PDF rules need. Nothing is rendered, and no script in the file runs. */
16
+ export async function readPdfFacts(bytes: ArrayBuffer | Uint8Array): Promise<PdfFacts> {
17
+ const task = getDocument({
18
+ data: bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes),
19
+ enableXfa: false,
20
+ disableFontFace: true,
21
+ verbosity: 0,
22
+ });
23
+ try {
24
+ const pdf = await task.promise;
25
+ const [mark, meta] = await Promise.all([pdf.getMarkInfo(), pdf.getMetadata()]);
26
+ // A tagged PDF marks itself as tagged and has a structure tree.
27
+ const tree = pdf.numPages > 0 ? await (await pdf.getPage(1)).getStructTree() : null;
28
+ const info = meta.info as { Title?: unknown; Language?: unknown };
29
+ // Typed as always there, but null for a PDF with no XMP metadata.
30
+ const metadata = meta.metadata as { get: (name: string) => unknown } | null;
31
+ const xmpTitle = metadata?.get('dc:title');
32
+ const text = (value: unknown) =>
33
+ typeof value === 'string' && value.trim() !== '' ? value.trim() : null;
34
+ // PDF.js 6 gives the mark info as a Map; older versions as an object.
35
+ const marked: unknown =
36
+ mark instanceof Map ? (mark as Map<string, unknown>).get('Marked') : mark?.Marked;
37
+ return {
38
+ tagged: marked === true && tree !== null,
39
+ title: text(info.Title) ?? text(xmpTitle),
40
+ language: text(info.Language),
41
+ };
42
+ } finally {
43
+ await task.destroy();
44
+ }
45
+ }
@@ -0,0 +1,12 @@
1
+ import { entryChecker, type Manifest } from '@vespercms/core';
2
+
3
+ /*
4
+ * The accessibility checks themselves (spec §6.18). They read bodies with
5
+ * micromark and Markdoc, which are large, so this module loads on first use,
6
+ * like the body checks (DECISIONS.md, "The body editor loads on first use").
7
+ */
8
+ export function a11yChecker(manifest: Manifest): ReturnType<typeof entryChecker> {
9
+ return entryChecker(manifest);
10
+ }
11
+
12
+ export { pdfNotes } from '@vespercms/core';