@caretcms/core 0.2.0 → 0.4.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 (256) hide show
  1. package/README.md +195 -12
  2. package/dist/index.d.ts +26 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +113 -12
  5. package/dist/index.js.map +1 -1
  6. package/dist/loader.d.ts.map +1 -1
  7. package/dist/loader.js +35 -3
  8. package/dist/loader.js.map +1 -1
  9. package/dist/markdown/paragraphs.d.ts +14 -0
  10. package/dist/markdown/paragraphs.d.ts.map +1 -0
  11. package/dist/markdown/paragraphs.js +61 -0
  12. package/dist/markdown/paragraphs.js.map +1 -0
  13. package/dist/markdown/remark.d.ts.map +1 -1
  14. package/dist/markdown/remark.js +1 -0
  15. package/dist/markdown/remark.js.map +1 -1
  16. package/dist/markdown/satteri.d.ts.map +1 -1
  17. package/dist/markdown/satteri.js +35 -9
  18. package/dist/markdown/satteri.js.map +1 -1
  19. package/dist/markdown/stamp.d.ts +0 -9
  20. package/dist/markdown/stamp.d.ts.map +1 -1
  21. package/dist/markdown/stamp.js +6 -0
  22. package/dist/markdown/stamp.js.map +1 -1
  23. package/dist/providers/deployment/github.d.ts +27 -0
  24. package/dist/providers/deployment/github.d.ts.map +1 -0
  25. package/dist/providers/deployment/github.js +180 -0
  26. package/dist/providers/deployment/github.js.map +1 -0
  27. package/dist/providers/deployment/simulated.d.ts +10 -0
  28. package/dist/providers/deployment/simulated.d.ts.map +1 -0
  29. package/dist/providers/deployment/simulated.js +29 -0
  30. package/dist/providers/deployment/simulated.js.map +1 -0
  31. package/dist/runtime/auth/session.d.ts +1 -0
  32. package/dist/runtime/auth/session.d.ts.map +1 -1
  33. package/dist/runtime/auth/session.js +16 -0
  34. package/dist/runtime/auth/session.js.map +1 -1
  35. package/dist/runtime/authorization.d.ts +11 -0
  36. package/dist/runtime/authorization.d.ts.map +1 -0
  37. package/dist/runtime/authorization.js +29 -0
  38. package/dist/runtime/authorization.js.map +1 -0
  39. package/dist/runtime/collection-policy.d.ts +11 -0
  40. package/dist/runtime/collection-policy.d.ts.map +1 -0
  41. package/dist/runtime/collection-policy.js +44 -0
  42. package/dist/runtime/collection-policy.js.map +1 -0
  43. package/dist/runtime/config.d.ts +3 -0
  44. package/dist/runtime/config.d.ts.map +1 -1
  45. package/dist/runtime/config.js +4 -1
  46. package/dist/runtime/config.js.map +1 -1
  47. package/dist/runtime/content-errors.d.ts +9 -0
  48. package/dist/runtime/content-errors.d.ts.map +1 -0
  49. package/dist/runtime/content-errors.js +17 -0
  50. package/dist/runtime/content-errors.js.map +1 -0
  51. package/dist/runtime/content.d.ts.map +1 -1
  52. package/dist/runtime/content.js +13 -4
  53. package/dist/runtime/content.js.map +1 -1
  54. package/dist/runtime/deployment-status.d.ts +14 -0
  55. package/dist/runtime/deployment-status.d.ts.map +1 -0
  56. package/dist/runtime/deployment-status.js +57 -0
  57. package/dist/runtime/deployment-status.js.map +1 -0
  58. package/dist/runtime/draft-state.d.ts +16 -0
  59. package/dist/runtime/draft-state.d.ts.map +1 -0
  60. package/dist/runtime/draft-state.js +29 -0
  61. package/dist/runtime/draft-state.js.map +1 -0
  62. package/dist/runtime/i18n.d.ts +97 -0
  63. package/dist/runtime/i18n.d.ts.map +1 -0
  64. package/dist/runtime/i18n.js +190 -0
  65. package/dist/runtime/i18n.js.map +1 -0
  66. package/dist/runtime/index.d.ts +1 -0
  67. package/dist/runtime/index.d.ts.map +1 -1
  68. package/dist/runtime/index.js +1 -0
  69. package/dist/runtime/index.js.map +1 -1
  70. package/dist/runtime/middleware.d.ts.map +1 -1
  71. package/dist/runtime/middleware.js +61 -4
  72. package/dist/runtime/middleware.js.map +1 -1
  73. package/dist/runtime/mutations/contracts.d.ts +12 -0
  74. package/dist/runtime/mutations/contracts.d.ts.map +1 -1
  75. package/dist/runtime/mutations/contracts.js +54 -4
  76. package/dist/runtime/mutations/contracts.js.map +1 -1
  77. package/dist/runtime/mutations/engine.d.ts +2 -0
  78. package/dist/runtime/mutations/engine.d.ts.map +1 -1
  79. package/dist/runtime/mutations/engine.js +220 -57
  80. package/dist/runtime/mutations/engine.js.map +1 -1
  81. package/dist/runtime/providers.d.ts +4 -1
  82. package/dist/runtime/providers.d.ts.map +1 -1
  83. package/dist/runtime/providers.js +18 -1
  84. package/dist/runtime/providers.js.map +1 -1
  85. package/dist/runtime/publish-recovery.d.ts +32 -0
  86. package/dist/runtime/publish-recovery.d.ts.map +1 -0
  87. package/dist/runtime/publish-recovery.js +98 -0
  88. package/dist/runtime/publish-recovery.js.map +1 -0
  89. package/dist/runtime/publish.d.ts +10 -4
  90. package/dist/runtime/publish.d.ts.map +1 -1
  91. package/dist/runtime/publish.js +105 -63
  92. package/dist/runtime/publish.js.map +1 -1
  93. package/dist/runtime/rebuild-webhook.d.ts +4 -0
  94. package/dist/runtime/rebuild-webhook.d.ts.map +1 -1
  95. package/dist/runtime/rebuild-webhook.js +12 -2
  96. package/dist/runtime/rebuild-webhook.js.map +1 -1
  97. package/dist/runtime/request-context.d.ts +7 -1
  98. package/dist/runtime/request-context.d.ts.map +1 -1
  99. package/dist/runtime/request-context.js +7 -1
  100. package/dist/runtime/request-context.js.map +1 -1
  101. package/dist/runtime/rewrite.d.ts.map +1 -1
  102. package/dist/runtime/rewrite.js +28 -2
  103. package/dist/runtime/rewrite.js.map +1 -1
  104. package/dist/runtime/rich-allowlist.js +1 -1
  105. package/dist/runtime/rich-allowlist.js.map +1 -1
  106. package/dist/runtime/routes/admin-entry.d.ts.map +1 -1
  107. package/dist/runtime/routes/admin-entry.js +129 -21
  108. package/dist/runtime/routes/admin-entry.js.map +1 -1
  109. package/dist/runtime/routes/admin.d.ts.map +1 -1
  110. package/dist/runtime/routes/admin.js +28 -14
  111. package/dist/runtime/routes/admin.js.map +1 -1
  112. package/dist/runtime/routes/auth-login.d.ts.map +1 -1
  113. package/dist/runtime/routes/auth-login.js +4 -0
  114. package/dist/runtime/routes/auth-login.js.map +1 -1
  115. package/dist/runtime/routes/auth-logout.d.ts.map +1 -1
  116. package/dist/runtime/routes/auth-logout.js +7 -2
  117. package/dist/runtime/routes/auth-logout.js.map +1 -1
  118. package/dist/runtime/routes/auth-session.d.ts.map +1 -1
  119. package/dist/runtime/routes/auth-session.js +10 -2
  120. package/dist/runtime/routes/auth-session.js.map +1 -1
  121. package/dist/runtime/routes/collection-list.d.ts.map +1 -1
  122. package/dist/runtime/routes/collection-list.js +213 -66
  123. package/dist/runtime/routes/collection-list.js.map +1 -1
  124. package/dist/runtime/routes/deployment.d.ts +5 -0
  125. package/dist/runtime/routes/deployment.d.ts.map +1 -0
  126. package/dist/runtime/routes/deployment.js +38 -0
  127. package/dist/runtime/routes/deployment.js.map +1 -0
  128. package/dist/runtime/routes/draft.d.ts.map +1 -1
  129. package/dist/runtime/routes/draft.js +10 -3
  130. package/dist/runtime/routes/draft.js.map +1 -1
  131. package/dist/runtime/routes/entries.d.ts.map +1 -1
  132. package/dist/runtime/routes/entries.js +87 -43
  133. package/dist/runtime/routes/entries.js.map +1 -1
  134. package/dist/runtime/routes/history.d.ts.map +1 -1
  135. package/dist/runtime/routes/history.js +40 -21
  136. package/dist/runtime/routes/history.js.map +1 -1
  137. package/dist/runtime/routes/mutate.d.ts.map +1 -1
  138. package/dist/runtime/routes/mutate.js +4 -1
  139. package/dist/runtime/routes/mutate.js.map +1 -1
  140. package/dist/runtime/routes/publish.d.ts.map +1 -1
  141. package/dist/runtime/routes/publish.js +127 -33
  142. package/dist/runtime/routes/publish.js.map +1 -1
  143. package/dist/runtime/routes/schema.d.ts.map +1 -1
  144. package/dist/runtime/routes/schema.js +11 -2
  145. package/dist/runtime/routes/schema.js.map +1 -1
  146. package/dist/runtime/routes/studio-home.d.ts.map +1 -1
  147. package/dist/runtime/routes/studio-home.js +41 -20
  148. package/dist/runtime/routes/studio-home.js.map +1 -1
  149. package/dist/runtime/routes/upload.d.ts.map +1 -1
  150. package/dist/runtime/routes/upload.js +3 -0
  151. package/dist/runtime/routes/upload.js.map +1 -1
  152. package/dist/runtime/schema-registry.d.ts +8 -1
  153. package/dist/runtime/schema-registry.d.ts.map +1 -1
  154. package/dist/runtime/schema-registry.js +49 -3
  155. package/dist/runtime/schema-registry.js.map +1 -1
  156. package/dist/runtime/storage/entry-commit.d.ts +8 -0
  157. package/dist/runtime/storage/entry-commit.d.ts.map +1 -0
  158. package/dist/runtime/storage/entry-commit.js +45 -0
  159. package/dist/runtime/storage/entry-commit.js.map +1 -0
  160. package/dist/runtime/storage/filesystem-adapter.d.ts +5 -0
  161. package/dist/runtime/storage/filesystem-adapter.d.ts.map +1 -1
  162. package/dist/runtime/storage/filesystem-adapter.js +12 -3
  163. package/dist/runtime/storage/filesystem-adapter.js.map +1 -1
  164. package/dist/runtime/storage/frontmatter-codec.d.ts +5 -23
  165. package/dist/runtime/storage/frontmatter-codec.d.ts.map +1 -1
  166. package/dist/runtime/storage/frontmatter-codec.js +156 -13
  167. package/dist/runtime/storage/frontmatter-codec.js.map +1 -1
  168. package/dist/runtime/storage/in-memory-adapter.d.ts +7 -0
  169. package/dist/runtime/storage/in-memory-adapter.d.ts.map +1 -1
  170. package/dist/runtime/storage/in-memory-adapter.js +6 -0
  171. package/dist/runtime/storage/in-memory-adapter.js.map +1 -1
  172. package/dist/runtime/storage/markdown-adapter.d.ts.map +1 -1
  173. package/dist/runtime/storage/markdown-adapter.js +18 -20
  174. package/dist/runtime/storage/markdown-adapter.js.map +1 -1
  175. package/dist/runtime/storage/session-overlay-adapter.d.ts +12 -4
  176. package/dist/runtime/storage/session-overlay-adapter.d.ts.map +1 -1
  177. package/dist/runtime/storage/session-overlay-adapter.js +100 -13
  178. package/dist/runtime/storage/session-overlay-adapter.js.map +1 -1
  179. package/dist/runtime/storage/sidecar-meta-store.d.ts +5 -0
  180. package/dist/runtime/storage/sidecar-meta-store.d.ts.map +1 -1
  181. package/dist/runtime/storage/sidecar-meta-store.js +48 -5
  182. package/dist/runtime/storage/sidecar-meta-store.js.map +1 -1
  183. package/dist/runtime/themes/preset-tokens.js +4 -4
  184. package/dist/runtime/themes/preset-tokens.js.map +1 -1
  185. package/dist/runtime/utils.d.ts.map +1 -1
  186. package/dist/runtime/utils.js +3 -4
  187. package/dist/runtime/utils.js.map +1 -1
  188. package/dist/runtime/views/studio-layout.d.ts +3 -0
  189. package/dist/runtime/views/studio-layout.d.ts.map +1 -1
  190. package/dist/runtime/views/studio-layout.js +36 -13
  191. package/dist/runtime/views/studio-layout.js.map +1 -1
  192. package/dist/schema-utils.d.ts +7 -0
  193. package/dist/schema-utils.d.ts.map +1 -1
  194. package/dist/schema-utils.js +143 -0
  195. package/dist/schema-utils.js.map +1 -1
  196. package/dist/types.d.ts +165 -1
  197. package/dist/types.d.ts.map +1 -1
  198. package/package.json +12 -3
  199. package/static/cms/admin-entry.css +144 -13
  200. package/static/cms/admin-entry.js +539 -586
  201. package/static/cms/dev-toolbar/app.js +78 -19
  202. package/static/cms/editor/config.js +35 -15
  203. package/static/cms/editor/content-map.css +8 -1
  204. package/static/cms/editor/content-map.js +62 -23
  205. package/static/cms/editor/deployment-status.js +66 -0
  206. package/static/cms/editor/draft-client.js +87 -0
  207. package/static/cms/editor/guards.js +20 -0
  208. package/static/cms/editor/helpers.js +30 -1
  209. package/static/cms/editor/highlight.js +1 -0
  210. package/static/cms/editor/image-edit.js +44 -1
  211. package/static/cms/editor/image-utils.js +23 -7
  212. package/static/cms/editor/link-follow.js +122 -41
  213. package/static/cms/editor/link-ui.css +10 -1
  214. package/static/cms/editor/linkify.js +2 -0
  215. package/static/cms/editor/md-block-edit.js +140 -37
  216. package/static/cms/editor/md-paragraphs.js +98 -0
  217. package/static/cms/editor/md-serialize.js +1 -1
  218. package/static/cms/editor/panel.js +53 -7
  219. package/static/cms/editor/preview-mode.js +44 -0
  220. package/static/cms/editor/publish-result.js +100 -0
  221. package/static/cms/editor/rich-toolbar.js +58 -15
  222. package/static/cms/editor/sanitize.js +6 -2
  223. package/static/cms/editor/save-queue.js +75 -17
  224. package/static/cms/editor/section-controls/activation.js +3 -0
  225. package/static/cms/editor/section-controls/api.js +25 -7
  226. package/static/cms/editor/section-controls/index.js +60 -8
  227. package/static/cms/editor/section-controls/model.js +39 -4
  228. package/static/cms/editor/section-controls/mutations.js +15 -0
  229. package/static/cms/editor/section-controls/page-context.js +9 -0
  230. package/static/cms/editor/section-controls/pickers.js +53 -2
  231. package/static/cms/editor/section-controls/reorder.js +23 -0
  232. package/static/cms/editor/section-controls/selection.js +6 -0
  233. package/static/cms/editor/section-controls/spacing-drag.js +16 -0
  234. package/static/cms/editor/section-controls/ui-elements.js +7 -0
  235. package/static/cms/editor/section-controls/utils.js +7 -0
  236. package/static/cms/editor/section-controls.css +18 -3
  237. package/static/cms/editor/security.js +13 -2
  238. package/static/cms/editor/stega-hydrate.js +17 -4
  239. package/static/cms/editor/sync.js +475 -56
  240. package/static/cms/editor/text-edit.js +37 -3
  241. package/static/cms/editor/text-link-interactions.js +59 -17
  242. package/static/cms/editor/toast.js +23 -9
  243. package/static/cms/editor/tokens-and-text.css +9 -0
  244. package/static/cms/editor/toolbar.css +5 -0
  245. package/static/cms/editor/toolbar.js +91 -101
  246. package/static/cms/editor.css +1 -1
  247. package/static/cms/editor.js +1 -0
  248. package/static/cms/studio/entry-loader.js +102 -0
  249. package/static/cms/studio/field-model.js +110 -0
  250. package/static/cms/studio/fields.js +319 -0
  251. package/static/cms/studio/history-client.js +74 -0
  252. package/static/cms/studio/mutation-client.js +74 -0
  253. package/static/cms/studio/sync-client.js +107 -0
  254. package/static/cms/studio/upload-client.js +109 -0
  255. package/static/cms/studio-tokens.css +4 -4
  256. package/static/cms/studio.css +24 -0
@@ -0,0 +1,102 @@
1
+ import { templateFromSchema } from './field-model.js';
2
+
3
+ /** @typedef {import('../../../src/schema-utils.js').JsonSchemaNode} Schema */
4
+ /** @typedef {{ path: string, message: string }} ValidationIssue */
5
+ /**
6
+ * @typedef {{
7
+ * kind: 'ready', data: Record<string, unknown>, revision: number, schema: Schema | null,
8
+ * validationIssues: ValidationIssue[], publicationFieldName: string | null,
9
+ * initialized: boolean,
10
+ * } | { kind: 'missing', schema: Schema | null, publicationFieldName: string | null }
11
+ * | { kind: 'unauthorized' }
12
+ * | { kind: 'error', code: string }} EntryLoadResult
13
+ */
14
+
15
+ /** @param {unknown} value @returns {value is Record<string, unknown>} */
16
+ function isRecord(value) {
17
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
18
+ }
19
+
20
+ /** @param {unknown} value @returns {ValidationIssue[]} */
21
+ function validationIssues(value) {
22
+ if (!Array.isArray(value)) return [];
23
+ return value.flatMap(issue => isRecord(issue)
24
+ && typeof issue.path === 'string'
25
+ && typeof issue.message === 'string'
26
+ ? [{ path: issue.path, message: issue.message }]
27
+ : []);
28
+ }
29
+
30
+ /** @param {unknown} value @returns {Schema | null} */
31
+ function schemaValue(value) {
32
+ return isRecord(value) ? /** @type {Schema} */ (value) : null;
33
+ }
34
+
35
+ /** @param {{ apiBasePath: string, fetchImpl?: typeof fetch }} options */
36
+ export function createStudioEntryLoader({ apiBasePath, fetchImpl = fetch }) {
37
+ /**
38
+ * @param {{ collection: string, id: string, isNew?: boolean, initializeIfMissing?: boolean }} input
39
+ * @returns {Promise<EntryLoadResult>}
40
+ */
41
+ async function load(input) {
42
+ const entrySearch = new URLSearchParams({ collection: input.collection, id: input.id });
43
+ const schemaSearch = new URLSearchParams({ collection: input.collection });
44
+ const [entryResponse, schemaResponse] = await Promise.all([
45
+ fetchImpl(`${apiBasePath}/entries?${entrySearch}`, { credentials: 'same-origin' }),
46
+ fetchImpl(`${apiBasePath}/schema?${schemaSearch}`, { credentials: 'same-origin' }),
47
+ ]);
48
+ if (entryResponse.status === 401 || schemaResponse.status === 401) return { kind: 'unauthorized' };
49
+
50
+ if (!entryResponse.ok) {
51
+ const failure = await entryResponse.json().catch(() => null);
52
+ return {
53
+ kind: 'error',
54
+ code: isRecord(failure) && typeof failure.code === 'string' ? failure.code : 'loadFailed',
55
+ };
56
+ }
57
+
58
+ const entryBody = await entryResponse.json().catch(() => null);
59
+ if (!isRecord(entryBody) || !Array.isArray(entryBody.entries)) {
60
+ return { kind: 'error', code: 'loadFailed' };
61
+ }
62
+ const schemaBody = schemaResponse.ok
63
+ ? await schemaResponse.json().catch(() => null)
64
+ : null;
65
+ const schema = isRecord(schemaBody) ? schemaValue(schemaBody.schema) : null;
66
+ const metadata = isRecord(schemaBody) && isRecord(schemaBody.metadata) ? schemaBody.metadata : null;
67
+ const publication = metadata && isRecord(metadata.publication) ? metadata.publication : null;
68
+ const publicationFieldName = publication
69
+ ? (typeof publication.field === 'string' ? publication.field : 'published')
70
+ : null;
71
+
72
+ const rawEntry = entryBody.entries[0];
73
+ if (rawEntry !== undefined) {
74
+ if (!isRecord(rawEntry) || !isRecord(rawEntry.data)) {
75
+ return { kind: 'error', code: 'loadFailed' };
76
+ }
77
+ return {
78
+ kind: 'ready',
79
+ data: rawEntry.data,
80
+ revision: typeof rawEntry.revision === 'number' ? rawEntry.revision : 0,
81
+ schema,
82
+ validationIssues: validationIssues(rawEntry.validationIssues),
83
+ publicationFieldName,
84
+ initialized: false,
85
+ };
86
+ }
87
+
88
+ if (!input.isNew && !input.initializeIfMissing) {
89
+ return { kind: 'missing', schema, publicationFieldName };
90
+ }
91
+ const template = isRecord(schemaBody) && isRecord(schemaBody.template)
92
+ ? schemaBody.template
93
+ : templateFromSchema(schema);
94
+ if (!isRecord(template)) return { kind: 'error', code: 'loadFailed' };
95
+ return {
96
+ kind: 'ready', data: template, revision: 0, schema,
97
+ validationIssues: [], publicationFieldName, initialized: true,
98
+ };
99
+ }
100
+
101
+ return { load };
102
+ }
@@ -0,0 +1,110 @@
1
+ /** Stateless Studio schema defaults and field presentation helpers. */
2
+ /** @typedef {import('../../../src/schema-utils.js').JsonSchemaNode} Schema */
3
+
4
+ /** @template T @param {T} v @returns {T} */
5
+ export function deepClone(v) { return JSON.parse(JSON.stringify(v)); }
6
+
7
+ /** @param {Schema | null | undefined} schema @returns {unknown} */
8
+ export function templateFromSchema(schema) {
9
+ if (!schema || typeof schema !== "object") return "";
10
+ if (schema.default !== undefined) return deepClone(schema.default);
11
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) return deepClone(schema.enum[0]);
12
+ if (schema.type === "object") {
13
+ /** @type {Record<string, unknown>} */
14
+ var objectTemplate = {};
15
+ Object.keys(schema.properties || {}).forEach(function (key) {
16
+ objectTemplate[key] = templateFromSchema(schema.properties?.[key]);
17
+ });
18
+ return objectTemplate;
19
+ }
20
+ if (schema.type === "array") return [];
21
+ if (schema.type === "boolean") return false;
22
+ if (schema.type === "number" || schema.type === "integer") {
23
+ return typeof schema.minimum === "number" ? schema.minimum : 0;
24
+ }
25
+ return "";
26
+ }
27
+
28
+ /** @param {string} path @param {number} index @returns {string} */
29
+ export function generatedId(path, index) {
30
+ var parts = path.split(".").filter(Boolean);
31
+ var key = parts.pop() || "item";
32
+ if (/^\d+$/.test(key)) key = parts.pop() || "item";
33
+ key = key.replace(/ies$/, "y").replace(/s$/, "");
34
+ return (key + "-" + (index + 1) + "-" + Date.now().toString(36))
35
+ .toLowerCase()
36
+ .replace(/[^a-z0-9]+/g, "-")
37
+ .replace(/^-|-$/g, "");
38
+ }
39
+
40
+ /** @param {string} filename @returns {string} */
41
+ export function friendlyFileTitle(filename) {
42
+ return String(filename || "Image")
43
+ .replace(/\.[^.]+$/, "")
44
+ .replace(/[-_]+/g, " ")
45
+ .replace(/\s+/g, " ")
46
+ .trim()
47
+ .replace(/\b\w/g, function (c) { return c.toUpperCase(); }) || "Image";
48
+ }
49
+
50
+ /** @param {string} path @returns {string} */
51
+ export function singularItemLabel(path) {
52
+ var key = (path.split(".").filter(Boolean).pop() || "item").toLowerCase();
53
+ if (key === "images") return "image";
54
+ if (key === "details") return "detail";
55
+ if (key === "documents") return "document";
56
+ if (key === "videos") return "video";
57
+ if (key === "links") return "link";
58
+ if (key === "resume") return "résumé item";
59
+ return key.replace(/ies$/, "y").replace(/s$/, "") || "item";
60
+ }
61
+
62
+ /** @param {Record<string, unknown> | null | undefined} data @returns {string} */
63
+ export function getTitle(data) {
64
+ if (!data) return "Untitled";
65
+ var keys = ["name", "title", "question", "company_name", "headline", "label"];
66
+ for (var i = 0; i < keys.length; i++) {
67
+ const value = data[keys[i]];
68
+ if (typeof value === "string" && value) return value;
69
+ }
70
+ return "Untitled";
71
+ }
72
+
73
+ /** @param {string} key @returns {string} */
74
+ export function humanizeKey(key) {
75
+ return key.replace(/_/g, " ").replace(/\b\w/g, function (c) { return c.toUpperCase(); });
76
+ }
77
+
78
+ /** @param {string} path @returns {string} */
79
+ export function fieldIdFromPath(path) {
80
+ return "caret-field-" + String(path || "").replace(/[^a-zA-Z0-9_-]/g, "-");
81
+ }
82
+
83
+ /** @param {unknown} data @param {string} path @returns {unknown} */
84
+ export function getNestedValue(data, path) {
85
+ return path.split(".").reduce(function (value, key) {
86
+ if (value === null || typeof value !== "object") return undefined;
87
+ return /** @type {Record<string, unknown>} */ (value)[key];
88
+ }, data);
89
+ }
90
+
91
+ /**
92
+ * @param {Record<string, unknown>} data
93
+ * @param {string} path
94
+ * @param {unknown} value
95
+ */
96
+ export function setNestedValue(data, path, value) {
97
+ var keys = path.split(".");
98
+ /** @type {Record<string, unknown>} */
99
+ var target = data;
100
+ for (var i = 0; i < keys.length - 1; i++) {
101
+ var key = keys[i];
102
+ var next = target[key];
103
+ if (next === null || typeof next !== "object") {
104
+ next = /^\d+$/.test(keys[i + 1]) ? [] : {};
105
+ target[key] = next;
106
+ }
107
+ target = /** @type {Record<string, unknown>} */ (next);
108
+ }
109
+ target[keys[keys.length - 1]] = value;
110
+ }
@@ -0,0 +1,319 @@
1
+ /** Studio field rendering. Persistence and specialized widgets are injected. */
2
+ import { fieldIdFromPath, humanizeKey, templateFromSchema } from './field-model.js';
3
+ /** @typedef {import('./field-model.js').Schema} Schema */
4
+ /** @typedef {Record<string, unknown>} Entry */
5
+
6
+ /** @param {string} label @param {boolean} isRequired @param {boolean} [inline] */
7
+ export function createFieldGroup(label, isRequired, inline) {
8
+ var wrapper = document.createElement("div");
9
+ wrapper.className = "caret-field-group" + (inline ? " caret-field-inline" : "");
10
+
11
+ var labelEl = document.createElement("label");
12
+ labelEl.className = "studio-label";
13
+ labelEl.textContent = label;
14
+ if (isRequired === false) {
15
+ var opt = document.createElement("span");
16
+ opt.textContent = " (optional)";
17
+ opt.style.cssText = "color:var(--studio-text-dim);font-weight:400;text-transform:none;letter-spacing:normal;";
18
+ labelEl.appendChild(opt);
19
+ }
20
+ wrapper.appendChild(labelEl);
21
+
22
+ // Once children are populated, link the label to the first form control
23
+ // for screen-reader accessibility. Path is read from data-fieldPath
24
+ // (set by the caller after createFieldGroup returns).
25
+ queueMicrotask(function () {
26
+ var path = wrapper.dataset.fieldPath;
27
+ if (!path) return;
28
+ // Nested object/array groups contain other field groups. Only claim a
29
+ // control whose nearest field-group owner is this wrapper; otherwise an
30
+ // outer `hero` group can overwrite `hero.headline`'s stable identity.
31
+ var control = Array.from(/** @type {NodeListOf<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>} */ (wrapper.querySelectorAll("input, select, textarea"))).find(function (candidate) {
32
+ return candidate.closest(".caret-field-group") === wrapper;
33
+ });
34
+ if (!control) return;
35
+ var fid = fieldIdFromPath(path);
36
+ control.id = fid;
37
+ control.name = path;
38
+ labelEl.htmlFor = control.id;
39
+ });
40
+
41
+ return wrapper;
42
+ }
43
+
44
+ /**
45
+ * @param {{
46
+ * updateField: (path: string, value: unknown) => void,
47
+ * buildSingleImage: (value: unknown, path: string) => HTMLElement,
48
+ * buildImageGallery: (value: unknown, path: string) => HTMLElement,
49
+ * buildObjectArray: (value: unknown, path: string, schema: Schema | null) => HTMLElement,
50
+ * buildTagList: (value: unknown, path: string) => HTMLElement,
51
+ * getPublicationField: () => string | null
52
+ * }} dependencies
53
+ */
54
+ export function createFieldRenderer({ updateField, buildSingleImage, buildImageGallery, buildObjectArray, buildTagList, getPublicationField }) {
55
+ /* ─── Schema-driven rendering ───────────────────────────────────── */
56
+ /** @param {HTMLElement} container @param {Entry | null} data @param {Schema | null} schema @param {string} basePath */
57
+ function renderFields(container, data, schema, basePath) {
58
+ if (!schema || !schema.properties) {
59
+ renderFieldsFallback(container, data, basePath);
60
+ return;
61
+ }
62
+
63
+ var required = new Set(schema.required || []);
64
+
65
+ const properties = schema.properties;
66
+ Object.keys(properties).forEach(function (key) {
67
+ var prop = properties[key];
68
+ var path = basePath ? basePath + "." + key : key;
69
+ var label = String(prop.title || humanizeKey(key));
70
+ var isRequired = required.has(key);
71
+ var value = data ? data[key] : undefined;
72
+
73
+ // Backfill missing keys with sensible defaults
74
+ if (value === undefined && data) {
75
+ data[key] = templateFromSchema(prop);
76
+ }
77
+ var current = data ? data[key] : undefined;
78
+
79
+ var group;
80
+
81
+ if (prop.type === "string" && prop.enum) {
82
+ group = createFieldGroup(label, isRequired);
83
+ group.dataset.fieldPath = path;
84
+ var select = document.createElement("select");
85
+ select.className = "studio-input";
86
+ prop.enum.forEach(function (opt) {
87
+ var option = document.createElement("option");
88
+ option.value = String(opt);
89
+ option.textContent = String(opt).charAt(0).toUpperCase() + String(opt).slice(1);
90
+ if (opt === current) option.selected = true;
91
+ select.appendChild(option);
92
+ });
93
+ select.addEventListener("change", function () { updateField(path, select.value); });
94
+ group.appendChild(select);
95
+ } else if (prop.type === "string" && prop.format === "image") {
96
+ group = createFieldGroup(label, isRequired);
97
+ group.dataset.fieldPath = path;
98
+ group.appendChild(buildSingleImage(current || "", path));
99
+ } else if (prop.type === "string" && (prop.format === "textarea" || prop.format === "html")) {
100
+ group = createFieldGroup(label, isRequired);
101
+ group.dataset.fieldPath = path;
102
+ var ta = document.createElement("textarea");
103
+ ta.className = "studio-input";
104
+ ta.rows = 4;
105
+ ta.value = String(current || "");
106
+ ta.addEventListener("input", function () { updateField(path, ta.value); });
107
+ group.appendChild(ta);
108
+ } else if (prop.type === "string" && (prop.format === "email" || prop.format === "uri" || prop.format === "url")) {
109
+ group = createFieldGroup(label, isRequired);
110
+ group.dataset.fieldPath = path;
111
+ var emailInput = document.createElement("input");
112
+ emailInput.type = prop.format === "email" ? "email" : "url";
113
+ emailInput.className = "studio-input";
114
+ emailInput.value = String(current || "");
115
+ emailInput.addEventListener("input", function () { updateField(path, emailInput.value); });
116
+ group.appendChild(emailInput);
117
+ } else if (prop.type === "string") {
118
+ group = createFieldGroup(label, isRequired);
119
+ group.dataset.fieldPath = path;
120
+ /** @type {HTMLInputElement | HTMLTextAreaElement} */
121
+ var input;
122
+ if (typeof current === "string" && (current.length > 100 || /[\r\n]/.test(current))) {
123
+ input = document.createElement("textarea");
124
+ input.rows = 4;
125
+ } else {
126
+ input = document.createElement("input");
127
+ input.type = "text";
128
+ }
129
+ input.className = "studio-input";
130
+ input.value = String(current || "");
131
+ input.addEventListener("input", function () { updateField(path, input.value); });
132
+ group.appendChild(input);
133
+ } else if (prop.type === "number" || prop.type === "integer") {
134
+ group = createFieldGroup(label, isRequired);
135
+ group.dataset.fieldPath = path;
136
+ var num = document.createElement("input");
137
+ num.type = "number";
138
+ num.className = "studio-input";
139
+ num.value = String(current == null ? 0 : current);
140
+ if (prop.minimum !== undefined) num.min = String(prop.minimum);
141
+ if (prop.maximum !== undefined) num.max = String(prop.maximum);
142
+ num.addEventListener("input", function () { updateField(path, Number(num.value) || 0); });
143
+ group.appendChild(num);
144
+ } else if (prop.type === "boolean") {
145
+ group = createFieldGroup(label, isRequired, true);
146
+ group.dataset.fieldPath = path;
147
+ var toggle = document.createElement("button");
148
+ toggle.type = "button";
149
+ toggle.className = "studio-toggle" + (current ? " active" : "");
150
+ toggle.addEventListener("click", function () {
151
+ var next = !toggle.classList.contains("active");
152
+ toggle.classList.toggle("active");
153
+ updateField(path, next);
154
+ });
155
+ toggle.setAttribute("role", "switch");
156
+ toggle.setAttribute("aria-label", label);
157
+ toggle.setAttribute("aria-checked", current ? "true" : "false");
158
+ toggle.addEventListener("click", function () {
159
+ toggle.setAttribute("aria-checked", toggle.classList.contains("active") ? "true" : "false");
160
+ });
161
+ group.appendChild(toggle);
162
+ if (path === getPublicationField()) {
163
+ group.classList.add("caret-field-with-help");
164
+ var publishHelp = document.createElement("p");
165
+ publishHelp.className = "caret-field-help";
166
+ publishHelp.textContent = current
167
+ ? "Turn this off and Save to remove the entry from the public site without deleting it."
168
+ : "Turn this on and Save when the entry is ready to appear on the public site.";
169
+ toggle.addEventListener("click", function () {
170
+ publishHelp.textContent = toggle.classList.contains("active")
171
+ ? "Turn this off and Save to remove the entry from the public site without deleting it."
172
+ : "Turn this on and Save when the entry is ready to appear on the public site.";
173
+ });
174
+ group.appendChild(publishHelp);
175
+ }
176
+ } else if (prop.type === "array" && prop.format === "image-gallery") {
177
+ group = createFieldGroup(label, isRequired);
178
+ group.dataset.fieldPath = path;
179
+ group.appendChild(buildImageGallery(current || [], path));
180
+ } else if (prop.type === "array" && prop.items && prop.items.type === "object") {
181
+ group = createFieldGroup(label, isRequired);
182
+ group.dataset.fieldPath = path;
183
+ group.appendChild(buildObjectArray(current || [], path, prop.items));
184
+ } else if (prop.type === "array" && prop.items && prop.items.type === "string") {
185
+ group = createFieldGroup(label, isRequired);
186
+ group.dataset.fieldPath = path;
187
+ group.appendChild(buildTagList(current || [], path));
188
+ } else if (prop.type === "object" && prop.properties) {
189
+ group = createFieldGroup(label, isRequired);
190
+ group.dataset.fieldPath = path;
191
+ var nested = document.createElement("div");
192
+ nested.className = "caret-nested";
193
+ renderFields(nested, /** @type {Entry} */ (current || {}), prop, path);
194
+ group.appendChild(nested);
195
+ } else {
196
+ group = createFieldGroup(label, isRequired);
197
+ group.dataset.fieldPath = path;
198
+ var jsonTa = document.createElement("textarea");
199
+ jsonTa.className = "studio-input";
200
+ jsonTa.style.fontFamily = "var(--studio-font-mono)";
201
+ jsonTa.style.fontSize = "11px";
202
+ jsonTa.rows = 4;
203
+ jsonTa.value = JSON.stringify(current, null, 2);
204
+ jsonTa.addEventListener("input", function () {
205
+ try { updateField(path, JSON.parse(jsonTa.value)); } catch (e) { /* ignore until valid */ }
206
+ });
207
+ group.appendChild(jsonTa);
208
+ }
209
+
210
+ container.appendChild(group);
211
+ });
212
+ }
213
+
214
+ /** @param {HTMLElement} container @param {Entry | null} data @param {string} basePath */
215
+ function renderFieldsFallback(container, data, basePath) {
216
+ if (!data || typeof data !== "object") return;
217
+ Object.keys(data).forEach(function (key) {
218
+ var value = data[key];
219
+ var path = basePath ? basePath + "." + key : key;
220
+ var label = humanizeKey(key);
221
+
222
+ if (key === "images" && Array.isArray(value) && value.every(function (v) { return typeof v === "string"; })) {
223
+ var g = createFieldGroup(label, true);
224
+ g.dataset.fieldPath = path;
225
+ g.appendChild(buildImageGallery(value, path));
226
+ container.appendChild(g);
227
+ return;
228
+ }
229
+ if ((key === "image" || key === "thumbnail" || key === "bg_image") && typeof value === "string") {
230
+ var g2 = createFieldGroup(label, true);
231
+ g2.dataset.fieldPath = path;
232
+ g2.appendChild(buildSingleImage(value, path));
233
+ container.appendChild(g2);
234
+ return;
235
+ }
236
+ if (Array.isArray(value) && value.every(function (v) { return typeof v === "string"; })) {
237
+ var g3 = createFieldGroup(label, true);
238
+ g3.dataset.fieldPath = path;
239
+ g3.appendChild(buildTagList(value, path));
240
+ container.appendChild(g3);
241
+ return;
242
+ }
243
+ if (Array.isArray(value) && value.every(function (v) { return typeof v === "object" && v !== null && !Array.isArray(v); })) {
244
+ var g4 = createFieldGroup(label, true);
245
+ g4.dataset.fieldPath = path;
246
+ g4.appendChild(buildObjectArray(value, path, null));
247
+ container.appendChild(g4);
248
+ return;
249
+ }
250
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
251
+ var g5 = createFieldGroup(label, true);
252
+ g5.dataset.fieldPath = path;
253
+ var nested = document.createElement("div");
254
+ nested.className = "caret-nested";
255
+ renderFieldsFallback(nested, /** @type {Entry} */ (value), path);
256
+ g5.appendChild(nested);
257
+ container.appendChild(g5);
258
+ return;
259
+ }
260
+ if (typeof value === "boolean") {
261
+ var g6 = createFieldGroup(label, true, true);
262
+ g6.dataset.fieldPath = path;
263
+ var t = document.createElement("button");
264
+ t.type = "button";
265
+ t.className = "studio-toggle" + (value ? " active" : "");
266
+ t.addEventListener("click", function () {
267
+ var next = !t.classList.contains("active");
268
+ t.classList.toggle("active");
269
+ updateField(path, next);
270
+ });
271
+ g6.appendChild(t);
272
+ container.appendChild(g6);
273
+ return;
274
+ }
275
+ if (typeof value === "number") {
276
+ var g7 = createFieldGroup(label, true);
277
+ g7.dataset.fieldPath = path;
278
+ var n = document.createElement("input");
279
+ n.type = "number";
280
+ n.className = "studio-input";
281
+ n.value = String(value);
282
+ n.addEventListener("input", function () { updateField(path, Number(n.value) || 0); });
283
+ g7.appendChild(n);
284
+ container.appendChild(g7);
285
+ return;
286
+ }
287
+ if (typeof value === "string") {
288
+ var g8 = createFieldGroup(label, true);
289
+ g8.dataset.fieldPath = path;
290
+ /** @type {HTMLInputElement | HTMLTextAreaElement} */
291
+ var i;
292
+ if (value.length > 100 || /[\r\n]/.test(value)) { i = document.createElement("textarea"); i.rows = 4; }
293
+ else { i = document.createElement("input"); i.type = "text"; }
294
+ i.className = "studio-input";
295
+ i.value = value;
296
+ i.addEventListener("input", function () { updateField(path, i.value); });
297
+ g8.appendChild(i);
298
+ container.appendChild(g8);
299
+ return;
300
+ }
301
+ // Fallback to JSON
302
+ var g9 = createFieldGroup(label, true);
303
+ g9.dataset.fieldPath = path;
304
+ var jt = document.createElement("textarea");
305
+ jt.className = "studio-input";
306
+ jt.style.fontFamily = "var(--studio-font-mono)";
307
+ jt.style.fontSize = "11px";
308
+ jt.rows = 4;
309
+ jt.value = JSON.stringify(value, null, 2);
310
+ jt.addEventListener("input", function () {
311
+ try { updateField(path, JSON.parse(jt.value)); } catch (e) { /* ignore */ }
312
+ });
313
+ g9.appendChild(jt);
314
+ container.appendChild(g9);
315
+ });
316
+ }
317
+
318
+ return { renderFields, renderFieldsFallback };
319
+ }
@@ -0,0 +1,74 @@
1
+ /** @typedef {{ id: string, name?: string, email?: string }} HistoryEditor */
2
+ /** @typedef {{ ts: number, action: string, editor?: HistoryEditor }} HistoryItem */
3
+ /**
4
+ * @typedef {{ kind: 'loaded', items: HistoryItem[] }
5
+ * | { kind: 'unauthorized' }
6
+ * | { kind: 'error' }} HistoryResult
7
+ */
8
+ /**
9
+ * @typedef {{ kind: 'restored', data: unknown, revision?: number }
10
+ * | { kind: 'unauthorized' }
11
+ * | { kind: 'error' }} RestoreResult
12
+ */
13
+
14
+ /** @param {unknown} value @returns {value is Record<string, unknown>} */
15
+ function isRecord(value) {
16
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
17
+ }
18
+
19
+ /** @param {unknown} value @returns {HistoryItem[]} */
20
+ function historyItems(value) {
21
+ if (!Array.isArray(value)) return [];
22
+ return value.flatMap(item => {
23
+ if (!isRecord(item) || typeof item.ts !== 'number' || !Number.isFinite(item.ts)) return [];
24
+ /** @type {HistoryItem} */
25
+ const normalized = {
26
+ ts: item.ts,
27
+ action: typeof item.action === 'string' && item.action ? item.action : 'save',
28
+ };
29
+ if (isRecord(item.editor) && typeof item.editor.id === 'string') {
30
+ normalized.editor = { id: item.editor.id };
31
+ if (typeof item.editor.name === 'string') normalized.editor.name = item.editor.name;
32
+ if (typeof item.editor.email === 'string') normalized.editor.email = item.editor.email;
33
+ }
34
+ return [normalized];
35
+ });
36
+ }
37
+
38
+ /** @param {{ apiBasePath: string, fetchImpl?: typeof fetch }} options */
39
+ export function createStudioHistoryClient({ apiBasePath, fetchImpl = fetch }) {
40
+ /** @param {{ collection: string, id: string }} input @returns {Promise<HistoryResult>} */
41
+ async function list(input) {
42
+ const search = new URLSearchParams(input);
43
+ const response = await fetchImpl(`${apiBasePath}/history?${search}`, {
44
+ credentials: 'same-origin',
45
+ });
46
+ if (response.status === 401) return { kind: 'unauthorized' };
47
+ if (!response.ok) return { kind: 'error' };
48
+ const body = await response.json().catch(() => null);
49
+ return isRecord(body)
50
+ ? { kind: 'loaded', items: historyItems(body.history) }
51
+ : { kind: 'error' };
52
+ }
53
+
54
+ /** @param {{ collection: string, id: string, ts: number }} input @returns {Promise<RestoreResult>} */
55
+ async function restore(input) {
56
+ const response = await fetchImpl(`${apiBasePath}/history`, {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json', 'x-caret-request': '1' },
59
+ credentials: 'same-origin',
60
+ body: JSON.stringify(input),
61
+ });
62
+ if (response.status === 401) return { kind: 'unauthorized' };
63
+ if (!response.ok) return { kind: 'error' };
64
+ const body = await response.json().catch(() => null);
65
+ if (!isRecord(body) || !Object.prototype.hasOwnProperty.call(body, 'data')) {
66
+ return { kind: 'error' };
67
+ }
68
+ return typeof body.revision === 'number'
69
+ ? { kind: 'restored', data: body.data, revision: body.revision }
70
+ : { kind: 'restored', data: body.data };
71
+ }
72
+
73
+ return { list, restore };
74
+ }
@@ -0,0 +1,74 @@
1
+ /** @typedef {{ path: string, message: string }} ValidationIssue */
2
+ /**
3
+ * @typedef {{ kind: 'saved', revision?: number }
4
+ * | { kind: 'validation', issues: ValidationIssue[] }
5
+ * | { kind: 'conflict', currentRevision: number }
6
+ * | { kind: 'unauthorized' }
7
+ * | { kind: 'error' }} SaveResult
8
+ */
9
+
10
+ /** @param {unknown} value @returns {value is Record<string, unknown>} */
11
+ function isRecord(value) {
12
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
13
+ }
14
+
15
+ /** @param {unknown} value @returns {ValidationIssue[]} */
16
+ function validationIssues(value) {
17
+ if (!Array.isArray(value)) return [];
18
+ return value.flatMap(issue => isRecord(issue)
19
+ && typeof issue.path === 'string'
20
+ && typeof issue.message === 'string'
21
+ ? [{ path: issue.path, message: issue.message }]
22
+ : []);
23
+ }
24
+
25
+ /**
26
+ * @param {{ apiBasePath: string, fetchImpl?: typeof fetch }} options
27
+ */
28
+ export function createStudioMutationClient({ apiBasePath, fetchImpl = fetch }) {
29
+ /** @param {Record<string, unknown>} payload */
30
+ async function mutate(payload) {
31
+ const response = await fetchImpl(`${apiBasePath}/mutate`, {
32
+ method: 'POST',
33
+ headers: { 'Content-Type': 'application/json', 'x-caret-request': '1' },
34
+ credentials: 'same-origin',
35
+ body: JSON.stringify(payload),
36
+ });
37
+ if (response.status === 401) return { response, body: null };
38
+ const body = await response.json().catch(() => null);
39
+ return { response, body };
40
+ }
41
+
42
+ /**
43
+ * @param {{ collection: string, id: string, data: Record<string, unknown>, expectedRevision?: number }} input
44
+ * @returns {Promise<SaveResult>}
45
+ */
46
+ async function save(input) {
47
+ const payload = { type: 'put_entry', ...input };
48
+ const { response, body } = await mutate(payload);
49
+ if (response.status === 401) return { kind: 'unauthorized' };
50
+ if (!response.ok) {
51
+ const issues = isRecord(body) ? validationIssues(body.issues) : [];
52
+ if (issues.length > 0) return { kind: 'validation', issues };
53
+ if (response.status === 409 && isRecord(body) && typeof body.currentRevision === 'number') {
54
+ return { kind: 'conflict', currentRevision: body.currentRevision };
55
+ }
56
+ return { kind: 'error' };
57
+ }
58
+ return isRecord(body) && typeof body.revision === 'number'
59
+ ? { kind: 'saved', revision: body.revision }
60
+ : { kind: 'saved' };
61
+ }
62
+
63
+ /**
64
+ * @param {{ collection: string, id: string, expectedRevision?: number }} input
65
+ * @returns {Promise<'deleted' | 'unauthorized' | 'error'>}
66
+ */
67
+ async function remove(input) {
68
+ const { response } = await mutate({ type: 'delete_entry', ...input });
69
+ if (response.status === 401) return 'unauthorized';
70
+ return response.ok ? 'deleted' : 'error';
71
+ }
72
+
73
+ return { save, remove };
74
+ }