@caretcms/core 0.1.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 (249) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -0
  3. package/dist/browser-runtime.d.ts +12 -0
  4. package/dist/browser-runtime.d.ts.map +1 -0
  5. package/dist/browser-runtime.js +298 -0
  6. package/dist/browser-runtime.js.map +1 -0
  7. package/dist/index.d.ts +81 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +343 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/loader.d.ts +61 -0
  12. package/dist/loader.d.ts.map +1 -0
  13. package/dist/loader.js +80 -0
  14. package/dist/loader.js.map +1 -0
  15. package/dist/providers/storage/filesystem.d.ts +7 -0
  16. package/dist/providers/storage/filesystem.d.ts.map +1 -0
  17. package/dist/providers/storage/filesystem.js +6 -0
  18. package/dist/providers/storage/filesystem.js.map +1 -0
  19. package/dist/providers/uploads/local.d.ts +6 -0
  20. package/dist/providers/uploads/local.d.ts.map +1 -0
  21. package/dist/providers/uploads/local.js +6 -0
  22. package/dist/providers/uploads/local.js.map +1 -0
  23. package/dist/runtime/auth/cookie-utils.d.ts +3 -0
  24. package/dist/runtime/auth/cookie-utils.d.ts.map +1 -0
  25. package/dist/runtime/auth/cookie-utils.js +33 -0
  26. package/dist/runtime/auth/cookie-utils.js.map +1 -0
  27. package/dist/runtime/auth/demo-session.d.ts +24 -0
  28. package/dist/runtime/auth/demo-session.d.ts.map +1 -0
  29. package/dist/runtime/auth/demo-session.js +42 -0
  30. package/dist/runtime/auth/demo-session.js.map +1 -0
  31. package/dist/runtime/auth/rate-limiter.d.ts +26 -0
  32. package/dist/runtime/auth/rate-limiter.d.ts.map +1 -0
  33. package/dist/runtime/auth/rate-limiter.js +65 -0
  34. package/dist/runtime/auth/rate-limiter.js.map +1 -0
  35. package/dist/runtime/auth/session.d.ts +14 -0
  36. package/dist/runtime/auth/session.d.ts.map +1 -0
  37. package/dist/runtime/auth/session.js +128 -0
  38. package/dist/runtime/auth/session.js.map +1 -0
  39. package/dist/runtime/bind.d.ts +14 -0
  40. package/dist/runtime/bind.d.ts.map +1 -0
  41. package/dist/runtime/bind.js +19 -0
  42. package/dist/runtime/bind.js.map +1 -0
  43. package/dist/runtime/config.d.ts +20 -0
  44. package/dist/runtime/config.d.ts.map +1 -0
  45. package/dist/runtime/config.js +40 -0
  46. package/dist/runtime/config.js.map +1 -0
  47. package/dist/runtime/content.d.ts +28 -0
  48. package/dist/runtime/content.d.ts.map +1 -0
  49. package/dist/runtime/content.js +37 -0
  50. package/dist/runtime/content.js.map +1 -0
  51. package/dist/runtime/index.d.ts +14 -0
  52. package/dist/runtime/index.d.ts.map +1 -0
  53. package/dist/runtime/index.js +16 -0
  54. package/dist/runtime/index.js.map +1 -0
  55. package/dist/runtime/middleware.d.ts +14 -0
  56. package/dist/runtime/middleware.d.ts.map +1 -0
  57. package/dist/runtime/middleware.js +80 -0
  58. package/dist/runtime/middleware.js.map +1 -0
  59. package/dist/runtime/mutations/contracts.d.ts +91 -0
  60. package/dist/runtime/mutations/contracts.d.ts.map +1 -0
  61. package/dist/runtime/mutations/contracts.js +320 -0
  62. package/dist/runtime/mutations/contracts.js.map +1 -0
  63. package/dist/runtime/mutations/engine.d.ts +23 -0
  64. package/dist/runtime/mutations/engine.d.ts.map +1 -0
  65. package/dist/runtime/mutations/engine.js +255 -0
  66. package/dist/runtime/mutations/engine.js.map +1 -0
  67. package/dist/runtime/providers.d.ts +9 -0
  68. package/dist/runtime/providers.d.ts.map +1 -0
  69. package/dist/runtime/providers.js +44 -0
  70. package/dist/runtime/providers.js.map +1 -0
  71. package/dist/runtime/request-context.d.ts +11 -0
  72. package/dist/runtime/request-context.d.ts.map +1 -0
  73. package/dist/runtime/request-context.js +16 -0
  74. package/dist/runtime/request-context.js.map +1 -0
  75. package/dist/runtime/rewrite.d.ts +13 -0
  76. package/dist/runtime/rewrite.d.ts.map +1 -0
  77. package/dist/runtime/rewrite.js +253 -0
  78. package/dist/runtime/rewrite.js.map +1 -0
  79. package/dist/runtime/routes/_helpers.d.ts +17 -0
  80. package/dist/runtime/routes/_helpers.d.ts.map +1 -0
  81. package/dist/runtime/routes/_helpers.js +38 -0
  82. package/dist/runtime/routes/_helpers.js.map +1 -0
  83. package/dist/runtime/routes/admin-entry.d.ts +3 -0
  84. package/dist/runtime/routes/admin-entry.d.ts.map +1 -0
  85. package/dist/runtime/routes/admin-entry.js +170 -0
  86. package/dist/runtime/routes/admin-entry.js.map +1 -0
  87. package/dist/runtime/routes/admin.d.ts +3 -0
  88. package/dist/runtime/routes/admin.d.ts.map +1 -0
  89. package/dist/runtime/routes/admin.js +189 -0
  90. package/dist/runtime/routes/admin.js.map +1 -0
  91. package/dist/runtime/routes/auth-login.d.ts +4 -0
  92. package/dist/runtime/routes/auth-login.d.ts.map +1 -0
  93. package/dist/runtime/routes/auth-login.js +71 -0
  94. package/dist/runtime/routes/auth-login.js.map +1 -0
  95. package/dist/runtime/routes/auth-logout.d.ts +4 -0
  96. package/dist/runtime/routes/auth-logout.d.ts.map +1 -0
  97. package/dist/runtime/routes/auth-logout.js +47 -0
  98. package/dist/runtime/routes/auth-logout.js.map +1 -0
  99. package/dist/runtime/routes/auth-session.d.ts +3 -0
  100. package/dist/runtime/routes/auth-session.d.ts.map +1 -0
  101. package/dist/runtime/routes/auth-session.js +8 -0
  102. package/dist/runtime/routes/auth-session.js.map +1 -0
  103. package/dist/runtime/routes/collection-list.d.ts +3 -0
  104. package/dist/runtime/routes/collection-list.d.ts.map +1 -0
  105. package/dist/runtime/routes/collection-list.js +420 -0
  106. package/dist/runtime/routes/collection-list.js.map +1 -0
  107. package/dist/runtime/routes/collections-metadata.d.ts +3 -0
  108. package/dist/runtime/routes/collections-metadata.d.ts.map +1 -0
  109. package/dist/runtime/routes/collections-metadata.js +17 -0
  110. package/dist/runtime/routes/collections-metadata.js.map +1 -0
  111. package/dist/runtime/routes/editor-assets.d.ts +3 -0
  112. package/dist/runtime/routes/editor-assets.d.ts.map +1 -0
  113. package/dist/runtime/routes/editor-assets.js +73 -0
  114. package/dist/runtime/routes/editor-assets.js.map +1 -0
  115. package/dist/runtime/routes/entries.d.ts +3 -0
  116. package/dist/runtime/routes/entries.d.ts.map +1 -0
  117. package/dist/runtime/routes/entries.js +96 -0
  118. package/dist/runtime/routes/entries.js.map +1 -0
  119. package/dist/runtime/routes/history.d.ts +4 -0
  120. package/dist/runtime/routes/history.d.ts.map +1 -0
  121. package/dist/runtime/routes/history.js +54 -0
  122. package/dist/runtime/routes/history.js.map +1 -0
  123. package/dist/runtime/routes/mutate.d.ts +4 -0
  124. package/dist/runtime/routes/mutate.d.ts.map +1 -0
  125. package/dist/runtime/routes/mutate.js +38 -0
  126. package/dist/runtime/routes/mutate.js.map +1 -0
  127. package/dist/runtime/routes/schema.d.ts +3 -0
  128. package/dist/runtime/routes/schema.d.ts.map +1 -0
  129. package/dist/runtime/routes/schema.js +58 -0
  130. package/dist/runtime/routes/schema.js.map +1 -0
  131. package/dist/runtime/routes/studio-home.d.ts +3 -0
  132. package/dist/runtime/routes/studio-home.d.ts.map +1 -0
  133. package/dist/runtime/routes/studio-home.js +99 -0
  134. package/dist/runtime/routes/studio-home.js.map +1 -0
  135. package/dist/runtime/routes/theme-stylesheet.d.ts +3 -0
  136. package/dist/runtime/routes/theme-stylesheet.d.ts.map +1 -0
  137. package/dist/runtime/routes/theme-stylesheet.js +32 -0
  138. package/dist/runtime/routes/theme-stylesheet.js.map +1 -0
  139. package/dist/runtime/routes/upload.d.ts +3 -0
  140. package/dist/runtime/routes/upload.d.ts.map +1 -0
  141. package/dist/runtime/routes/upload.js +33 -0
  142. package/dist/runtime/routes/upload.js.map +1 -0
  143. package/dist/runtime/sanitize-html.d.ts +8 -0
  144. package/dist/runtime/sanitize-html.d.ts.map +1 -0
  145. package/dist/runtime/sanitize-html.js +86 -0
  146. package/dist/runtime/sanitize-html.js.map +1 -0
  147. package/dist/runtime/schema/template.d.ts +15 -0
  148. package/dist/runtime/schema/template.d.ts.map +1 -0
  149. package/dist/runtime/schema/template.js +38 -0
  150. package/dist/runtime/schema/template.js.map +1 -0
  151. package/dist/runtime/schema-registry.d.ts +17 -0
  152. package/dist/runtime/schema-registry.d.ts.map +1 -0
  153. package/dist/runtime/schema-registry.js +16 -0
  154. package/dist/runtime/schema-registry.js.map +1 -0
  155. package/dist/runtime/storage/filesystem-adapter.d.ts +36 -0
  156. package/dist/runtime/storage/filesystem-adapter.d.ts.map +1 -0
  157. package/dist/runtime/storage/filesystem-adapter.js +291 -0
  158. package/dist/runtime/storage/filesystem-adapter.js.map +1 -0
  159. package/dist/runtime/storage/filesystem-upload-handler.d.ts +11 -0
  160. package/dist/runtime/storage/filesystem-upload-handler.d.ts.map +1 -0
  161. package/dist/runtime/storage/filesystem-upload-handler.js +20 -0
  162. package/dist/runtime/storage/filesystem-upload-handler.js.map +1 -0
  163. package/dist/runtime/storage/image-validation.d.ts +28 -0
  164. package/dist/runtime/storage/image-validation.d.ts.map +1 -0
  165. package/dist/runtime/storage/image-validation.js +94 -0
  166. package/dist/runtime/storage/image-validation.js.map +1 -0
  167. package/dist/runtime/storage/in-memory-adapter.d.ts +29 -0
  168. package/dist/runtime/storage/in-memory-adapter.d.ts.map +1 -0
  169. package/dist/runtime/storage/in-memory-adapter.js +110 -0
  170. package/dist/runtime/storage/in-memory-adapter.js.map +1 -0
  171. package/dist/runtime/storage/quota-upload-handler.d.ts +27 -0
  172. package/dist/runtime/storage/quota-upload-handler.d.ts.map +1 -0
  173. package/dist/runtime/storage/quota-upload-handler.js +34 -0
  174. package/dist/runtime/storage/quota-upload-handler.js.map +1 -0
  175. package/dist/runtime/storage/session-overlay-adapter.d.ts +33 -0
  176. package/dist/runtime/storage/session-overlay-adapter.d.ts.map +1 -0
  177. package/dist/runtime/storage/session-overlay-adapter.js +121 -0
  178. package/dist/runtime/storage/session-overlay-adapter.js.map +1 -0
  179. package/dist/runtime/themes/preset-tokens.d.ts +17 -0
  180. package/dist/runtime/themes/preset-tokens.d.ts.map +1 -0
  181. package/dist/runtime/themes/preset-tokens.js +47 -0
  182. package/dist/runtime/themes/preset-tokens.js.map +1 -0
  183. package/dist/runtime/utils.d.ts +6 -0
  184. package/dist/runtime/utils.d.ts.map +1 -0
  185. package/dist/runtime/utils.js +29 -0
  186. package/dist/runtime/utils.js.map +1 -0
  187. package/dist/runtime/views/studio-layout.d.ts +25 -0
  188. package/dist/runtime/views/studio-layout.d.ts.map +1 -0
  189. package/dist/runtime/views/studio-layout.js +108 -0
  190. package/dist/runtime/views/studio-layout.js.map +1 -0
  191. package/dist/schema-utils.d.ts +16 -0
  192. package/dist/schema-utils.d.ts.map +1 -0
  193. package/dist/schema-utils.js +62 -0
  194. package/dist/schema-utils.js.map +1 -0
  195. package/dist/types.d.ts +76 -0
  196. package/dist/types.d.ts.map +1 -0
  197. package/dist/types.js +2 -0
  198. package/dist/types.js.map +1 -0
  199. package/editor-runtime.d.ts +6 -0
  200. package/editor-runtime.js +2 -0
  201. package/package.json +88 -0
  202. package/static/cms/admin-entry.css +207 -0
  203. package/static/cms/admin-entry.js +1083 -0
  204. package/static/cms/editor/config.js +118 -0
  205. package/static/cms/editor/content-map.css +141 -0
  206. package/static/cms/editor/content-map.js +172 -0
  207. package/static/cms/editor/guards.js +72 -0
  208. package/static/cms/editor/helpers.js +51 -0
  209. package/static/cms/editor/image-edit.js +151 -0
  210. package/static/cms/editor/image-utils.js +84 -0
  211. package/static/cms/editor/image.css +133 -0
  212. package/static/cms/editor/link-follow.js +144 -0
  213. package/static/cms/editor/link-ui.css +199 -0
  214. package/static/cms/editor/linkify.js +55 -0
  215. package/static/cms/editor/panel.css +91 -0
  216. package/static/cms/editor/panel.js +70 -0
  217. package/static/cms/editor/rich-toolbar.css +67 -0
  218. package/static/cms/editor/rich-toolbar.js +200 -0
  219. package/static/cms/editor/sanitize.js +89 -0
  220. package/static/cms/editor/save-queue.js +166 -0
  221. package/static/cms/editor/section-controls/activation.js +10 -0
  222. package/static/cms/editor/section-controls/api.js +87 -0
  223. package/static/cms/editor/section-controls/constants.js +24 -0
  224. package/static/cms/editor/section-controls/index.js +622 -0
  225. package/static/cms/editor/section-controls/model.js +76 -0
  226. package/static/cms/editor/section-controls/mutations.js +112 -0
  227. package/static/cms/editor/section-controls/page-context.js +34 -0
  228. package/static/cms/editor/section-controls/pickers.js +196 -0
  229. package/static/cms/editor/section-controls/reorder.js +92 -0
  230. package/static/cms/editor/section-controls/selection.js +54 -0
  231. package/static/cms/editor/section-controls/spacing-drag.js +83 -0
  232. package/static/cms/editor/section-controls/ui-elements.js +54 -0
  233. package/static/cms/editor/section-controls/utils.js +35 -0
  234. package/static/cms/editor/section-controls.css +349 -0
  235. package/static/cms/editor/section-controls.js +1 -0
  236. package/static/cms/editor/security.js +48 -0
  237. package/static/cms/editor/sync.js +72 -0
  238. package/static/cms/editor/text-edit.js +151 -0
  239. package/static/cms/editor/text-link-interactions.js +254 -0
  240. package/static/cms/editor/toast.css +50 -0
  241. package/static/cms/editor/toast.js +29 -0
  242. package/static/cms/editor/tokens-and-text.css +93 -0
  243. package/static/cms/editor/toolbar.css +249 -0
  244. package/static/cms/editor/toolbar.js +117 -0
  245. package/static/cms/editor.css +12 -0
  246. package/static/cms/editor.js +116 -0
  247. package/static/cms/studio-login.css +300 -0
  248. package/static/cms/studio-tokens.css +85 -0
  249. package/static/cms/studio.css +437 -0
@@ -0,0 +1,118 @@
1
+ /** Runtime config injected by the integration bootstrap script. */
2
+ const defaults = {
3
+ mode: 'embedded',
4
+ mountPath: '/admin',
5
+ apiBasePath: '/api/cms',
6
+ cloud: null,
7
+ };
8
+
9
+ const cfg = (typeof window !== 'undefined' && window.__CARET__) || defaults;
10
+
11
+ function normalizePath(value, fallback) {
12
+ if (!value || typeof value !== 'string') return fallback;
13
+ const trimmed = value.trim();
14
+ if (!trimmed) return fallback;
15
+ const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
16
+ return withLeadingSlash.length > 1 && withLeadingSlash.endsWith('/')
17
+ ? withLeadingSlash.slice(0, -1)
18
+ : withLeadingSlash;
19
+ }
20
+
21
+ function normalizeEndpoint(value) {
22
+ if (!value || typeof value !== 'string') return '';
23
+ return value.trim().replace(/\/$/, '');
24
+ }
25
+
26
+ function getCloudConfig() {
27
+ if (!cfg || cfg.mode !== 'cloud' || !cfg.cloud || typeof cfg.cloud !== 'object') {
28
+ return null;
29
+ }
30
+
31
+ const endpoint = normalizeEndpoint(cfg.cloud.endpoint);
32
+ const projectId =
33
+ typeof cfg.cloud.projectId === 'string' ? cfg.cloud.projectId.trim() : '';
34
+ if (!endpoint || !projectId) return null;
35
+
36
+ return {
37
+ endpoint,
38
+ projectId,
39
+ contentPath: normalizePath(cfg.cloud.contentPath, '/content'),
40
+ publicToken:
41
+ typeof cfg.cloud.publicToken === 'string' && cfg.cloud.publicToken.trim()
42
+ ? cfg.cloud.publicToken.trim()
43
+ : null,
44
+ environment:
45
+ typeof cfg.cloud.environment === 'string' && cfg.cloud.environment.trim()
46
+ ? cfg.cloud.environment.trim()
47
+ : null,
48
+ };
49
+ }
50
+
51
+ function getStorageKey(cloud) {
52
+ return `caretcms:cloud-session:${cloud.endpoint}:${cloud.projectId}`;
53
+ }
54
+
55
+ export const CMS_CONFIG = cfg;
56
+ export const CMS_MODE = cfg.mode === 'cloud' || cfg.mode === 'hybrid' ? cfg.mode : 'embedded';
57
+ export const MOUNT_PATH = normalizePath(cfg.mountPath, defaults.mountPath);
58
+ export const API_BASE = normalizePath(cfg.apiBasePath, defaults.apiBasePath);
59
+ export const CLOUD = getCloudConfig();
60
+ export const STUDIO_PATH = CLOUD ? null : `${MOUNT_PATH}/cms`;
61
+
62
+ export function isCloudMode() {
63
+ return CMS_MODE === 'cloud' && CLOUD !== null;
64
+ }
65
+
66
+ export function getCloudSessionToken() {
67
+ if (!isCloudMode() || typeof window === 'undefined') return null;
68
+ try {
69
+ const value = window.localStorage.getItem(getStorageKey(CLOUD));
70
+ return value && value.trim() ? value.trim() : null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ export function clearCloudSessionToken() {
77
+ if (!isCloudMode() || typeof window === 'undefined') return;
78
+ try {
79
+ window.localStorage.removeItem(getStorageKey(CLOUD));
80
+ } catch {
81
+ // Ignore localStorage failures.
82
+ }
83
+ }
84
+
85
+ export function buildCmsUrl(path, search) {
86
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
87
+ let url;
88
+
89
+ if (isCloudMode()) {
90
+ url = new URL(`${CLOUD.contentPath}${normalizedPath}`, `${CLOUD.endpoint}/`);
91
+ url.searchParams.set('projectId', CLOUD.projectId);
92
+ if (CLOUD.environment) {
93
+ url.searchParams.set('environment', CLOUD.environment);
94
+ }
95
+ } else {
96
+ url = new URL(`${API_BASE}${normalizedPath}`, window.location.origin);
97
+ }
98
+
99
+ if (search) {
100
+ const params = search instanceof URLSearchParams ? search : new URLSearchParams(search);
101
+ for (const [key, value] of params.entries()) {
102
+ if (value) url.searchParams.set(key, value);
103
+ }
104
+ }
105
+
106
+ return url.toString();
107
+ }
108
+
109
+ export function getEditorLoginUrl(redirectTo = window.location.href) {
110
+ if (isCloudMode()) {
111
+ const url = new URL(`${CLOUD.contentPath}/admin`, `${CLOUD.endpoint}/`);
112
+ url.searchParams.set('projectId', CLOUD.projectId);
113
+ url.searchParams.set('redirect', redirectTo);
114
+ return url.toString();
115
+ }
116
+
117
+ return `${MOUNT_PATH}?redirect=${encodeURIComponent(redirectTo)}`;
118
+ }
@@ -0,0 +1,141 @@
1
+ .cms-content-map {
2
+ position: fixed;
3
+ top: 48px;
4
+ right: -340px;
5
+ width: 320px;
6
+ max-height: calc(100vh - 60px);
7
+ background: #1a1a2e;
8
+ border-left: 1px solid rgba(255, 255, 255, 0.1);
9
+ border-radius: 8px 0 0 8px;
10
+ box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3);
11
+ z-index: 100001;
12
+ transition: right 0.25s ease;
13
+ display: flex;
14
+ flex-direction: column;
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16
+ font-size: 12px;
17
+ color: #e0e0e0;
18
+ }
19
+
20
+ .cms-content-map-open {
21
+ right: 0;
22
+ }
23
+
24
+ .cms-content-map-header {
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: space-between;
28
+ padding: 10px 14px;
29
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
30
+ }
31
+
32
+ .cms-content-map-title {
33
+ font-weight: 600;
34
+ font-size: 13px;
35
+ }
36
+
37
+ .cms-content-map-close {
38
+ background: none;
39
+ border: none;
40
+ color: #888;
41
+ font-size: 18px;
42
+ cursor: pointer;
43
+ padding: 0 4px;
44
+ line-height: 1;
45
+ }
46
+
47
+ .cms-content-map-close:hover {
48
+ color: #fff;
49
+ }
50
+
51
+ .cms-content-map-body {
52
+ overflow-y: auto;
53
+ flex: 1;
54
+ padding: 8px 0;
55
+ }
56
+
57
+ .cms-content-map-loading,
58
+ .cms-content-map-empty {
59
+ padding: 20px 14px;
60
+ text-align: center;
61
+ color: #888;
62
+ }
63
+
64
+ .cms-content-map-group {
65
+ margin-bottom: 4px;
66
+ }
67
+
68
+ .cms-content-map-group-title {
69
+ padding: 6px 14px 4px;
70
+ font-size: 10px;
71
+ font-weight: 700;
72
+ text-transform: uppercase;
73
+ letter-spacing: 0.05em;
74
+ color: #6c7a89;
75
+ }
76
+
77
+ .cms-content-map-item {
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: space-between;
81
+ padding: 5px 14px;
82
+ cursor: pointer;
83
+ transition: background 0.15s;
84
+ }
85
+
86
+ .cms-content-map-item:hover {
87
+ background: rgba(255, 255, 255, 0.05);
88
+ }
89
+
90
+ .cms-content-map-field {
91
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
92
+ font-size: 11px;
93
+ color: #c8d6e5;
94
+ }
95
+
96
+ .cms-content-map-tags {
97
+ display: flex;
98
+ gap: 4px;
99
+ align-items: center;
100
+ }
101
+
102
+ .cms-map-status {
103
+ font-size: 10px;
104
+ padding: 1px 6px;
105
+ border-radius: 3px;
106
+ font-weight: 500;
107
+ }
108
+
109
+ .cms-map-default {
110
+ background: rgba(255, 255, 255, 0.08);
111
+ color: #888;
112
+ }
113
+
114
+ .cms-map-saved {
115
+ background: rgba(46, 213, 115, 0.15);
116
+ color: #2ed573;
117
+ }
118
+
119
+ .cms-map-scoped {
120
+ font-size: 10px;
121
+ padding: 1px 6px;
122
+ border-radius: 3px;
123
+ background: rgba(116, 185, 255, 0.15);
124
+ color: #74b9ff;
125
+ font-weight: 500;
126
+ }
127
+
128
+ .cms-map-type {
129
+ font-size: 10px;
130
+ padding: 1px 6px;
131
+ border-radius: 3px;
132
+ background: rgba(255, 165, 2, 0.15);
133
+ color: #ffa502;
134
+ font-weight: 500;
135
+ }
136
+
137
+ .cms-map-highlight {
138
+ outline: 2px solid #74b9ff !important;
139
+ outline-offset: 2px;
140
+ transition: outline-color 0.3s;
141
+ }
@@ -0,0 +1,172 @@
1
+ import { buildCmsUrl } from './config.js';
2
+ import { readHeaders } from './security.js';
3
+ import { parseCaretAttr, resolveBinding, getResolvedKey } from './helpers.js';
4
+
5
+ /**
6
+ * Content Map dev panel.
7
+ * Lists all data-caret bindings on the current page with override status.
8
+ */
9
+
10
+ let panel = null;
11
+ let isOpen = false;
12
+
13
+ function createPanel() {
14
+ const el = document.createElement('div');
15
+ el.className = 'cms-content-map';
16
+ el.innerHTML = `
17
+ <div class="cms-content-map-header">
18
+ <span class="cms-content-map-title">Content Map</span>
19
+ <button type="button" class="cms-content-map-close">&times;</button>
20
+ </div>
21
+ <div class="cms-content-map-body"></div>
22
+ `;
23
+ document.body.appendChild(el);
24
+
25
+ el.querySelector('.cms-content-map-close')?.addEventListener('click', () => {
26
+ toggle();
27
+ });
28
+
29
+ return el;
30
+ }
31
+
32
+ async function loadOverrides(entryKeys) {
33
+ const overrides = new Map();
34
+ for (const key of entryKeys) {
35
+ const [collection, id] = key.split('::');
36
+ try {
37
+ const res = await fetch(buildCmsUrl('/entries', { collection, id }), {
38
+ headers: readHeaders(),
39
+ });
40
+ if (res.ok) {
41
+ const data = await res.json();
42
+ if (data.entries && data.entries.length > 0) {
43
+ overrides.set(key, data.entries[0].data);
44
+ }
45
+ }
46
+ } catch {
47
+ // Skip failed loads
48
+ }
49
+ }
50
+ return overrides;
51
+ }
52
+
53
+ function getNestedValue(data, path) {
54
+ const keys = path.split('.');
55
+ let current = data;
56
+ for (const key of keys) {
57
+ if (current === null || current === undefined) return undefined;
58
+ if (typeof current !== 'object') return undefined;
59
+ current = current[key];
60
+ }
61
+ return current;
62
+ }
63
+
64
+ async function refresh() {
65
+ if (!panel) return;
66
+ const body = panel.querySelector('.cms-content-map-body');
67
+ if (!body) return;
68
+
69
+ const elements = document.querySelectorAll('[data-caret]');
70
+ const bindings = [];
71
+ const entryKeys = new Set();
72
+
73
+ elements.forEach((el) => {
74
+ const attr = el.getAttribute('data-caret') || '';
75
+ const parsed = parseCaretAttr(attr);
76
+ const resolved = resolveBinding(el, parsed);
77
+ if (!resolved) return;
78
+
79
+ const key = `${resolved.collection}::${resolved.id}`;
80
+ entryKeys.add(key);
81
+
82
+ const isScoped = parsed && !parsed.collection;
83
+ bindings.push({
84
+ el,
85
+ resolved,
86
+ fullKey: `${resolved.collection}::${resolved.id}::${resolved.field}`,
87
+ isImg: el instanceof HTMLImageElement,
88
+ isScoped,
89
+ });
90
+ });
91
+
92
+ body.innerHTML = `<div class="cms-content-map-loading">Loading...</div>`;
93
+
94
+ const overrides = await loadOverrides(entryKeys);
95
+
96
+ if (bindings.length === 0) {
97
+ body.innerHTML = `<div class="cms-content-map-empty">No data-caret bindings found on this page.</div>`;
98
+ return;
99
+ }
100
+
101
+ // Group by collection::id
102
+ const groups = new Map();
103
+ for (const b of bindings) {
104
+ const key = `${b.resolved.collection}::${b.resolved.id}`;
105
+ if (!groups.has(key)) groups.set(key, []);
106
+ groups.get(key).push(b);
107
+ }
108
+
109
+ let html = '';
110
+ for (const [entryKey, fields] of groups) {
111
+ const entryData = overrides.get(entryKey);
112
+ html += `<div class="cms-content-map-group">`;
113
+ html += `<div class="cms-content-map-group-title">${escapeHtml(entryKey)}</div>`;
114
+
115
+ for (const f of fields) {
116
+ const hasOverride = entryData
117
+ ? getNestedValue(entryData, f.resolved.field) !== undefined
118
+ : false;
119
+
120
+ const statusClass = hasOverride ? 'cms-map-saved' : 'cms-map-default';
121
+ const statusLabel = hasOverride ? 'saved' : 'default';
122
+ const scopeTag = f.isScoped ? '<span class="cms-map-scoped">scoped</span>' : '';
123
+ const typeTag = f.isImg ? '<span class="cms-map-type">img</span>' : '';
124
+
125
+ html += `
126
+ <div class="cms-content-map-item" data-caret-map-key="${escapeAttr(f.fullKey)}">
127
+ <span class="cms-content-map-field">${escapeHtml(f.resolved.field)}</span>
128
+ <span class="cms-content-map-tags">
129
+ ${typeTag}${scopeTag}
130
+ <span class="cms-map-status ${statusClass}">${statusLabel}</span>
131
+ </span>
132
+ </div>
133
+ `;
134
+ }
135
+
136
+ html += `</div>`;
137
+ }
138
+
139
+ body.innerHTML = html;
140
+
141
+ // Click to scroll to element
142
+ body.querySelectorAll('.cms-content-map-item').forEach((item) => {
143
+ item.addEventListener('click', () => {
144
+ const key = item.getAttribute('data-caret-map-key');
145
+ const target = bindings.find((b) => b.fullKey === key);
146
+ if (target?.el) {
147
+ target.el.scrollIntoView({ behavior: 'smooth', block: 'center' });
148
+ target.el.classList.add('cms-map-highlight');
149
+ setTimeout(() => target.el.classList.remove('cms-map-highlight'), 2000);
150
+ }
151
+ });
152
+ });
153
+ }
154
+
155
+ function toggle() {
156
+ if (!panel) panel = createPanel();
157
+ isOpen = !isOpen;
158
+ panel.classList.toggle('cms-content-map-open', isOpen);
159
+ if (isOpen) refresh();
160
+ }
161
+
162
+ function escapeHtml(str) {
163
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
164
+ }
165
+
166
+ function escapeAttr(str) {
167
+ return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
168
+ }
169
+
170
+ export function mountContentMap({ mapButton }) {
171
+ mapButton?.addEventListener('click', toggle);
172
+ }
@@ -0,0 +1,72 @@
1
+ export function mountEditorGuards(state) {
2
+ document.body.classList.add('cms-edit-mode');
3
+
4
+ window.addEventListener('beforeunload', (e) => {
5
+ if (state.dirtyEl) {
6
+ e.preventDefault();
7
+ e.returnValue = '';
8
+ }
9
+ });
10
+
11
+ // Smart navigation: only block clicks on CMS-editable elements.
12
+ document.addEventListener(
13
+ 'click',
14
+ (e) => {
15
+ const target = e.target;
16
+ if (!(target instanceof Element)) return;
17
+
18
+ // Allow clicks inside the CMS toolbar
19
+ if (target.closest('.cms-toolbar')) return;
20
+
21
+ // Allow clicks on image overlay buttons
22
+ if (target.closest('.cms-img-overlay')) return;
23
+
24
+ // Allow clicks inside the link popover
25
+ if (target.closest('.cms-link-popover')) return;
26
+
27
+ // Allow clicks on the floating "Open page" affordance — it's a real
28
+ // anchor that lets visitors follow links instead of editing them.
29
+ if (target.closest('.cms-link-follow')) return;
30
+
31
+ const editable = target.closest('[data-caret]');
32
+ const link = target.closest('a');
33
+ if (link) {
34
+ // Link contains or is a CMS editable — block nav, focus editable
35
+ const cmsInLink = editable || link.querySelector('[data-caret]');
36
+ if (cmsInLink instanceof HTMLElement) {
37
+ e.preventDefault();
38
+ e.stopPropagation();
39
+ cmsInLink.focus();
40
+ return;
41
+ }
42
+ // Non-CMS link — let it navigate normally
43
+ return;
44
+ }
45
+
46
+ // For buttons: only intercept if they contain CMS content
47
+ const btn = target.closest('button');
48
+ if (btn && !btn.closest('.cms-toolbar') && !btn.closest('.cms-img-wrapper')) {
49
+ const cmsInBtn = editable || btn.querySelector('[data-caret]');
50
+ if (cmsInBtn instanceof HTMLElement) {
51
+ e.preventDefault();
52
+ e.stopPropagation();
53
+ cmsInBtn.focus();
54
+ }
55
+ }
56
+ },
57
+ true,
58
+ );
59
+
60
+ // Block form submissions (except CMS forms)
61
+ document.addEventListener(
62
+ 'submit',
63
+ (e) => {
64
+ const target = e.target;
65
+ if (!(target instanceof Element)) return;
66
+ if (!target.closest('.cms-toolbar')) {
67
+ e.preventDefault();
68
+ }
69
+ },
70
+ true,
71
+ );
72
+ }
@@ -0,0 +1,51 @@
1
+ export function parseCaretAttr(attr) {
2
+ const parts = attr.split('::');
3
+ if (parts.length === 3) return { collection: parts[0], id: parts[1], field: parts[2] };
4
+ // Field-only (scoped) — return just the field, caller resolves scope
5
+ if (parts.length === 1 && !attr.includes('::')) return { collection: null, id: null, field: attr };
6
+ return null;
7
+ }
8
+
9
+ /**
10
+ * Resolve a parsed binding by walking up the DOM to find data-caret-scope.
11
+ * If the binding already has collection+id, returns as-is.
12
+ * If field-only, walks up to nearest [data-caret-scope] ancestor.
13
+ * Returns null if scope cannot be resolved.
14
+ */
15
+ export function resolveBinding(el, parsed) {
16
+ if (!parsed) return null;
17
+ if (parsed.collection && parsed.id) return parsed;
18
+
19
+ // Walk up to find nearest data-caret-scope
20
+ let node = el.parentElement;
21
+ while (node) {
22
+ const scope = node.getAttribute('data-caret-scope');
23
+ if (scope) {
24
+ const parts = scope.split('::');
25
+ if (parts.length === 2) {
26
+ return { collection: parts[0], id: parts[1], field: parsed.field };
27
+ }
28
+ }
29
+ node = node.parentElement;
30
+ }
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * Get the full data-caret key for an element, resolving scope if needed.
36
+ * Returns "collection::id::field" or null if unresolvable.
37
+ */
38
+ export function getResolvedKey(el) {
39
+ const attr = el.getAttribute('data-caret');
40
+ if (!attr) return null;
41
+ const parsed = parseCaretAttr(attr);
42
+ const resolved = resolveBinding(el, parsed);
43
+ if (!resolved) return null;
44
+ return `${resolved.collection}::${resolved.id}::${resolved.field}`;
45
+ }
46
+
47
+ export function flash(el, success) {
48
+ const cls = success ? 'cms-saved' : 'cms-error';
49
+ el.classList.add(cls);
50
+ setTimeout(() => el.classList.remove(cls), 2000);
51
+ }
@@ -0,0 +1,151 @@
1
+ import { buildCmsUrl } from './config.js';
2
+ import { uploadHeaders } from './security.js';
3
+ import { resolveBinding } from './helpers.js';
4
+
5
+ export function mountImageEditors({
6
+ parseCaretAttr,
7
+ flash,
8
+ compressImage,
9
+ updateEmblaCarousel,
10
+ saveField,
11
+ setStatus,
12
+ showToast,
13
+ onUnauthorized,
14
+ }) {
15
+ const syncBoundImages = (marker, src) => {
16
+ document.querySelectorAll('[data-caret]').forEach((node) => {
17
+ if (!(node instanceof HTMLImageElement)) return;
18
+ if (node.getAttribute('data-caret') !== marker) return;
19
+ node.src = src;
20
+ updateEmblaCarousel(node, src);
21
+ });
22
+ };
23
+
24
+ document.querySelectorAll('[data-caret]').forEach((el) => {
25
+ if (!(el instanceof HTMLImageElement)) return;
26
+ const img = el;
27
+ if (img.dataset.caretImgMounted === 'true') return;
28
+ img.dataset.caretImgMounted = 'true';
29
+
30
+ const existingWrapper = img.parentElement;
31
+ if (existingWrapper?.classList.contains('cms-img-wrapper')) {
32
+ existingWrapper.parentNode?.insertBefore(img, existingWrapper);
33
+ existingWrapper.remove();
34
+ }
35
+
36
+ const wrapper = document.createElement('div');
37
+ wrapper.className = 'cms-img-wrapper';
38
+ wrapper.style.position = 'relative';
39
+ wrapper.style.display = 'block';
40
+ wrapper.style.width = '100%';
41
+ wrapper.style.height = '100%';
42
+
43
+ img.parentNode?.insertBefore(wrapper, img);
44
+ wrapper.appendChild(img);
45
+
46
+ // Corner badge — persistent edit indicator
47
+ const badge = document.createElement('div');
48
+ badge.className = 'cms-img-badge';
49
+ badge.innerHTML =
50
+ '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
51
+ wrapper.appendChild(badge);
52
+
53
+ // Full overlay with idle + uploading states
54
+ const overlay = document.createElement('button');
55
+ overlay.className = 'cms-img-overlay';
56
+ overlay.type = 'button';
57
+ overlay.innerHTML = `
58
+ <span class="cms-img-overlay-content cms-img-state-idle">
59
+ <svg class="cms-img-overlay-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
60
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z"/>
61
+ <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM18.75 10.5h.008v.008h-.008V10.5z"/>
62
+ </svg>
63
+ <span class="cms-img-overlay-text">Replace Image</span>
64
+ </span>
65
+ <span class="cms-img-overlay-content cms-img-state-uploading">
66
+ <span class="cms-img-spinner"></span>
67
+ <span class="cms-img-overlay-text">Uploading...</span>
68
+ </span>
69
+ `;
70
+ wrapper.appendChild(overlay);
71
+
72
+ const input = document.createElement('input');
73
+ input.type = 'file';
74
+ input.accept = 'image/jpeg,image/png,image/webp,image/avif';
75
+ input.style.display = 'none';
76
+ wrapper.appendChild(input);
77
+
78
+ overlay.addEventListener('click', (e) => {
79
+ e.stopPropagation(); // prevent slide click (lightbox)
80
+ input.click();
81
+ });
82
+
83
+ input.addEventListener('change', async () => {
84
+ const file = input.files?.[0];
85
+ if (!file) return;
86
+
87
+ const marker = img.getAttribute('data-caret') || '';
88
+ const parsed = parseCaretAttr(marker);
89
+ const resolved = resolveBinding(img, parsed);
90
+ if (!resolved) return;
91
+
92
+ // Optimistic preview
93
+ const previewUrl = URL.createObjectURL(file);
94
+ const originalSrc = img.src;
95
+ img.src = previewUrl;
96
+
97
+ overlay.classList.add('uploading');
98
+ badge.classList.add('cms-img-badge-hidden');
99
+ setStatus('saving', 'Uploading...');
100
+
101
+ try {
102
+ const compressed = await compressImage(file);
103
+ const formData = new FormData();
104
+ formData.append('file', compressed);
105
+ const headers = await uploadHeaders();
106
+
107
+ const uploadRes = await fetch(buildCmsUrl('/upload'), {
108
+ method: 'POST',
109
+ headers,
110
+ body: formData,
111
+ });
112
+ if (uploadRes.status === 401) {
113
+ onUnauthorized();
114
+ return;
115
+ }
116
+ if (!uploadRes.ok) throw new Error('Upload failed');
117
+ const { url } = await uploadRes.json();
118
+
119
+ const result = await saveField(resolved.collection, resolved.id, resolved.field, url);
120
+ if (result.ok) {
121
+ syncBoundImages(marker, url);
122
+ showToast('Image updated', 'success');
123
+ } else {
124
+ const rollbackSrc =
125
+ result.reason === 'conflict' && typeof result.latestValue === 'string'
126
+ ? result.latestValue
127
+ : originalSrc;
128
+ syncBoundImages(marker, rollbackSrc);
129
+ if (result.reason === 'conflict') {
130
+ showToast('Image changed elsewhere. Loaded latest image.', 'error');
131
+ } else if (result.reason !== 'unauthorized') {
132
+ showToast('Failed to save image. Changes reverted.', 'error');
133
+ }
134
+ }
135
+
136
+ flash(img, result.ok);
137
+ } catch {
138
+ img.src = originalSrc;
139
+ flash(img, false);
140
+ setStatus('error', 'Upload failed');
141
+ showToast('Upload failed', 'error');
142
+ } finally {
143
+ URL.revokeObjectURL(previewUrl);
144
+ overlay.classList.remove('uploading');
145
+ badge.classList.remove('cms-img-badge-hidden');
146
+ }
147
+
148
+ input.value = '';
149
+ });
150
+ });
151
+ }