caracms 0.0.0-stage → 0.2.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 (281) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -2
  3. package/bin/caracms.mjs +174 -0
  4. package/bin/gate.mjs +275 -0
  5. package/bin/init.mjs +712 -0
  6. package/bin/media.mjs +662 -0
  7. package/bin/site-runner.mjs +61 -0
  8. package/bin/templates/Page.astro.txt +31 -0
  9. package/bin/templates/TextBlock.astro.txt +13 -0
  10. package/bin/templates/caracms.config.ts.txt +22 -0
  11. package/bin/templates/catch-all.astro.txt +25 -0
  12. package/bin/templates/health.ts.txt +6 -0
  13. package/bin/templates/railway.ts.txt +147 -0
  14. package/package.json +66 -4
  15. package/skills/caracms-adopt/SKILL.md +508 -0
  16. package/skills/caracms-adopt/look-same.mjs +957 -0
  17. package/skills/caracms-blocks/SKILL.md +376 -0
  18. package/skills/caracms-content/SKILL.md +201 -0
  19. package/skills/caracms-tina/SKILL.md +427 -0
  20. package/skills/caracms-tina/field-kinds.md +88 -0
  21. package/skills/caracms-tina/tina-to-caracms.mjs +1011 -0
  22. package/src/block-runs.ts +84 -0
  23. package/src/cards.ts +158 -0
  24. package/src/cli/schemas.ts +122 -0
  25. package/src/cli/site.ts +66 -0
  26. package/src/cli/validate.ts +471 -0
  27. package/src/code-routes-disk.ts +67 -0
  28. package/src/code-routes.ts +134 -0
  29. package/src/columns.ts +39 -0
  30. package/src/components/Block.astro +46 -0
  31. package/src/components/BlockRunTree.astro +45 -0
  32. package/src/components/Blocks.astro +139 -0
  33. package/src/components/Buttons.astro +116 -0
  34. package/src/components/Fonts.astro +14 -0
  35. package/src/components/Form.astro +221 -0
  36. package/src/components/Head.astro +62 -0
  37. package/src/components/Link.astro +68 -0
  38. package/src/components/Lockup.astro +116 -0
  39. package/src/components/Media.astro +124 -0
  40. package/src/components/MediaPlayer.astro +102 -0
  41. package/src/components/RenderDocument.astro +64 -0
  42. package/src/components/RichText.astro +59 -0
  43. package/src/components/Text.astro +81 -0
  44. package/src/components/attributes.ts +33 -0
  45. package/src/components/descriptor.ts +45 -0
  46. package/src/components/form-client.ts +182 -0
  47. package/src/components/media-player.ts +46 -0
  48. package/src/components/media-props.ts +35 -0
  49. package/src/components/sitemap.xml.ts +36 -0
  50. package/src/config.ts +825 -0
  51. package/src/content-schema.ts +166 -0
  52. package/src/content.ts +422 -0
  53. package/src/document.ts +579 -0
  54. package/src/editor/README.md +405 -0
  55. package/src/editor/addresses.ts +61 -0
  56. package/src/editor/brand.ts +118 -0
  57. package/src/editor/client/block-list/BlockList.tsx +342 -0
  58. package/src/editor/client/block-list/Picker.tsx +105 -0
  59. package/src/editor/client/block-list/UnknownBlock.tsx +119 -0
  60. package/src/editor/client/block-list/insert-points.ts +56 -0
  61. package/src/editor/client/block-list/link.ts +71 -0
  62. package/src/editor/client/block-list/reorder.ts +105 -0
  63. package/src/editor/client/block-list/summary.ts +118 -0
  64. package/src/editor/client/collections/CollectionGrid.tsx +143 -0
  65. package/src/editor/client/collections/CollectionTable.tsx +406 -0
  66. package/src/editor/client/collections/table.ts +270 -0
  67. package/src/editor/client/context.tsx +27 -0
  68. package/src/editor/client/documents/DocMenu.tsx +154 -0
  69. package/src/editor/client/documents/actions.ts +192 -0
  70. package/src/editor/client/documents/dialogs.tsx +687 -0
  71. package/src/editor/client/editor.ts +995 -0
  72. package/src/editor/client/fields/ButtonsField.tsx +87 -0
  73. package/src/editor/client/fields/DateField.tsx +200 -0
  74. package/src/editor/client/fields/FieldControl.tsx +61 -0
  75. package/src/editor/client/fields/FieldFrame.tsx +109 -0
  76. package/src/editor/client/fields/GroupField.tsx +113 -0
  77. package/src/editor/client/fields/HiddenToggle.tsx +51 -0
  78. package/src/editor/client/fields/LinkField.tsx +307 -0
  79. package/src/editor/client/fields/ListField.tsx +642 -0
  80. package/src/editor/client/fields/LockupField.tsx +365 -0
  81. package/src/editor/client/fields/MediaField.tsx +823 -0
  82. package/src/editor/client/fields/PageFields.tsx +82 -0
  83. package/src/editor/client/fields/ReferenceField.tsx +209 -0
  84. package/src/editor/client/fields/RichTextField.tsx +306 -0
  85. package/src/editor/client/fields/SelectField.tsx +63 -0
  86. package/src/editor/client/fields/SeoGroup.tsx +282 -0
  87. package/src/editor/client/fields/SimpleFields.tsx +89 -0
  88. package/src/editor/client/fields/TextField.tsx +51 -0
  89. package/src/editor/client/fields/date.ts +73 -0
  90. package/src/editor/client/fields/host.tsx +172 -0
  91. package/src/editor/client/fields/list-summary.ts +85 -0
  92. package/src/editor/client/fields/lockup-rules.ts +181 -0
  93. package/src/editor/client/fields/parts.tsx +458 -0
  94. package/src/editor/client/fields/rich-text-commands.ts +183 -0
  95. package/src/editor/client/fields/rich-text-doc.ts +156 -0
  96. package/src/editor/client/fields/rich-text-schema.ts +224 -0
  97. package/src/editor/client/fields/stale.ts +58 -0
  98. package/src/editor/client/inbox/InboxStage.tsx +272 -0
  99. package/src/editor/client/inbox/store.ts +311 -0
  100. package/src/editor/client/live/format.ts +77 -0
  101. package/src/editor/client/live/marks.tsx +90 -0
  102. package/src/editor/client/live/notices.tsx +213 -0
  103. package/src/editor/client/live/publish.tsx +617 -0
  104. package/src/editor/client/live/session.tsx +162 -0
  105. package/src/editor/client/main.tsx +12 -0
  106. package/src/editor/client/media/LibraryStage.tsx +75 -0
  107. package/src/editor/client/media/ReplaceDialog.tsx +355 -0
  108. package/src/editor/client/media/store.ts +618 -0
  109. package/src/editor/client/nav/DocRow.tsx +122 -0
  110. package/src/editor/client/nav/search.ts +71 -0
  111. package/src/editor/client/nav/tree.ts +93 -0
  112. package/src/editor/client/overlays/OverlayMount.tsx +26 -0
  113. package/src/editor/client/overlays/editor-marks.ts +159 -0
  114. package/src/editor/client/overlays/index.ts +50 -0
  115. package/src/editor/client/overlays/layer.ts +437 -0
  116. package/src/editor/client/overlays/overlay-css.ts +177 -0
  117. package/src/editor/client/overlays/rich-text-bar.ts +717 -0
  118. package/src/editor/client/pane/Pane.tsx +91 -0
  119. package/src/editor/client/pane/levels/BlockLevel.tsx +123 -0
  120. package/src/editor/client/pane/levels/CollectionLevel.tsx +115 -0
  121. package/src/editor/client/pane/levels/DocumentLevel.tsx +93 -0
  122. package/src/editor/client/pane/levels/DocumentsLevel.tsx +390 -0
  123. package/src/editor/client/pane/levels/FileLevel.tsx +132 -0
  124. package/src/editor/client/pane/levels/FormLevel.tsx +550 -0
  125. package/src/editor/client/pane/levels/MediaLevel.tsx +657 -0
  126. package/src/editor/client/pane/levels/SettingsM6.tsx +299 -0
  127. package/src/editor/client/pane/levels/SubmissionsLevel.tsx +185 -0
  128. package/src/editor/client/pane/parts.tsx +179 -0
  129. package/src/editor/client/pane/switcher.tsx +165 -0
  130. package/src/editor/client/preview/driver.ts +640 -0
  131. package/src/editor/client/preview/field-text.ts +71 -0
  132. package/src/editor/client/preview/follow-links.ts +234 -0
  133. package/src/editor/client/preview/layout.ts +37 -0
  134. package/src/editor/client/preview/link-target.ts +50 -0
  135. package/src/editor/client/preview/markers.ts +86 -0
  136. package/src/editor/client/preview/page-edit.ts +632 -0
  137. package/src/editor/client/preview/page-editor-registry.ts +63 -0
  138. package/src/editor/client/preview/page-editor.ts +235 -0
  139. package/src/editor/client/preview/render-sequencer.ts +113 -0
  140. package/src/editor/client/preview/selection.ts +116 -0
  141. package/src/editor/client/preview/warnings.ts +61 -0
  142. package/src/editor/client/shell/App.tsx +101 -0
  143. package/src/editor/client/shell/Bar.tsx +197 -0
  144. package/src/editor/client/shell/Brand.tsx +33 -0
  145. package/src/editor/client/shell/Icons.tsx +87 -0
  146. package/src/editor/client/shell/NarrowNote.tsx +47 -0
  147. package/src/editor/client/shell/Popovers.tsx +47 -0
  148. package/src/editor/client/shell/Stage.tsx +132 -0
  149. package/src/editor/client/shell/Toast.tsx +61 -0
  150. package/src/editor/client/state/document-store.ts +465 -0
  151. package/src/editor/client/state/keeper.ts +713 -0
  152. package/src/editor/client/state/live.ts +257 -0
  153. package/src/editor/client/state/schema.ts +42 -0
  154. package/src/editor/client/state/store.ts +39 -0
  155. package/src/editor/fonts/OFL-GeistMono.txt +93 -0
  156. package/src/editor/fonts/OFL-InstrumentSans.txt +93 -0
  157. package/src/editor/fonts/geist-mono-latin-ext-wght-normal.woff2 +0 -0
  158. package/src/editor/fonts/geist-mono-latin-wght-normal.woff2 +0 -0
  159. package/src/editor/fonts/instrument-sans-latin-ext-wght-normal.woff2 +0 -0
  160. package/src/editor/fonts/instrument-sans-latin-wght-normal.woff2 +0 -0
  161. package/src/editor/middleware.ts +62 -0
  162. package/src/editor/routes/_live.ts +26 -0
  163. package/src/editor/routes/admin.astro +80 -0
  164. package/src/editor/routes/create.ts +19 -0
  165. package/src/editor/routes/delete.ts +19 -0
  166. package/src/editor/routes/document.ts +39 -0
  167. package/src/editor/routes/documents.ts +30 -0
  168. package/src/editor/routes/github.ts +23 -0
  169. package/src/editor/routes/history.ts +9 -0
  170. package/src/editor/routes/lock.ts +9 -0
  171. package/src/editor/routes/login/assets.d.ts +10 -0
  172. package/src/editor/routes/login/login.ts +43 -0
  173. package/src/editor/routes/media.ts +16 -0
  174. package/src/editor/routes/move.ts +17 -0
  175. package/src/editor/routes/notice.ts +9 -0
  176. package/src/editor/routes/preview.astro +77 -0
  177. package/src/editor/routes/publish.ts +9 -0
  178. package/src/editor/routes/status.ts +9 -0
  179. package/src/editor/routes/submissions.ts +16 -0
  180. package/src/editor/routes/try-again.ts +9 -0
  181. package/src/editor/routes/write.ts +18 -0
  182. package/src/editor/server/admin-props.ts +151 -0
  183. package/src/editor/server/cards.ts +61 -0
  184. package/src/editor/server/create.ts +414 -0
  185. package/src/editor/server/drafts-store.ts +127 -0
  186. package/src/editor/server/media/alt.ts +107 -0
  187. package/src/editor/server/media/bucket.ts +295 -0
  188. package/src/editor/server/media/config.ts +78 -0
  189. package/src/editor/server/media/handler.ts +221 -0
  190. package/src/editor/server/media/library.ts +254 -0
  191. package/src/editor/server/media/memory.ts +184 -0
  192. package/src/editor/server/media/mux.ts +241 -0
  193. package/src/editor/server/media/sniff.ts +599 -0
  194. package/src/editor/server/media/upload.ts +175 -0
  195. package/src/editor/server/mode.ts +63 -0
  196. package/src/editor/server/move.ts +295 -0
  197. package/src/editor/server/render.tsx +9 -0
  198. package/src/editor/server/replace.ts +72 -0
  199. package/src/editor/server/settings.ts +35 -0
  200. package/src/editor/server/write.ts +366 -0
  201. package/src/editor/styles/editor.css +502 -0
  202. package/src/editor/styles/fonts.css +40 -0
  203. package/src/editor/styles/levels/README.md +4 -0
  204. package/src/editor/styles/levels/collections.css +99 -0
  205. package/src/editor/styles/levels/document-level.css +10 -0
  206. package/src/editor/styles/levels/documents.css +9 -0
  207. package/src/editor/styles/levels/fields.css +171 -0
  208. package/src/editor/styles/levels/files.css +95 -0
  209. package/src/editor/styles/levels/live.css +10 -0
  210. package/src/editor/styles/levels/m5a-documents.css +51 -0
  211. package/src/editor/styles/levels/m6.css +82 -0
  212. package/src/editor/styles/levels/media.css +175 -0
  213. package/src/editor/styles/levels/switcher.css +3 -0
  214. package/src/editor/styles/tokens.css +114 -0
  215. package/src/editor/tsconfig.json +16 -0
  216. package/src/editor/types.ts +881 -0
  217. package/src/editor/ui-state.ts +113 -0
  218. package/src/email.ts +67 -0
  219. package/src/fields.ts +1129 -0
  220. package/src/files-route.ts +201 -0
  221. package/src/files.ts +116 -0
  222. package/src/forms/email.ts +149 -0
  223. package/src/forms/endpoint.ts +347 -0
  224. package/src/forms/inbox.ts +273 -0
  225. package/src/forms/rate-limit.ts +130 -0
  226. package/src/forms/route.ts +24 -0
  227. package/src/forms/rules.ts +278 -0
  228. package/src/forms/store.ts +486 -0
  229. package/src/head.ts +125 -0
  230. package/src/index.ts +434 -0
  231. package/src/json-source.ts +116 -0
  232. package/src/links.ts +121 -0
  233. package/src/live/README.md +43 -0
  234. package/src/live/auth/allow-rules.ts +70 -0
  235. package/src/live/auth/allow.ts +128 -0
  236. package/src/live/auth/auth.ts +155 -0
  237. package/src/live/auth/email.ts +84 -0
  238. package/src/live/auth/guard.ts +111 -0
  239. package/src/live/auth/index.ts +68 -0
  240. package/src/live/auth/links.ts +101 -0
  241. package/src/live/auth/login.ts +245 -0
  242. package/src/live/auth/middleware.ts +53 -0
  243. package/src/live/auth/request.ts +56 -0
  244. package/src/live/auth/routes.ts +9 -0
  245. package/src/live/auth/session.ts +181 -0
  246. package/src/live/auth/views.ts +355 -0
  247. package/src/live/autosaves.ts +306 -0
  248. package/src/live/backend.ts +652 -0
  249. package/src/live/branches.ts +19 -0
  250. package/src/live/drafts-store.ts +591 -0
  251. package/src/live/editor.ts +465 -0
  252. package/src/live/git.ts +1021 -0
  253. package/src/live/github.ts +271 -0
  254. package/src/live/locals.ts +24 -0
  255. package/src/live/locks.ts +273 -0
  256. package/src/live/merge.ts +197 -0
  257. package/src/live/messages.ts +151 -0
  258. package/src/live/mode.ts +210 -0
  259. package/src/live/redis.ts +533 -0
  260. package/src/live/runtime.ts +164 -0
  261. package/src/live/status.ts +109 -0
  262. package/src/live/sweep.ts +228 -0
  263. package/src/live/timings.ts +47 -0
  264. package/src/live/webhook.ts +39 -0
  265. package/src/lockup-css.ts +58 -0
  266. package/src/measure.ts +145 -0
  267. package/src/media-value.ts +239 -0
  268. package/src/media.ts +255 -0
  269. package/src/read.ts +256 -0
  270. package/src/redirect-middleware.ts +49 -0
  271. package/src/redirects.ts +76 -0
  272. package/src/relink.ts +293 -0
  273. package/src/reserved.ts +24 -0
  274. package/src/rich-text.ts +690 -0
  275. package/src/schema-key.ts +63 -0
  276. package/src/seo.ts +124 -0
  277. package/src/serialize.ts +697 -0
  278. package/src/skipped.ts +35 -0
  279. package/src/slug.ts +121 -0
  280. package/src/text-styles.ts +62 -0
  281. package/src/virtual.d.ts +28 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Who's where, and what's waiting, in navigation (M4 Decision 9; the mockup's metaFor
3
+ * and bubble): the Pages tree, a collection's level, "Find a document", the Recent
4
+ * strip and the ⌘K switcher. Live mode only; local mode has no marks (editor.marks is
5
+ * empty), so these render nothing and the rows look as in M3.
6
+ *
7
+ * - `DocMarksView`: a row's marks. The merge notice's warning mark; someone else's
8
+ * lock as their avatar with "Aoife is editing" (the avatar alone when `compact`);
9
+ * other people with it open (presence) as avatars; the waiting dot.
10
+ * - `Bubble`: a closed group's, a closed parent's or a collection's row carries the
11
+ * marks of what's inside it: one avatar, one dot. No side rails anywhere: marks are
12
+ * icons, avatars and dots.
13
+ */
14
+ import { useEditor, useStore } from '../context.tsx';
15
+ import type { DocMarks } from '../editor.ts';
16
+ import { Icon } from '../shell/Icons.tsx';
17
+ import type { Person } from '../../types.ts';
18
+ import { colourOf, displayName, initialOf } from './format.ts';
19
+
20
+ export function Avatar({ person, size, title }: { person: Person; size?: 'sm' | 'lg'; title?: string }) {
21
+ return (
22
+ <span className={`av${size ? ` ${size}` : ''}`} style={{ ['--c' as string]: colourOf(person) }} aria-hidden="true" title={title}>
23
+ {initialOf(person)}
24
+ </span>
25
+ );
26
+ }
27
+
28
+ export function DocMarksView({ file, compact = false, lock = true }: { file: string; compact?: boolean; /** M6c (G5): false leaves someone's lock out (a grid card says it on its picture: CollectionGrid.tsx). */ lock?: boolean }) {
29
+ const editor = useEditor();
30
+ const m: DocMarks | undefined = useStore(editor.marks, (all) => all.get(file));
31
+ if (!m) return null;
32
+ return (
33
+ <>
34
+ {m.kept ? (
35
+ <span className="wmark" title="Changed outside the editor" data-mark="kept">
36
+ <Icon name="warn" size="xs" />
37
+ <span className="sr">, changed outside the editor</span>
38
+ </span>
39
+ ) : null}
40
+ {m.editing && lock ? (
41
+ <span className="mk" data-mark="editing">
42
+ <Avatar person={m.editing} size="sm" title={`${displayName(m.editing)} is editing`} />
43
+ {compact ? <span className="sr">, {displayName(m.editing)} is editing</span> : <span className="who">{displayName(m.editing)} is editing</span>}
44
+ </span>
45
+ ) : null}
46
+ {m.present.map((p) => (
47
+ <span key={p.email} className="mk" data-mark="present">
48
+ <Avatar person={p} size="sm" title={`${displayName(p)} has it open`} />
49
+ <span className="sr">, {displayName(p)} has it open</span>
50
+ </span>
51
+ ))}
52
+ {m.waiting ? (
53
+ <span className="dot" title="Changes waiting" data-mark="waiting">
54
+ <span className="sr">, changes waiting</span>
55
+ </span>
56
+ ) : null}
57
+ </>
58
+ );
59
+ }
60
+
61
+ /** The marks of what's inside a closed group, a closed parent or a collection, bubbled up. */
62
+ export function Bubble({ files }: { files: readonly string[] }) {
63
+ const editor = useEditor();
64
+ const all = useStore(editor.marks);
65
+ if (all.size === 0) return null;
66
+ let someone: Person | null = null;
67
+ let waiting = false;
68
+ for (const file of files) {
69
+ const m = all.get(file);
70
+ if (!m) continue;
71
+ someone ??= m.editing ?? m.present[0] ?? null;
72
+ waiting ||= m.waiting;
73
+ }
74
+ if (!someone && !waiting) return null;
75
+ return (
76
+ <>
77
+ {someone ? (
78
+ <span className="mk" data-mark="bubble-person">
79
+ <Avatar person={someone} size="sm" title={`${displayName(someone)} is in here`} />
80
+ <span className="sr">, {displayName(someone)} is in here</span>
81
+ </span>
82
+ ) : null}
83
+ {waiting ? (
84
+ <span className="dot" title="Changes waiting inside" data-mark="bubble-waiting">
85
+ <span className="sr">, changes waiting inside</span>
86
+ </span>
87
+ ) : null}
88
+ </>
89
+ );
90
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * The open document's notes in live mode (the mockup's docNotes; M4 Decisions 7, 9,
3
+ * 10 and 11), under the pane's global notes in the document and block levels:
4
+ *
5
+ * - **Someone else's lock** (`#locked`): "Aoife is editing", read-only until they're
6
+ * done, opening up without a reload, with **Take over** (asks once, inline).
7
+ * - **Your own lock in another tab**: "You're editing this in another tab", with
8
+ * **Edit here** (the lock note, M4's added state in the mockup's language).
9
+ * - **Edits that weren't kept** (Decision 11): "You had changes here that weren't
10
+ * kept before Aoife edited it", with **Use mine** (off while someone else holds the
11
+ * lock) and **Discard mine**.
12
+ * - **The merge notice** (`#merge`), until dismissed or published.
13
+ * - **"Aoife published Home as it stood at 14:20"** (`#published-over`).
14
+ * - **Someone deleted it** (M5a Decision 12: the poll's `open.deleted`): who and when,
15
+ * read-only, with a way back to the documents.
16
+ *
17
+ * `ReadOnlyChip` is the head's "Read-only" chip under a lock. Local mode: nothing.
18
+ */
19
+ import { useState } from 'react';
20
+ import { useEditor, useStore } from '../context.tsx';
21
+ import { Icon } from '../shell/Icons.tsx';
22
+ import type { NoticeView, Person } from '../../types.ts';
23
+ import { clock, displayName, when } from './format.ts';
24
+ import { Avatar } from './marks.tsx';
25
+
26
+ export function ReadOnlyChip() {
27
+ const editor = useEditor();
28
+ const veil = useStore(editor.access, (a) => a.veil);
29
+ if (!veil) return null;
30
+ return (
31
+ <span className="chip lock" data-chip="read-only">
32
+ <Icon name="lock" size="xs" />
33
+ Read-only
34
+ </span>
35
+ );
36
+ }
37
+
38
+ export function DocNotices() {
39
+ const editor = useEditor();
40
+ const access = useStore(editor.access);
41
+ const file = useStore(editor.doc.state, (s) => s.entry.file);
42
+ const title = useStore(editor.doc.state, (s) => s.document.title || s.entry.label);
43
+ const notices = useStore(editor.liveState, (s) => s.status?.notices);
44
+ const timeZone = useStore(editor.liveState, (s) => s.status?.timeZone ?? 'UTC');
45
+ if (editor.mode !== 'live') return null;
46
+ const mine = (notices ?? []).filter((n) => n.file === file);
47
+ const holder = access.holder;
48
+ const theirs = access.lock === 'theirs' && holder;
49
+ if (access.deleted) return <DeletedNote title={title} deleted={access.deleted} timeZone={timeZone} />;
50
+ return (
51
+ <>
52
+ {theirs ? <LockNote key={`${file}:${holder.person.email}`} title={title} /> : null}
53
+ {access.lock === 'yours-elsewhere' ? (
54
+ <div className="note lock" role="status" data-note="elsewhere">
55
+ {editor.you ? <Avatar person={editor.you} /> : <Icon name="lock" />}
56
+ <div>
57
+ <b>You're editing this in another tab</b>
58
+ <p>{title} is read-only here.</p>
59
+ <div className="acts">
60
+ <button type="button" className="btn sm" data-act="edit-here" onClick={() => void editor.editHere()}>
61
+ Edit here
62
+ </button>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ ) : null}
67
+ {access.held ? <HeldNote /> : null}
68
+ {mine.map((n) => (
69
+ <Notice key={n.id} notice={n} timeZone={timeZone} />
70
+ ))}
71
+ </>
72
+ );
73
+ }
74
+
75
+ /** Someone else's lock, with Take over (asked once, inline, before it moves the lock). */
76
+ function LockNote({ title }: { title: string }) {
77
+ const editor = useEditor();
78
+ const holder = useStore(editor.access, (a) => a.holder)!;
79
+ const status = useStore(editor.liveState, (s) => s.status);
80
+ const [asking, setAsking] = useState(false);
81
+ const [busy, setBusy] = useState(false);
82
+ const name = displayName(holder.person);
83
+ const change = status?.open?.lastChange;
84
+ const theirChange = change?.at && change.by && change.by.email.toLowerCase() === holder.person.email.toLowerCase() ? change.at : null;
85
+ const takeOver = async () => {
86
+ setBusy(true);
87
+ try {
88
+ await editor.takeOver();
89
+ } finally {
90
+ setBusy(false);
91
+ setAsking(false);
92
+ }
93
+ };
94
+ return (
95
+ <div className="note lock" role="status" data-note="locked">
96
+ <Avatar person={holder.person} />
97
+ <div>
98
+ <b>{name} is editing</b>
99
+ {asking ? (
100
+ <>
101
+ <p>Take over {title} from {name}? Their changes are kept.</p>
102
+ <div className="acts">
103
+ <button type="button" className="btn sm" data-act="take-over-confirm" disabled={busy} onClick={() => void takeOver()}>
104
+ Take over
105
+ </button>
106
+ <button type="button" className="btn sm ghost" data-act="take-over-cancel" disabled={busy} onClick={() => setAsking(false)}>
107
+ Cancel
108
+ </button>
109
+ </div>
110
+ </>
111
+ ) : (
112
+ <>
113
+ <p>
114
+ {title} is read-only while {name} has it open.
115
+ {theirChange && status ? ` Their last change: ${when(theirChange, status.timeZone, status.now)}.` : ''}
116
+ </p>
117
+ <div className="acts">
118
+ <button type="button" className="btn sm" data-act="take-over" onClick={() => setAsking(true)}>
119
+ Take over
120
+ </button>
121
+ </div>
122
+ </>
123
+ )}
124
+ </div>
125
+ </div>
126
+ );
127
+ }
128
+
129
+ function HeldNote() {
130
+ const editor = useEditor();
131
+ const access = useStore(editor.access);
132
+ const held = access.held!;
133
+ const theirs = access.lock === 'theirs' ? access.holder : null;
134
+ const who = held.elsewhere ? null : held.by;
135
+ // No name, and not your other tab: the server couldn't tell who changed it (Decision 11, `known: false`).
136
+ const whom = who ? `before ${displayName(who)} edited it` : held.elsewhere || held.reason === 'locked' ? 'before you edited it in another tab' : 'before it was changed elsewhere';
137
+ return (
138
+ <div className="note warn" role="alert" data-note="not-kept">
139
+ <Icon name="warn" />
140
+ <div>
141
+ <b>You had changes here that weren't kept {whom}</b>
142
+ <p>Held in this browser.</p>
143
+ <div className="acts">
144
+ <button type="button" className="btn sm" data-act="use-mine" disabled={!!theirs} onClick={() => void editor.useMine()}>
145
+ Use mine
146
+ </button>
147
+ <button type="button" className="btn sm" data-act="discard-mine" onClick={() => void editor.discardMine()}>
148
+ Discard mine
149
+ </button>
150
+ {theirs ? <p className="why">Use mine is off while {displayName(theirs.person)} is editing.</p> : null}
151
+ </div>
152
+ </div>
153
+ </div>
154
+ );
155
+ }
156
+
157
+ function Notice({ notice, timeZone }: { notice: NoticeView; timeZone: string }) {
158
+ const editor = useEditor();
159
+ if (notice.kind === 'draft-kept') {
160
+ return (
161
+ <div className="note warn" role="status" data-note="draft-kept">
162
+ <Icon name="warn" />
163
+ <div>
164
+ <b>{notice.title} was also changed outside the editor.</b>
165
+ <p>Your draft was kept; check it before publishing.</p>
166
+ <div className="acts">
167
+ <button type="button" className="btn sm" data-act="dismiss" onClick={() => editor.dismissNotice(notice.id)}>
168
+ Dismiss
169
+ </button>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ );
174
+ }
175
+ return (
176
+ <div className="note select" role="status" data-note="published-over">
177
+ <Icon name="check" />
178
+ <div>
179
+ <b>
180
+ {displayName(notice.by)} published {notice.title} as it stood at {clock(notice.at, timeZone)}
181
+ </b>
182
+ <div className="acts">
183
+ <button type="button" className="btn sm" data-act="dismiss" onClick={() => editor.dismissNotice(notice.id)}>
184
+ OK
185
+ </button>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ );
190
+ }
191
+
192
+ /** The open document was deleted by someone else (M5a Decision 12): read-only, saying who and when. */
193
+ function DeletedNote({ title, deleted, timeZone }: { title: string; deleted: { by: Person | null; at: string | null }; timeZone: string }) {
194
+ const editor = useEditor();
195
+ const by = deleted.by ? displayName(deleted.by) : null;
196
+ const at = deleted.at ? clock(deleted.at, timeZone) : null;
197
+ return (
198
+ <div className="note danger" role="alert" data-note="deleted">
199
+ <Icon name="trash" />
200
+ <div>
201
+ <b>{by ? `${by} deleted ${title}` : `${title} was deleted`}</b>
202
+ <p>
203
+ {at ? `It was deleted at ${at}. ` : ''}It can't be edited.
204
+ </p>
205
+ <div className="acts">
206
+ <button type="button" className="btn sm" data-act="deleted-back" onClick={() => editor.goTo('documents')}>
207
+ Back to documents
208
+ </button>
209
+ </div>
210
+ </div>
211
+ </div>
212
+ );
213
+ }