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,307 @@
1
+ /**
2
+ * Link (M5a Decision 4): `{ "label", "href" }`. The mockup's `.lnk`: where it goes
3
+ * is a document picked from the site (shown by its title and address; the URL is
4
+ * what's stored) or a typed `/path`, `https://…`, `mailto:`, `tel:` or `#anchor`.
5
+ * Typing a name lists the site's documents; typing an address keeps it as typed.
6
+ *
7
+ * - A link that fails the link rule is kept as typed, with the rule's message
8
+ * (Decision 15); the page shows its label as text.
9
+ * - A `/path` that matches no document says so ("Links to /contact, which no page
10
+ * or route serves"), never an error.
11
+ * - Half a link (only a label, or only where it goes) says it shows nothing.
12
+ * - M6c (G9): an address-only link (`addressOnly`) is where it goes alone: no Label box,
13
+ * never half a link; the rule, the picker and the missing-page warning as any link's.
14
+ *
15
+ * - M6c G4 (Decisions 5, 7, 10): **File…** beside the box opens the library on Files as a
16
+ * picker (`pick.link`), and the file's address is what's stored. A file link shows the
17
+ * file (its tile, name, type and size); a PDF gets "When clicked": New tab (the
18
+ * default) or Download (`?download` in the address); any other type says it downloads.
19
+ * A file link whose key isn't in the library gets the missing-target warning.
20
+ *
21
+ * `LinkControl` is the "where it goes" part alone, which buttons and lists use too.
22
+ */
23
+ import { useEffect, useState } from 'react';
24
+ import type { FieldWarning, LinkField as LinkDescriptor, LinkValue } from '../../../fields.ts';
25
+ import { fileAddress, fileKeyOf, fileTypeOf } from '../../../files.ts';
26
+ import { internalLinkPath } from '../../../links.ts';
27
+ import { useEditor, useStore } from '../context.tsx';
28
+ import { docIcon } from '../nav/DocRow.tsx';
29
+ import { parentLabel } from '../nav/search.ts';
30
+ import { Icon } from '../shell/Icons.tsx';
31
+ import { FieldFrame } from './FieldFrame.tsx';
32
+ import { fileSize, mediaFor } from '../media/store.ts';
33
+ import { FileTile } from '../pane/levels/MediaLevel.tsx';
34
+ import { useFieldWarnings, useHost, warningsAt } from './host.tsx';
35
+ import { ownerOf } from './MediaField.tsx';
36
+ import { documentAt, FieldIcon, LinkPopover, looksLikeAddress, useLinkable, Warnings } from './parts.tsx';
37
+
38
+ /** A file link's address with New tab or Download chosen, its fragment kept. */
39
+ export function withDownload(href: string, download: boolean): string {
40
+ const file = fileKeyOf(href.trim());
41
+ if (!file) return href;
42
+ const hash = href.indexOf('#');
43
+ return `${fileAddress(file.key, { download })}${hash === -1 ? '' : href.slice(hash)}`;
44
+ }
45
+
46
+ /** M6c G4: "When clicked": New tab or Download, for a link to a PDF (Decision 5). */
47
+ export function OpensToggle(props: { href: string; onChange: (href: string) => void; disabled?: boolean }) {
48
+ const download = !!fileKeyOf(props.href.trim())?.download;
49
+ return (
50
+ <div className="opens">
51
+ <span className="lbl">When clicked</span>
52
+ <div className="mini-seg" role="radiogroup" aria-label="When clicked">
53
+ <button type="button" role="radio" aria-checked={!download} data-opens="tab" disabled={props.disabled} onClick={() => props.onChange(withDownload(props.href, false))}>
54
+ <Icon name="ext" />
55
+ New tab
56
+ </button>
57
+ <button type="button" role="radio" aria-checked={download} data-opens="download" disabled={props.disabled} onClick={() => props.onChange(withDownload(props.href, true))}>
58
+ <Icon name="download" />
59
+ Download
60
+ </button>
61
+ </div>
62
+ </div>
63
+ );
64
+ }
65
+
66
+ /** The library's files, read once when a file link shows (last known meanwhile). */
67
+ export function useLibraryFiles() {
68
+ const editor = useEditor();
69
+ const media = mediaFor(editor);
70
+ const files = useStore(media.files);
71
+ useEffect(() => {
72
+ if (media.files.get().list === null && !media.files.get().unset) void media.loadImages();
73
+ }, [media]);
74
+ return files;
75
+ }
76
+
77
+ /**
78
+ * Where a link goes. `path` is the href's own path (`cta.href`, `buttons.1.href`),
79
+ * `warnings` the ones about it (link rule, a document that doesn't exist).
80
+ */
81
+ export function LinkControl(props: { path: string; href: string; onChange: (href: string) => void; warnings: readonly FieldWarning[]; label?: string }) {
82
+ const editor = useEditor();
83
+ const host = useHost();
84
+ const documents = useStore(editor.documents);
85
+ const linkable = useLinkable();
86
+ const [query, setQuery] = useState<string | null>(null);
87
+ const href = props.href;
88
+ const file = fileKeyOf(href.trim());
89
+ const doc = href && !file ? documentAt(href, linkable) : undefined;
90
+ const missing = props.warnings.find((w) => w.code === 'link-missing');
91
+ const hiddenTarget = props.warnings.find((w) => w.code === 'link-hidden');
92
+ const rule = props.warnings.find((w) => w.code === 'link-rule');
93
+ const files = useLibraryFiles();
94
+ const chooseFile = () => {
95
+ if (host.readOnly) return;
96
+ const { owner, back } = ownerOf(editor, host.scope);
97
+ editor.openMedia({ pick: { scope: host.scope, path: props.path, label: `${owner} · ${props.label ?? 'Link'}`, owner, multi: false, back, link: true } });
98
+ };
99
+ const fileButton = (
100
+ <button type="button" className="btn" data-act="link-file" title="Link to a file in the library" disabled={host.readOnly} onClick={chooseFile}>
101
+ <FieldIcon name="file" size="sm" />
102
+ File…
103
+ </button>
104
+ );
105
+ const focusInput = () => requestAnimationFrame(() => document.querySelector<HTMLInputElement>(`input[data-path="${CSS.escape(props.path)}"]`)?.focus());
106
+ const clear = (
107
+ <button
108
+ type="button"
109
+ className="ib"
110
+ aria-label="Change where it goes"
111
+ onClick={() => {
112
+ props.onChange('');
113
+ focusInput();
114
+ }}
115
+ >
116
+ <Icon name="x" size="sm" />
117
+ </button>
118
+ );
119
+
120
+ if (file && !rule) {
121
+ const entry = files.list?.find((f) => f.key === file.key);
122
+ const name = entry?.name ?? file.key.slice(file.key.lastIndexOf('/') + 1);
123
+ const type = fileTypeOf(file.key)!;
124
+ if (missing) {
125
+ return (
126
+ <>
127
+ <div className="lnk-row">
128
+ <div className="lnk is-missing" data-link-path={props.path}>
129
+ <div className="lnk-box" data-path={props.path} tabIndex={-1}>
130
+ <Icon name="warn" />
131
+ <span className="lnk-doc two">
132
+ <b>{name}</b>
133
+ <span>isn't in the library</span>
134
+ </span>
135
+ {clear}
136
+ </div>
137
+ </div>
138
+ {fileButton}
139
+ </div>
140
+ <Warnings warnings={[missing]} />
141
+ </>
142
+ );
143
+ }
144
+ return (
145
+ <>
146
+ <div className="lnk" data-link-path={props.path} data-link-file={file.key}>
147
+ <div className="lnk-box" data-path={props.path} tabIndex={-1}>
148
+ <span className="lnk-file">
149
+ <FileTile size="sm" />
150
+ <span className="lnk-doc two">
151
+ <b>{name}</b>
152
+ <span className="mono">
153
+ {type.label}
154
+ {entry?.bytes != null ? ` · ${fileSize(entry.bytes)}` : ''}
155
+ </span>
156
+ </span>
157
+ </span>
158
+ {clear}
159
+ </div>
160
+ </div>
161
+ {type.opensInTab ? (
162
+ <>
163
+ <OpensToggle href={href} onChange={props.onChange} disabled={host.readOnly} />
164
+ <p className="field-hint">
165
+ <FieldIcon name="link" size="xs" />
166
+ <span className="mono" style={{ overflowWrap: 'anywhere' }}>
167
+ {href.trim()}
168
+ </span>
169
+ </p>
170
+ </>
171
+ ) : (
172
+ <p className="dl-note">
173
+ <Icon name="download" size="sm" />
174
+ <span>
175
+ <b>Downloads.</b> {/^[AEIOU]/.test(type.label) ? 'An' : 'A'} {type.label} file doesn’t open in the browser, so clicking it saves the file.
176
+ </span>
177
+ </p>
178
+ )}
179
+ </>
180
+ );
181
+ }
182
+ if (doc && !rule) {
183
+ const collection = editor.site.collections.find((c) => c.name === doc.collection);
184
+ const crumb = collection?.nested ? parentLabel(doc, documents) : '';
185
+ // M9 Decision 7: a link to a hidden page keeps its chip, with the warning under it.
186
+ return (
187
+ <>
188
+ <div className="lnk" data-link-path={props.path} data-link-hidden={hiddenTarget ? '' : undefined}>
189
+ <div className="lnk-box" data-path={props.path} tabIndex={-1}>
190
+ <Icon name={docIcon(collection)} />
191
+ <span className="lnk-doc">
192
+ <b>
193
+ {crumb ? `${crumb} / ` : ''}
194
+ {doc.label || doc.title}
195
+ </b>
196
+ <span className="mono">{doc.url}</span>
197
+ </span>
198
+ {clear}
199
+ </div>
200
+ </div>
201
+ {hiddenTarget ? <Warnings warnings={[hiddenTarget]} /> : null}
202
+ </>
203
+ );
204
+ }
205
+ if (missing && !rule) {
206
+ return (
207
+ <>
208
+ <div className="lnk is-missing" data-link-path={props.path}>
209
+ <div className="lnk-box" data-path={props.path} tabIndex={-1}>
210
+ <Icon name="warn" />
211
+ <span className="lnk-doc">
212
+ <b>{internalLinkPath(href) ?? href}</b>
213
+ <span>no page or route serves it</span>
214
+ </span>
215
+ {clear}
216
+ </div>
217
+ </div>
218
+ <Warnings warnings={[missing]} />
219
+ </>
220
+ );
221
+ }
222
+ const kind = href.startsWith('mailto:') ? 'mail' : href.startsWith('tel:') ? 'phone' : href.startsWith('#') ? 'anchor' : href.startsWith('/') ? 'page' : href ? 'ext' : 'search';
223
+ const open = query !== null && query.trim() !== '' && !looksLikeAddress(query);
224
+ return (
225
+ <>
226
+ <div className="lnk-row">
227
+ <div className={`lnk${rule && !open ? ' is-bad' : ''}`} data-link-path={props.path}>
228
+ <div className="lnk-box">
229
+ {kind === 'anchor' ? (
230
+ <span className="lnk-glyph" aria-hidden="true">
231
+ #
232
+ </span>
233
+ ) : (
234
+ <Icon name={kind} />
235
+ )}
236
+ <input
237
+ value={href}
238
+ placeholder="Find a page, or type an address"
239
+ aria-label={props.label ?? 'Where it goes'}
240
+ data-path={props.path}
241
+ autoComplete="off"
242
+ spellCheck={false}
243
+ aria-invalid={rule && !open ? true : undefined}
244
+ onChange={(e) => {
245
+ setQuery(e.target.value);
246
+ props.onChange(e.target.value);
247
+ }}
248
+ onBlur={() => setQuery(null)}
249
+ onKeyDown={(e) => {
250
+ if (e.key === 'Escape' && open) {
251
+ e.preventDefault();
252
+ setQuery(null);
253
+ }
254
+ }}
255
+ />
256
+ </div>
257
+ {open ? (
258
+ <LinkPopover
259
+ query={query}
260
+ onPick={(next) => {
261
+ props.onChange(next);
262
+ setQuery(null);
263
+ }}
264
+ onClose={() => setQuery(null)}
265
+ />
266
+ ) : null}
267
+ </div>
268
+ {fileButton}
269
+ </div>
270
+ {rule && !open ? <Warnings warnings={[rule]} /> : null}
271
+ </>
272
+ );
273
+ }
274
+
275
+ export function LinkField(props: { name: string; label: string; field: LinkDescriptor; value: unknown }) {
276
+ const host = useHost();
277
+ const id = `f-${host.scope}-${props.name}`;
278
+ const value = (props.value && typeof props.value === 'object' ? props.value : { label: '', href: '' }) as LinkValue;
279
+ const warnings = useFieldWarnings(props.name, props.field, props.value);
280
+ if (props.field.addressOnly) {
281
+ return (
282
+ <FieldFrame blockKey={host.scope} name={props.name} label={props.label} help={props.field.help} labelId={`${id}-l`}>
283
+ <LinkControl path={`${props.name}.href`} href={value.href ?? ''} label={props.label} warnings={warningsAt(warnings, 'href')} onChange={(href) => host.set(`${props.name}.href`, href)} />
284
+ <Warnings warnings={warnings.filter((w) => w.code === 'unreadable')} />
285
+ </FieldFrame>
286
+ );
287
+ }
288
+ return (
289
+ <FieldFrame blockKey={host.scope} name={props.name} label={props.label} help={props.field.help} labelId={`${id}-l`}>
290
+ <label className="sub-label" htmlFor={`${id}-label`}>
291
+ Label
292
+ </label>
293
+ <input
294
+ className="input"
295
+ id={`${id}-label`}
296
+ data-path={`${props.name}.label`}
297
+ value={value.label}
298
+ autoComplete="off"
299
+ spellCheck={false}
300
+ onChange={(e) => host.set(`${props.name}.label`, e.target.value.replace(/\n+/g, ' '))}
301
+ />
302
+ <span className="sub-label">Link</span>
303
+ <LinkControl path={`${props.name}.href`} href={value.href} warnings={warningsAt(warnings, 'href')} onChange={(href) => host.set(`${props.name}.href`, href)} />
304
+ <Warnings warnings={warnings.filter((w) => w.code === 'half-link' || w.code === 'unreadable')} />
305
+ </FieldFrame>
306
+ );
307
+ }