@407dev/cms-astro 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 (212) hide show
  1. package/README.md +69 -0
  2. package/dist/chunk-2TKIQIOE.js +118 -0
  3. package/dist/chunk-2TKIQIOE.js.map +1 -0
  4. package/dist/chunk-53OQ5RXJ.js +912 -0
  5. package/dist/chunk-53OQ5RXJ.js.map +1 -0
  6. package/dist/chunk-56OMNQLH.js +1013 -0
  7. package/dist/chunk-56OMNQLH.js.map +1 -0
  8. package/dist/chunk-5AOOMQ7K.js +1031 -0
  9. package/dist/chunk-5AOOMQ7K.js.map +1 -0
  10. package/dist/chunk-5XRUCFDC.js +743 -0
  11. package/dist/chunk-5XRUCFDC.js.map +1 -0
  12. package/dist/chunk-62ZHCOLZ.js +25 -0
  13. package/dist/chunk-62ZHCOLZ.js.map +1 -0
  14. package/dist/chunk-6AZLRSYA.js +126 -0
  15. package/dist/chunk-6AZLRSYA.js.map +1 -0
  16. package/dist/chunk-6L3ZTCFB.js +14 -0
  17. package/dist/chunk-6L3ZTCFB.js.map +1 -0
  18. package/dist/chunk-6O7ZNNDT.js +697 -0
  19. package/dist/chunk-6O7ZNNDT.js.map +1 -0
  20. package/dist/chunk-6PQIXTMK.js +697 -0
  21. package/dist/chunk-6PQIXTMK.js.map +1 -0
  22. package/dist/chunk-7AW6M7QW.js +1013 -0
  23. package/dist/chunk-7AW6M7QW.js.map +1 -0
  24. package/dist/chunk-7E3XJLKX.js +73 -0
  25. package/dist/chunk-7E3XJLKX.js.map +1 -0
  26. package/dist/chunk-7LM54OTA.js +1031 -0
  27. package/dist/chunk-7LM54OTA.js.map +1 -0
  28. package/dist/chunk-7LMUFCWX.js +126 -0
  29. package/dist/chunk-7LMUFCWX.js.map +1 -0
  30. package/dist/chunk-A3OGL4OX.js +706 -0
  31. package/dist/chunk-A3OGL4OX.js.map +1 -0
  32. package/dist/chunk-AO4UAWOV.js +1030 -0
  33. package/dist/chunk-AO4UAWOV.js.map +1 -0
  34. package/dist/chunk-B4YYLS7Y.js +14 -0
  35. package/dist/chunk-B4YYLS7Y.js.map +1 -0
  36. package/dist/chunk-BDFVGXM2.js +697 -0
  37. package/dist/chunk-BDFVGXM2.js.map +1 -0
  38. package/dist/chunk-CXM7NZVX.js +1031 -0
  39. package/dist/chunk-CXM7NZVX.js.map +1 -0
  40. package/dist/chunk-DMLOBKYE.js +96 -0
  41. package/dist/chunk-DMLOBKYE.js.map +1 -0
  42. package/dist/chunk-DXJSYGS6.js +743 -0
  43. package/dist/chunk-DXJSYGS6.js.map +1 -0
  44. package/dist/chunk-EFUSAQZM.js +72 -0
  45. package/dist/chunk-EFUSAQZM.js.map +1 -0
  46. package/dist/chunk-F7KDGPBN.js +1013 -0
  47. package/dist/chunk-F7KDGPBN.js.map +1 -0
  48. package/dist/chunk-FBSQT45X.js +126 -0
  49. package/dist/chunk-FBSQT45X.js.map +1 -0
  50. package/dist/chunk-FEPDOMVH.js +697 -0
  51. package/dist/chunk-FEPDOMVH.js.map +1 -0
  52. package/dist/chunk-FQDOL2QW.js +25 -0
  53. package/dist/chunk-FQDOL2QW.js.map +1 -0
  54. package/dist/chunk-GB3LGQJM.js +109 -0
  55. package/dist/chunk-GB3LGQJM.js.map +1 -0
  56. package/dist/chunk-HCNP6R3P.js +736 -0
  57. package/dist/chunk-HCNP6R3P.js.map +1 -0
  58. package/dist/chunk-J6WEJDRI.js +743 -0
  59. package/dist/chunk-J6WEJDRI.js.map +1 -0
  60. package/dist/chunk-JHEAPNJ2.js +117 -0
  61. package/dist/chunk-JHEAPNJ2.js.map +1 -0
  62. package/dist/chunk-JOJDJ6GW.js +25 -0
  63. package/dist/chunk-JOJDJ6GW.js.map +1 -0
  64. package/dist/chunk-KTDV6WYB.js +202 -0
  65. package/dist/chunk-KTDV6WYB.js.map +1 -0
  66. package/dist/chunk-KZWJBKPC.js +734 -0
  67. package/dist/chunk-KZWJBKPC.js.map +1 -0
  68. package/dist/chunk-LEAQBGL6.js +109 -0
  69. package/dist/chunk-LEAQBGL6.js.map +1 -0
  70. package/dist/chunk-LGZJL2Q7.js +697 -0
  71. package/dist/chunk-LGZJL2Q7.js.map +1 -0
  72. package/dist/chunk-LQPBU46R.js +109 -0
  73. package/dist/chunk-LQPBU46R.js.map +1 -0
  74. package/dist/chunk-M55NNUTY.js +1013 -0
  75. package/dist/chunk-M55NNUTY.js.map +1 -0
  76. package/dist/chunk-M6NCYCC3.js +1013 -0
  77. package/dist/chunk-M6NCYCC3.js.map +1 -0
  78. package/dist/chunk-MGN7OZAU.js +109 -0
  79. package/dist/chunk-MGN7OZAU.js.map +1 -0
  80. package/dist/chunk-MGYZOX4K.js +1013 -0
  81. package/dist/chunk-MGYZOX4K.js.map +1 -0
  82. package/dist/chunk-MY4IEKJS.js +1031 -0
  83. package/dist/chunk-MY4IEKJS.js.map +1 -0
  84. package/dist/chunk-NCQRGAB4.js +705 -0
  85. package/dist/chunk-NCQRGAB4.js.map +1 -0
  86. package/dist/chunk-NI4NHXI4.js +697 -0
  87. package/dist/chunk-NI4NHXI4.js.map +1 -0
  88. package/dist/chunk-NPI5N7AU.js +14 -0
  89. package/dist/chunk-NPI5N7AU.js.map +1 -0
  90. package/dist/chunk-QN4CPMJF.js +126 -0
  91. package/dist/chunk-QN4CPMJF.js.map +1 -0
  92. package/dist/chunk-QYUVAKMC.js +1013 -0
  93. package/dist/chunk-QYUVAKMC.js.map +1 -0
  94. package/dist/chunk-RAAT6PDT.js +25 -0
  95. package/dist/chunk-RAAT6PDT.js.map +1 -0
  96. package/dist/chunk-RDPO5VPS.js +1013 -0
  97. package/dist/chunk-RDPO5VPS.js.map +1 -0
  98. package/dist/chunk-RQHIW2PF.js +25 -0
  99. package/dist/chunk-RQHIW2PF.js.map +1 -0
  100. package/dist/chunk-RREVHA52.js +722 -0
  101. package/dist/chunk-RREVHA52.js.map +1 -0
  102. package/dist/chunk-S7PT4PZN.js +25 -0
  103. package/dist/chunk-S7PT4PZN.js.map +1 -0
  104. package/dist/chunk-SUXV5M32.js +109 -0
  105. package/dist/chunk-SUXV5M32.js.map +1 -0
  106. package/dist/chunk-SZEORUY6.js +696 -0
  107. package/dist/chunk-SZEORUY6.js.map +1 -0
  108. package/dist/chunk-T7NGGWGY.js +25 -0
  109. package/dist/chunk-T7NGGWGY.js.map +1 -0
  110. package/dist/chunk-UEVWP5VE.js +696 -0
  111. package/dist/chunk-UEVWP5VE.js.map +1 -0
  112. package/dist/chunk-UJNKNP7U.js +748 -0
  113. package/dist/chunk-UJNKNP7U.js.map +1 -0
  114. package/dist/chunk-V4XVFWFI.js +1013 -0
  115. package/dist/chunk-V4XVFWFI.js.map +1 -0
  116. package/dist/chunk-VJU2CIKZ.js +729 -0
  117. package/dist/chunk-VJU2CIKZ.js.map +1 -0
  118. package/dist/chunk-VRQUP5L3.js +202 -0
  119. package/dist/chunk-VRQUP5L3.js.map +1 -0
  120. package/dist/chunk-WBJ5E7CL.js +742 -0
  121. package/dist/chunk-WBJ5E7CL.js.map +1 -0
  122. package/dist/chunk-XJPVYMZM.js +24 -0
  123. package/dist/chunk-XJPVYMZM.js.map +1 -0
  124. package/dist/chunk-Y3Q65C7R.js +14 -0
  125. package/dist/chunk-Y3Q65C7R.js.map +1 -0
  126. package/dist/chunk-YHEGFPTR.js +1030 -0
  127. package/dist/chunk-YHEGFPTR.js.map +1 -0
  128. package/dist/chunk-ZDLBN46C.js +697 -0
  129. package/dist/chunk-ZDLBN46C.js.map +1 -0
  130. package/dist/dev/index.d.ts +122 -0
  131. package/dist/dev/index.js +662 -0
  132. package/dist/dev/index.js.map +1 -0
  133. package/dist/fields/collections.d.ts +18 -0
  134. package/dist/fields/collections.js +37 -0
  135. package/dist/fields/collections.js.map +1 -0
  136. package/dist/index.d.ts +235 -0
  137. package/dist/index.js +956 -0
  138. package/dist/index.js.map +1 -0
  139. package/dist/middleware/index.d.ts +8 -0
  140. package/dist/middleware/index.js +13 -0
  141. package/dist/middleware/index.js.map +1 -0
  142. package/dist/preview-BEkLMD6N.d.ts +23 -0
  143. package/dist/preview-D9yewK8x.d.ts +24 -0
  144. package/dist/preview-DJEA4fj7.d.ts +23 -0
  145. package/dist/preview-DeD1Jn7U.d.ts +23 -0
  146. package/dist/toolbar/app.d.ts +28 -0
  147. package/dist/toolbar/app.js +1176 -0
  148. package/dist/toolbar/app.js.map +1 -0
  149. package/dist/types-15jfCQKb.d.ts +141 -0
  150. package/dist/types-BREdL9Gx.d.ts +140 -0
  151. package/dist/types-C6sUZNa2.d.ts +104 -0
  152. package/dist/types-CEYS5Fzc.d.ts +112 -0
  153. package/dist/types-DpeHpobR.d.ts +104 -0
  154. package/dist/types-NaKJe6Pg.d.ts +140 -0
  155. package/package.json +90 -0
  156. package/src/CmsHead.astro +25 -0
  157. package/src/CmsImage.astro +62 -0
  158. package/src/CmsVideo.astro +185 -0
  159. package/src/RichText.astro +32 -0
  160. package/src/client.test.ts +227 -0
  161. package/src/client.ts +164 -0
  162. package/src/dev/config.test.ts +258 -0
  163. package/src/dev/config.ts +319 -0
  164. package/src/dev/index.ts +5 -0
  165. package/src/dev/preview-token.ts +39 -0
  166. package/src/dev/routes.test.ts +354 -0
  167. package/src/dev/routes.ts +245 -0
  168. package/src/dev/token.test.ts +280 -0
  169. package/src/dev/token.ts +201 -0
  170. package/src/dev/verifier.test.ts +63 -0
  171. package/src/dev/verifier.ts +161 -0
  172. package/src/fields/attrs.ts +24 -0
  173. package/src/fields/collection.ts +72 -0
  174. package/src/fields/collections.ts +35 -0
  175. package/src/fields/entryScope.test.ts +62 -0
  176. package/src/fields/entryScope.ts +240 -0
  177. package/src/fields/group.ts +51 -0
  178. package/src/fields/helpers.test.ts +51 -0
  179. package/src/fields/helpers.ts +214 -0
  180. package/src/fields/index.ts +46 -0
  181. package/src/fields/scope.ts +21 -0
  182. package/src/fields/types.ts +119 -0
  183. package/src/head.test.ts +74 -0
  184. package/src/head.ts +123 -0
  185. package/src/image.test.ts +42 -0
  186. package/src/image.ts +64 -0
  187. package/src/index.test.ts +76 -0
  188. package/src/index.ts +13 -0
  189. package/src/integration.ts +218 -0
  190. package/src/jwt.ts +131 -0
  191. package/src/middleware/index.ts +4 -0
  192. package/src/middleware/preview.test.ts +184 -0
  193. package/src/middleware/preview.ts +206 -0
  194. package/src/preview/bridge-script.test.ts +683 -0
  195. package/src/preview/bridge-script.ts +763 -0
  196. package/src/preview/origins.ts +29 -0
  197. package/src/preview.ts +49 -0
  198. package/src/richtext/RichTextNode.astro +130 -0
  199. package/src/richtext/render.ts +248 -0
  200. package/src/richtext.test.ts +139 -0
  201. package/src/runtime/bundle.ts +28 -0
  202. package/src/runtime/store.ts +130 -0
  203. package/src/siteFiles.test.ts +93 -0
  204. package/src/siteFiles.ts +111 -0
  205. package/src/toolbar/app.test.ts +191 -0
  206. package/src/toolbar/app.ts +1091 -0
  207. package/src/toolbar/icon.ts +5 -0
  208. package/src/toolbar/widgets.test.ts +153 -0
  209. package/src/toolbar/widgets.ts +365 -0
  210. package/src/types.test.ts +154 -0
  211. package/src/types.ts +163 -0
  212. package/src/version.ts +3 -0
@@ -0,0 +1,1091 @@
1
+ import { chromeTokens } from '@407dev/design-tokens/tokens';
2
+ import { defineToolbarApp } from 'astro/toolbar';
3
+ import { DEFAULT_EDITOR_ORIGINS } from '../preview/origins';
4
+ import { isLightweightType, resolveToolbarWidget } from './widgets';
5
+
6
+ interface RenderedFieldInstance {
7
+ key: string;
8
+ kind?: string;
9
+ group?: string;
10
+ entryRef?: {
11
+ collection?: string;
12
+ slug?: string;
13
+ path?: string;
14
+ };
15
+ instances?: number;
16
+ }
17
+
18
+ interface FieldDefinitionItem {
19
+ key: string;
20
+ type: string;
21
+ constraints?: Record<string, unknown>;
22
+ default_value?: unknown;
23
+ value?: unknown;
24
+ orphaned_at?: string | null;
25
+ }
26
+
27
+ interface DevStatusResponse {
28
+ status: 'ok' | 'read-only' | 'refused' | 'unconfigured';
29
+ siteId: string;
30
+ devSiteId?: string;
31
+ apiUrl: string;
32
+ identity?: { sub: string; email?: string; exp?: number; workspaceRole?: string } | null;
33
+ mismatch?: { source: string; expected: string; actual: string } | null;
34
+ manifest: {
35
+ fresh: boolean;
36
+ localHash: string | null;
37
+ pushedHash: string | null;
38
+ };
39
+ verifier: {
40
+ available: boolean;
41
+ errors: Array<{
42
+ type: string;
43
+ message: string;
44
+ file?: string;
45
+ line?: number;
46
+ column?: number;
47
+ }>;
48
+ warnings: Array<{
49
+ type: string;
50
+ message: string;
51
+ file?: string;
52
+ line?: number;
53
+ column?: number;
54
+ }>;
55
+ };
56
+ editorOrigins?: string[];
57
+ error?: string;
58
+ }
59
+
60
+ interface DevBundleResponse {
61
+ bundle?: {
62
+ fields?: Record<string, unknown>;
63
+ site_id?: string;
64
+ } | null;
65
+ fields?: FieldDefinitionItem[];
66
+ }
67
+
68
+ export default defineToolbarApp({
69
+ init(canvas, app, server) {
70
+ let isEmbedded = false;
71
+ try {
72
+ isEmbedded = window.self !== window.top || window.parent !== window;
73
+ } catch {
74
+ isEmbedded = true;
75
+ }
76
+
77
+ if (isEmbedded) {
78
+ const hideToolbar = () => {
79
+ const toolbar = document.querySelector('astro-dev-toolbar');
80
+ if (toolbar instanceof HTMLElement) {
81
+ toolbar.style.setProperty('display', 'none', 'important');
82
+ }
83
+ };
84
+ hideToolbar();
85
+ document.addEventListener('astro:page-load', hideToolbar);
86
+ document.addEventListener('astro:after-swap', hideToolbar);
87
+ }
88
+
89
+ let devStatus: DevStatusResponse | null = null;
90
+ let fieldDefinitions: Map<string, FieldDefinitionItem> = new Map();
91
+ let bundleFieldsMap: Record<string, unknown> = {};
92
+ let renderedFieldsList: RenderedFieldInstance[] = [];
93
+
94
+ // Inject Toolbar Styles into Canvas ShadowRoot
95
+ const style = document.createElement('style');
96
+ style.textContent = `
97
+ :host {
98
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
99
+ color: ${chromeTokens.foreground};
100
+ font-size: 12px;
101
+ line-height: 1.4;
102
+ }
103
+ * {
104
+ box-sizing: border-box;
105
+ }
106
+ astro-dev-toolbar-window {
107
+ position: fixed !important;
108
+ top: 0 !important;
109
+ right: 0 !important;
110
+ bottom: 0 !important;
111
+ left: auto !important;
112
+ transform: none !important;
113
+ width: 440px !important;
114
+ max-width: 100vw !important;
115
+ height: 100vh !important;
116
+ height: 100dvh !important;
117
+ max-height: 100vh !important;
118
+ max-height: 100dvh !important;
119
+ border-radius: 0 !important;
120
+ border: none !important;
121
+ border-left: 1px solid ${chromeTokens.border} !important;
122
+ padding: 0 !important;
123
+ margin: 0 !important;
124
+ background: ${chromeTokens.background} !important;
125
+ box-shadow: -4px 0 24px rgba(0, 0, 0, 0.5) !important;
126
+ z-index: 999999999 !important;
127
+ overflow: hidden !important;
128
+ display: flex !important;
129
+ flex-direction: column !important;
130
+ }
131
+ .toolbar-container {
132
+ display: flex;
133
+ flex-direction: column;
134
+ gap: 12px;
135
+ padding: 16px 16px 56px 16px;
136
+ width: 100%;
137
+ height: 100%;
138
+ max-height: 100vh;
139
+ max-height: 100dvh;
140
+ min-height: 0;
141
+ flex: 1 1 auto;
142
+ overflow-y: auto;
143
+ overflow-x: hidden;
144
+ overscroll-behavior: contain;
145
+ scrollbar-gutter: stable;
146
+ scrollbar-width: thin;
147
+ scrollbar-color: ${chromeTokens.border} transparent;
148
+ background: ${chromeTokens.background};
149
+ border: none;
150
+ border-radius: 0;
151
+ box-shadow: none;
152
+ box-sizing: border-box;
153
+ }
154
+ .toolbar-container > * {
155
+ flex-shrink: 0;
156
+ }
157
+ .toolbar-container::-webkit-scrollbar {
158
+ width: 6px;
159
+ }
160
+ .toolbar-container::-webkit-scrollbar-track {
161
+ background: transparent;
162
+ }
163
+ .toolbar-container::-webkit-scrollbar-thumb {
164
+ background: ${chromeTokens.border};
165
+ border-radius: 3px;
166
+ }
167
+ .toolbar-container::-webkit-scrollbar-thumb:hover {
168
+ background: ${chromeTokens.borderStrong};
169
+ }
170
+ .header-card {
171
+ background: ${chromeTokens.card};
172
+ border: 1px solid ${chromeTokens.border};
173
+ border-radius: 6px;
174
+ padding: 12px;
175
+ display: flex;
176
+ flex-direction: column;
177
+ gap: 8px;
178
+ flex-shrink: 0;
179
+ }
180
+ .header-top {
181
+ display: flex;
182
+ justify-content: space-between;
183
+ align-items: center;
184
+ }
185
+ .title-area {
186
+ display: flex;
187
+ align-items: center;
188
+ gap: 8px;
189
+ }
190
+ .app-title {
191
+ font-weight: 600;
192
+ font-size: 13px;
193
+ color: ${chromeTokens.cardForeground};
194
+ }
195
+ .meta-row {
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: space-between;
199
+ font-size: 11px;
200
+ color: ${chromeTokens.mutedForeground};
201
+ }
202
+ .meta-chips {
203
+ display: flex;
204
+ align-items: center;
205
+ gap: 6px;
206
+ flex-wrap: wrap;
207
+ }
208
+ .chip {
209
+ display: inline-flex;
210
+ align-items: center;
211
+ padding: 2px 6px;
212
+ border-radius: 4px;
213
+ font-size: 10px;
214
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
215
+ }
216
+ .chip-site {
217
+ background: ${chromeTokens.muted};
218
+ color: ${chromeTokens.foregroundSecondary};
219
+ }
220
+ .chip-fresh {
221
+ background: rgba(16, 185, 129, 0.15);
222
+ color: ${chromeTokens.successText};
223
+ border: 1px solid rgba(16, 185, 129, 0.3);
224
+ }
225
+ .chip-unpushed {
226
+ background: rgba(234, 179, 8, 0.15);
227
+ color: ${chromeTokens.warningText};
228
+ border: 1px solid rgba(234, 179, 8, 0.3);
229
+ }
230
+ .toggle-wrap {
231
+ display: flex;
232
+ align-items: center;
233
+ gap: 6px;
234
+ font-size: 11px;
235
+ cursor: pointer;
236
+ user-select: none;
237
+ }
238
+ .toggle-input {
239
+ cursor: pointer;
240
+ }
241
+ .banner {
242
+ border-radius: 6px;
243
+ padding: 10px 12px;
244
+ font-size: 11px;
245
+ flex-shrink: 0;
246
+ }
247
+ .banner-error {
248
+ background: rgba(239, 68, 68, 0.15);
249
+ border: 1px solid rgba(239, 68, 68, 0.4);
250
+ color: ${chromeTokens.destructiveText};
251
+ }
252
+ .banner-warning {
253
+ background: rgba(234, 179, 8, 0.15);
254
+ border: 1px solid rgba(234, 179, 8, 0.4);
255
+ color: ${chromeTokens.warningText};
256
+ }
257
+ .banner-info {
258
+ background: rgba(59, 130, 246, 0.15);
259
+ border: 1px solid rgba(59, 130, 246, 0.4);
260
+ color: ${chromeTokens.infoText};
261
+ }
262
+ .section-title {
263
+ font-size: 11px;
264
+ font-weight: 600;
265
+ text-transform: uppercase;
266
+ letter-spacing: 0.05em;
267
+ color: ${chromeTokens.foregroundSubtle};
268
+ margin-top: 4px;
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: space-between;
272
+ flex-shrink: 0;
273
+ }
274
+ .diag-list {
275
+ display: flex;
276
+ flex-direction: column;
277
+ gap: 6px;
278
+ flex-shrink: 0;
279
+ }
280
+ .diag-item {
281
+ background: ${chromeTokens.card};
282
+ border: 1px solid ${chromeTokens.border};
283
+ border-radius: 6px;
284
+ padding: 8px 10px;
285
+ display: flex;
286
+ flex-direction: column;
287
+ gap: 4px;
288
+ flex-shrink: 0;
289
+ }
290
+ .diag-item-error {
291
+ border-left: 3px solid ${chromeTokens.destructive};
292
+ }
293
+ .diag-item-warning {
294
+ border-left: 3px solid ${chromeTokens.warning};
295
+ }
296
+ .diag-loc {
297
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
298
+ font-size: 10px;
299
+ color: ${chromeTokens.infoText};
300
+ text-decoration: none;
301
+ }
302
+ .diag-loc:hover {
303
+ text-decoration: underline;
304
+ }
305
+ .diag-msg {
306
+ color: ${chromeTokens.foregroundSecondary};
307
+ font-size: 11px;
308
+ }
309
+ .group-card {
310
+ background: ${chromeTokens.card};
311
+ border: 1px solid ${chromeTokens.border};
312
+ border-radius: 6px;
313
+ overflow: hidden;
314
+ flex-shrink: 0;
315
+ }
316
+ .group-header {
317
+ background: ${chromeTokens.muted};
318
+ padding: 6px 10px;
319
+ font-weight: 600;
320
+ font-size: 11px;
321
+ color: ${chromeTokens.foregroundSecondary};
322
+ display: flex;
323
+ align-items: center;
324
+ justify-content: space-between;
325
+ flex-shrink: 0;
326
+ }
327
+ .field-list {
328
+ display: flex;
329
+ flex-direction: column;
330
+ flex-shrink: 0;
331
+ }
332
+ .field-row {
333
+ padding: 8px 10px;
334
+ border-top: 1px solid ${chromeTokens.border};
335
+ display: flex;
336
+ flex-direction: column;
337
+ gap: 6px;
338
+ transition: background-color 0.3s ease;
339
+ flex-shrink: 0;
340
+ }
341
+ @keyframes fieldHighlightPulse {
342
+ 0% {
343
+ background: rgba(99, 102, 241, 0.35);
344
+ box-shadow: inset 0 0 0 1px ${chromeTokens.primary};
345
+ }
346
+ 70% {
347
+ background: rgba(99, 102, 241, 0.15);
348
+ box-shadow: inset 0 0 0 1px rgba(99, 102, 241, 0.4);
349
+ }
350
+ 100% {
351
+ background: transparent;
352
+ box-shadow: none;
353
+ }
354
+ }
355
+ .field-row-highlighted {
356
+ animation: fieldHighlightPulse 2s ease-out;
357
+ }
358
+ .field-meta {
359
+ display: flex;
360
+ align-items: center;
361
+ justify-content: space-between;
362
+ gap: 6px;
363
+ }
364
+ .field-key {
365
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
366
+ font-size: 11px;
367
+ font-weight: 500;
368
+ color: ${chromeTokens.foregroundSecondary};
369
+ }
370
+ .badge-group {
371
+ display: flex;
372
+ align-items: center;
373
+ gap: 4px;
374
+ }
375
+ .badge {
376
+ font-size: 9px;
377
+ padding: 1px 4px;
378
+ border-radius: 3px;
379
+ font-weight: 500;
380
+ text-transform: lowercase;
381
+ }
382
+ .badge-no-value {
383
+ background: rgba(239, 68, 68, 0.2);
384
+ color: ${chromeTokens.destructiveText};
385
+ }
386
+ .badge-no-default {
387
+ background: rgba(234, 179, 8, 0.2);
388
+ color: ${chromeTokens.warningText};
389
+ }
390
+ .badge-orphaned {
391
+ background: rgba(168, 85, 247, 0.2);
392
+ color: ${chromeTokens.primaryText};
393
+ }
394
+ .badge-type {
395
+ background: ${chromeTokens.muted};
396
+ color: ${chromeTokens.mutedForeground};
397
+ }
398
+ /* Widget inputs */
399
+ .cms-widget-container {
400
+ display: flex;
401
+ flex-direction: column;
402
+ gap: 4px;
403
+ }
404
+ .cms-widget-form {
405
+ display: flex;
406
+ gap: 6px;
407
+ align-items: center;
408
+ }
409
+ .cms-widget-input {
410
+ flex: 1;
411
+ background: ${chromeTokens.sunken};
412
+ border: 1px solid ${chromeTokens.borderStrong};
413
+ border-radius: 4px;
414
+ color: ${chromeTokens.foreground};
415
+ padding: 4px 8px;
416
+ font-size: 11px;
417
+ outline: none;
418
+ }
419
+ .cms-widget-input:focus {
420
+ border-color: ${chromeTokens.ring};
421
+ }
422
+ .cms-widget-input:disabled {
423
+ opacity: 0.5;
424
+ cursor: not-allowed;
425
+ }
426
+ .cms-widget-save-btn {
427
+ background: ${chromeTokens.primary};
428
+ color: ${chromeTokens.primaryForeground};
429
+ border: none;
430
+ border-radius: 4px;
431
+ padding: 4px 10px;
432
+ font-size: 11px;
433
+ font-weight: 500;
434
+ cursor: pointer;
435
+ }
436
+ .cms-widget-save-btn:hover:not(:disabled) {
437
+ background: color-mix(in srgb, ${chromeTokens.primary} 85%, black);
438
+ }
439
+ .cms-widget-save-btn:disabled {
440
+ opacity: 0.5;
441
+ cursor: not-allowed;
442
+ }
443
+ .cms-widget-select {
444
+ width: 100%;
445
+ background: ${chromeTokens.sunken};
446
+ border: 1px solid ${chromeTokens.borderStrong};
447
+ border-radius: 4px;
448
+ color: ${chromeTokens.foreground};
449
+ padding: 4px 8px;
450
+ font-size: 11px;
451
+ outline: none;
452
+ }
453
+ .cms-widget-toggle-row {
454
+ display: flex;
455
+ align-items: center;
456
+ gap: 6px;
457
+ }
458
+ .cms-widget-toggle-label {
459
+ display: flex;
460
+ align-items: center;
461
+ gap: 6px;
462
+ cursor: pointer;
463
+ }
464
+ .cms-widget-checkbox {
465
+ cursor: pointer;
466
+ }
467
+ .cms-widget-toggle-text {
468
+ font-size: 11px;
469
+ color: ${chromeTokens.foregroundSecondary};
470
+ }
471
+ .cms-widget-summary-row {
472
+ display: flex;
473
+ align-items: center;
474
+ justify-content: space-between;
475
+ gap: 8px;
476
+ background: ${chromeTokens.sunken};
477
+ border: 1px solid ${chromeTokens.border};
478
+ border-radius: 4px;
479
+ padding: 4px 8px;
480
+ }
481
+ .cms-widget-summary-text {
482
+ font-size: 11px;
483
+ color: ${chromeTokens.mutedForeground};
484
+ overflow: hidden;
485
+ text-overflow: ellipsis;
486
+ white-space: nowrap;
487
+ max-width: 260px;
488
+ }
489
+ .cms-widget-editor-link {
490
+ font-size: 11px;
491
+ color: ${chromeTokens.primaryText};
492
+ text-decoration: none;
493
+ white-space: nowrap;
494
+ }
495
+ .cms-widget-editor-link:hover {
496
+ text-decoration: underline;
497
+ }
498
+ .cms-widget-error {
499
+ font-size: 10px;
500
+ color: ${chromeTokens.destructiveText};
501
+ margin-top: 2px;
502
+ }
503
+ .empty-notice {
504
+ font-size: 11px;
505
+ color: ${chromeTokens.foregroundSubtle};
506
+ font-style: italic;
507
+ padding: 8px 0;
508
+ flex-shrink: 0;
509
+ }
510
+ `;
511
+
512
+ canvas.appendChild(style);
513
+
514
+ const windowEl = document.createElement('astro-dev-toolbar-window');
515
+ windowEl.style.position = 'fixed';
516
+ windowEl.style.top = '0';
517
+ windowEl.style.right = '0';
518
+ windowEl.style.bottom = '0';
519
+ windowEl.style.left = 'auto';
520
+ windowEl.style.transform = 'none';
521
+ windowEl.style.width = '440px';
522
+ windowEl.style.maxWidth = '100vw';
523
+ windowEl.style.height = '100vh';
524
+ windowEl.style.maxHeight = '100vh';
525
+ windowEl.style.borderRadius = '0';
526
+ windowEl.style.border = 'none';
527
+ windowEl.style.borderLeft = `1px solid ${chromeTokens.border}`;
528
+ windowEl.style.padding = '0';
529
+ windowEl.style.margin = '0';
530
+ windowEl.style.background = chromeTokens.background;
531
+ windowEl.style.boxShadow = '-4px 0 24px rgba(0, 0, 0, 0.5)';
532
+ windowEl.style.zIndex = '999999999';
533
+ windowEl.style.display = 'flex';
534
+ windowEl.style.flexDirection = 'column';
535
+ windowEl.style.overflow = 'hidden';
536
+
537
+ const container = document.createElement('div');
538
+ container.className = 'toolbar-container';
539
+ container.style.height = '100%';
540
+ container.style.minHeight = '0';
541
+ container.style.flex = '1 1 auto';
542
+ container.style.overflowY = 'auto';
543
+ container.style.boxSizing = 'border-box';
544
+ windowEl.appendChild(container);
545
+ canvas.appendChild(windowEl);
546
+
547
+ function patchDomElement(key: string, kind: string, value: unknown, entryRef?: unknown) {
548
+ let selector: string;
549
+ const eRef = entryRef as { collection?: string; slug?: string; path?: string } | undefined;
550
+ const fPath = eRef?.path || key;
551
+
552
+ if (eRef && (eRef.collection || eRef.slug)) {
553
+ const colAttr = eRef.collection ? `[data-cms-collection="${eRef.collection}"]` : '';
554
+ const entryAttr = eRef.slug ? `[data-cms-entry="${eRef.slug}"]` : '';
555
+ const scopeSel = `${colAttr}${entryAttr}`;
556
+ selector = `${scopeSel} [data-cms-entry-field="${fPath}"], ${scopeSel}[data-cms-entry-field="${fPath}"]`;
557
+ } else if (eRef) {
558
+ selector = `[data-cms-entry-field="${fPath}"]`;
559
+ } else {
560
+ selector = `[data-cms-field="${key}"]`;
561
+ }
562
+
563
+ const els = document.querySelectorAll(selector);
564
+ for (let i = 0; i < els.length; i++) {
565
+ const el = els[i];
566
+ if (kind === 'image' || el.tagName.toLowerCase() === 'img') {
567
+ if (value && typeof value === 'object') {
568
+ const imgVal = value as { url?: string; alt?: string };
569
+ if (imgVal.url) el.setAttribute('src', imgVal.url);
570
+ if (imgVal.alt !== undefined) el.setAttribute('alt', imgVal.alt);
571
+ } else if (typeof value === 'string') {
572
+ el.setAttribute('src', value);
573
+ }
574
+ } else {
575
+ el.textContent = value !== null && value !== undefined ? String(value) : '';
576
+ }
577
+ }
578
+ }
579
+
580
+ async function handleSaveValue(
581
+ key: string,
582
+ val: unknown,
583
+ kind: string,
584
+ entryRef?: unknown,
585
+ ): Promise<{ ok: boolean; error?: string }> {
586
+ try {
587
+ const res = await fetch('/_cms/dev/value', {
588
+ method: 'PATCH',
589
+ headers: {
590
+ 'Content-Type': 'application/json',
591
+ },
592
+ body: JSON.stringify({ key, value: val }),
593
+ });
594
+
595
+ if (res.ok) {
596
+ // Update local bundle store representation
597
+ bundleFieldsMap[key] = val;
598
+ const def = fieldDefinitions.get(key);
599
+ if (def) {
600
+ def.value = val;
601
+ }
602
+
603
+ // Patch DOM in place
604
+ patchDomElement(key, kind, val, entryRef);
605
+
606
+ // Rescan fields
607
+ const w = window as unknown as { __CMS_RESCAN__?: () => void };
608
+ w.__CMS_RESCAN__?.();
609
+
610
+ render();
611
+ return { ok: true };
612
+ }
613
+
614
+ const data = (await res.json().catch(() => ({}))) as {
615
+ error?: string;
616
+ errors?: Array<{ message?: string }>;
617
+ };
618
+ const errorMsg =
619
+ data.errors?.map((e) => e.message).join(', ') ||
620
+ data.error ||
621
+ `Save failed (HTTP ${res.status})`;
622
+ return { ok: false, error: errorMsg };
623
+ } catch (err) {
624
+ const msg = err instanceof Error ? err.message : String(err);
625
+ return { ok: false, error: msg };
626
+ }
627
+ }
628
+
629
+ function render() {
630
+ container.innerHTML = '';
631
+
632
+ if (!devStatus) {
633
+ const loadingDiv = document.createElement('div');
634
+ loadingDiv.className = 'empty-notice';
635
+ loadingDiv.textContent = 'Loading CMS dev status...';
636
+ container.appendChild(loadingDiv);
637
+ return;
638
+ }
639
+
640
+ const isReadOnly = devStatus.status === 'read-only';
641
+ const isRefused = devStatus.status === 'refused';
642
+ const isUnconfigured = devStatus.status === 'unconfigured';
643
+ const disabled = isReadOnly || isRefused || isUnconfigured;
644
+
645
+ // 1. Header Card
646
+ const headerCard = document.createElement('div');
647
+ headerCard.className = 'header-card';
648
+
649
+ const headerTop = document.createElement('div');
650
+ headerTop.className = 'header-top';
651
+
652
+ const titleArea = document.createElement('div');
653
+ titleArea.className = 'title-area';
654
+ const titleSpan = document.createElement('span');
655
+ titleSpan.className = 'app-title';
656
+ titleSpan.textContent = 'CMS Content';
657
+ titleArea.appendChild(titleSpan);
658
+
659
+ // Highlight toggle
660
+ const toggleWrap = document.createElement('label');
661
+ toggleWrap.className = 'toggle-wrap';
662
+ const toggleCheckbox = document.createElement('input');
663
+ toggleCheckbox.type = 'checkbox';
664
+ toggleCheckbox.className = 'toggle-input';
665
+ toggleCheckbox.checked = document.documentElement.hasAttribute('data-cms-edit-mode');
666
+ toggleCheckbox.addEventListener('change', () => {
667
+ if (toggleCheckbox.checked) {
668
+ document.documentElement.setAttribute('data-cms-edit-mode', 'true');
669
+ } else {
670
+ document.documentElement.removeAttribute('data-cms-edit-mode');
671
+ }
672
+ });
673
+ const toggleLabel = document.createElement('span');
674
+ toggleLabel.textContent = 'Outlines';
675
+ toggleWrap.appendChild(toggleCheckbox);
676
+ toggleWrap.appendChild(toggleLabel);
677
+
678
+ headerTop.appendChild(titleArea);
679
+ headerTop.appendChild(toggleWrap);
680
+ headerCard.appendChild(headerTop);
681
+
682
+ // Meta chips
683
+ const metaRow = document.createElement('div');
684
+ metaRow.className = 'meta-row';
685
+
686
+ const chips = document.createElement('div');
687
+ chips.className = 'meta-chips';
688
+
689
+ const siteChip = document.createElement('span');
690
+ siteChip.className = 'chip chip-site';
691
+ siteChip.textContent = devStatus.siteId || 'No Site';
692
+ chips.appendChild(siteChip);
693
+
694
+ if (devStatus.identity?.email || devStatus.identity?.sub) {
695
+ const userChip = document.createElement('span');
696
+ userChip.className = 'chip chip-site';
697
+ userChip.textContent = devStatus.identity.email || devStatus.identity.sub;
698
+ chips.appendChild(userChip);
699
+ }
700
+
701
+ if (devStatus.verifier?.available) {
702
+ const freshChip = document.createElement('span');
703
+ if (devStatus.manifest?.fresh) {
704
+ freshChip.className = 'chip chip-fresh';
705
+ freshChip.textContent = '● Fresh';
706
+ } else {
707
+ freshChip.className = 'chip chip-unpushed';
708
+ freshChip.textContent = '▲ Unpushed';
709
+ }
710
+ chips.appendChild(freshChip);
711
+ }
712
+
713
+ metaRow.appendChild(chips);
714
+ headerCard.appendChild(metaRow);
715
+ container.appendChild(headerCard);
716
+
717
+ // 2. Alert Banners
718
+ if (isReadOnly && devStatus.mismatch) {
719
+ const banner = document.createElement('div');
720
+ banner.className = 'banner banner-error';
721
+ banner.textContent = `Toolbar is READ-ONLY: ${devStatus.mismatch.source} (${devStatus.mismatch.actual}) ≠ site rendered by this dev server (${devStatus.mismatch.expected})`;
722
+ container.appendChild(banner);
723
+ } else if (isRefused) {
724
+ const banner = document.createElement('div');
725
+ banner.className = 'banner banner-error';
726
+ banner.textContent = `CMS dev routes disabled: ${devStatus.error || 'Access refused'}`;
727
+ container.appendChild(banner);
728
+ } else if (isUnconfigured) {
729
+ const banner = document.createElement('div');
730
+ banner.className = 'banner banner-info';
731
+ banner.textContent =
732
+ devStatus.error || 'Not logged in. Run `cms login` to enable dev-time editing.';
733
+ container.appendChild(banner);
734
+ }
735
+
736
+ // 3. Diagnostics Section
737
+ const errors = devStatus.verifier?.errors || [];
738
+ const warnings = devStatus.verifier?.warnings || [];
739
+ const allDiags = [...errors, ...warnings];
740
+
741
+ if (allDiags.length > 0) {
742
+ const diagSec = document.createElement('div');
743
+ diagSec.className = 'section-title';
744
+ diagSec.textContent = `Diagnostics (${allDiags.length})`;
745
+ container.appendChild(diagSec);
746
+
747
+ const diagList = document.createElement('div');
748
+ diagList.className = 'diag-list';
749
+
750
+ for (const diag of allDiags) {
751
+ const item = document.createElement('div');
752
+ item.className = `diag-item ${diag.type === 'error' ? 'diag-item-error' : 'diag-item-warning'}`;
753
+
754
+ if (diag.file) {
755
+ const locLink = document.createElement('a');
756
+ locLink.className = 'diag-loc';
757
+ locLink.href = `vscode://file/${diag.file}:${diag.line || 1}:${diag.column || 1}`;
758
+ locLink.textContent = `${diag.file}:${diag.line || 1}:${diag.column || 1}`;
759
+ item.appendChild(locLink);
760
+ }
761
+
762
+ const msg = document.createElement('div');
763
+ msg.className = 'diag-msg';
764
+ msg.textContent = diag.message;
765
+ item.appendChild(msg);
766
+
767
+ diagList.appendChild(item);
768
+ }
769
+
770
+ container.appendChild(diagList);
771
+ }
772
+
773
+ // 4. Current Page Rendered Fields List
774
+ const fieldsSecTitle = document.createElement('div');
775
+ fieldsSecTitle.className = 'section-title';
776
+ fieldsSecTitle.textContent = `Rendered Fields (${renderedFieldsList.length})`;
777
+ container.appendChild(fieldsSecTitle);
778
+
779
+ if (renderedFieldsList.length === 0) {
780
+ const empty = document.createElement('div');
781
+ empty.className = 'empty-notice';
782
+ empty.textContent = 'No CMS fields detected on current page.';
783
+ container.appendChild(empty);
784
+ } else {
785
+ // Group rendered fields by data-cms-group, or scope prefix, or "Ungrouped"
786
+ const groupsMap = new Map<string, RenderedFieldInstance[]>();
787
+
788
+ for (const field of renderedFieldsList) {
789
+ let groupName = field.group;
790
+ if (!groupName) {
791
+ const parts = field.key.split('.');
792
+ if (parts.length > 1) {
793
+ groupName = parts.slice(0, -1).join('.');
794
+ } else {
795
+ groupName = 'Ungrouped';
796
+ }
797
+ }
798
+ if (!groupsMap.has(groupName)) {
799
+ groupsMap.set(groupName, []);
800
+ }
801
+ groupsMap.get(groupName)?.push(field);
802
+ }
803
+
804
+ const currentRoute = window.location.pathname || '/';
805
+ const editorOrigin = devStatus.editorOrigins?.[0] || DEFAULT_EDITOR_ORIGINS[0];
806
+
807
+ for (const [groupName, groupFields] of groupsMap.entries()) {
808
+ const groupCard = document.createElement('div');
809
+ groupCard.className = 'group-card';
810
+
811
+ const groupHeader = document.createElement('div');
812
+ groupHeader.className = 'group-header';
813
+ groupHeader.textContent = groupName;
814
+ groupCard.appendChild(groupHeader);
815
+
816
+ const fieldList = document.createElement('div');
817
+ fieldList.className = 'field-list';
818
+
819
+ for (const field of groupFields) {
820
+ const def = fieldDefinitions.get(field.key);
821
+ const rawVal =
822
+ bundleFieldsMap[field.key] !== undefined
823
+ ? bundleFieldsMap[field.key]
824
+ : def?.value !== undefined
825
+ ? def.value
826
+ : def?.default_value;
827
+
828
+ const typeId = def?.type || field.kind || 'text';
829
+ const isOrphaned = Boolean(def?.orphaned_at);
830
+ const hasNoValue = rawVal === undefined || rawVal === null || rawVal === '';
831
+ const hasNoDefault = def?.default_value === undefined || def?.default_value === null;
832
+
833
+ const row = document.createElement('div');
834
+ row.className = 'field-row';
835
+ row.dataset.fieldKey = field.key;
836
+ row.setAttribute('data-field-key', field.key);
837
+
838
+ const meta = document.createElement('div');
839
+ meta.className = 'field-meta';
840
+
841
+ const keySpan = document.createElement('span');
842
+ keySpan.className = 'field-key';
843
+ keySpan.textContent = field.key;
844
+ meta.appendChild(keySpan);
845
+
846
+ const badgeGroup = document.createElement('div');
847
+ badgeGroup.className = 'badge-group';
848
+
849
+ if (isOrphaned) {
850
+ const b = document.createElement('span');
851
+ b.className = 'badge badge-orphaned';
852
+ b.textContent = 'orphaned';
853
+ badgeGroup.appendChild(b);
854
+ }
855
+ if (hasNoValue) {
856
+ const b = document.createElement('span');
857
+ b.className = 'badge badge-no-value';
858
+ b.textContent = 'no value';
859
+ badgeGroup.appendChild(b);
860
+ }
861
+ if (hasNoDefault) {
862
+ const b = document.createElement('span');
863
+ b.className = 'badge badge-no-default';
864
+ b.textContent = 'no default';
865
+ badgeGroup.appendChild(b);
866
+ }
867
+
868
+ const typeBadge = document.createElement('span');
869
+ typeBadge.className = 'badge badge-type';
870
+ typeBadge.textContent = typeId;
871
+ badgeGroup.appendChild(typeBadge);
872
+
873
+ meta.appendChild(badgeGroup);
874
+ row.appendChild(meta);
875
+
876
+ // Widget resolution
877
+ const renderer = resolveToolbarWidget(typeId);
878
+ const widgetEl = renderer({
879
+ key: field.key,
880
+ typeId,
881
+ value: rawVal,
882
+ defaultValue: def?.default_value,
883
+ constraints: def?.constraints,
884
+ disabled,
885
+ siteId: devStatus.siteId,
886
+ routePath: currentRoute,
887
+ editorOrigin,
888
+ onSave: async (v) =>
889
+ handleSaveValue(field.key, v, field.kind || typeId, field.entryRef),
890
+ });
891
+
892
+ row.appendChild(widgetEl);
893
+ fieldList.appendChild(row);
894
+ }
895
+
896
+ groupCard.appendChild(fieldList);
897
+ container.appendChild(groupCard);
898
+ }
899
+ }
900
+
901
+ // 5. Orphaned Fields Section
902
+ const renderedKeys = new Set(renderedFieldsList.map((f) => f.key));
903
+ const orphanedDefs: FieldDefinitionItem[] = [];
904
+ for (const def of fieldDefinitions.values()) {
905
+ if (def.orphaned_at || !renderedKeys.has(def.key)) {
906
+ orphanedDefs.push(def);
907
+ }
908
+ }
909
+
910
+ if (orphanedDefs.length > 0) {
911
+ const orphanTitle = document.createElement('div');
912
+ orphanTitle.className = 'section-title';
913
+ orphanTitle.textContent = `Unrendered / Orphaned Fields (${orphanedDefs.length})`;
914
+ container.appendChild(orphanTitle);
915
+
916
+ const orphanCard = document.createElement('div');
917
+ orphanCard.className = 'group-card';
918
+ const orphanList = document.createElement('div');
919
+ orphanList.className = 'field-list';
920
+
921
+ for (const o of orphanedDefs) {
922
+ const row = document.createElement('div');
923
+ row.className = 'field-row';
924
+ row.dataset.fieldKey = o.key;
925
+ row.setAttribute('data-field-key', o.key);
926
+
927
+ const meta = document.createElement('div');
928
+ meta.className = 'field-meta';
929
+
930
+ const keySpan = document.createElement('span');
931
+ keySpan.className = 'field-key';
932
+ keySpan.textContent = o.key;
933
+ meta.appendChild(keySpan);
934
+
935
+ const badgeGroup = document.createElement('div');
936
+ badgeGroup.className = 'badge-group';
937
+ if (o.orphaned_at) {
938
+ const b = document.createElement('span');
939
+ b.className = 'badge badge-orphaned';
940
+ b.textContent = 'orphaned';
941
+ badgeGroup.appendChild(b);
942
+ } else {
943
+ const b = document.createElement('span');
944
+ b.className = 'badge badge-type';
945
+ b.textContent = 'unrendered';
946
+ badgeGroup.appendChild(b);
947
+ }
948
+ meta.appendChild(badgeGroup);
949
+ row.appendChild(meta);
950
+ orphanList.appendChild(row);
951
+ }
952
+
953
+ orphanCard.appendChild(orphanList);
954
+ container.appendChild(orphanCard);
955
+ }
956
+ }
957
+
958
+ async function loadData() {
959
+ try {
960
+ const statusRes = await fetch('/_cms/dev/status');
961
+ if (statusRes.ok) {
962
+ devStatus = (await statusRes.json()) as DevStatusResponse;
963
+ } else if (statusRes.status === 503) {
964
+ const errData = (await statusRes.json().catch(() => ({}))) as {
965
+ error?: string;
966
+ status?: 'refused' | 'unconfigured';
967
+ };
968
+ devStatus = {
969
+ status: errData.status || 'refused',
970
+ siteId: '',
971
+ apiUrl: '',
972
+ manifest: { fresh: true, localHash: null, pushedHash: null },
973
+ verifier: { available: false, errors: [], warnings: [] },
974
+ error: errData.error,
975
+ };
976
+ }
977
+
978
+ // Toggle app notification state based on diagnostics
979
+ if (devStatus?.verifier?.errors && devStatus.verifier.errors.length > 0) {
980
+ app.toggleNotification({ state: true, level: 'error' });
981
+ } else if (devStatus?.verifier?.warnings && devStatus.verifier.warnings.length > 0) {
982
+ app.toggleNotification({ state: true, level: 'warning' });
983
+ } else {
984
+ app.toggleNotification({ state: false });
985
+ }
986
+
987
+ if (devStatus && devStatus.status !== 'refused' && devStatus.status !== 'unconfigured') {
988
+ const bundleRes = await fetch('/_cms/dev/bundle');
989
+ if (bundleRes.ok) {
990
+ const bundleData = (await bundleRes.json()) as DevBundleResponse;
991
+ if (bundleData.bundle?.fields) {
992
+ bundleFieldsMap = bundleData.bundle.fields;
993
+ }
994
+ if (Array.isArray(bundleData.fields)) {
995
+ fieldDefinitions = new Map(bundleData.fields.map((f) => [f.key, f]));
996
+ }
997
+ }
998
+ }
999
+ } catch (_err) {
1000
+ // Fetch failed
1001
+ }
1002
+
1003
+ // Read rendered fields from window
1004
+ const winFields = (window as unknown as { __CMS_FIELDS__?: RenderedFieldInstance[] })
1005
+ .__CMS_FIELDS__;
1006
+ if (Array.isArray(winFields)) {
1007
+ renderedFieldsList = winFields;
1008
+ }
1009
+
1010
+ render();
1011
+ }
1012
+
1013
+ // Server HMR diagnostic events
1014
+ server.on('cms:diagnostics', (data: DevStatusResponse['verifier'] & { fresh?: boolean }) => {
1015
+ if (devStatus) {
1016
+ devStatus.verifier = {
1017
+ available: Boolean(data.available),
1018
+ errors: data.errors || [],
1019
+ warnings: data.warnings || [],
1020
+ };
1021
+ if (data.fresh !== undefined) {
1022
+ devStatus.manifest.fresh = data.fresh;
1023
+ }
1024
+
1025
+ if (data.errors && data.errors.length > 0) {
1026
+ app.toggleNotification({ state: true, level: 'error' });
1027
+ } else if (data.warnings && data.warnings.length > 0) {
1028
+ app.toggleNotification({ state: true, level: 'warning' });
1029
+ } else {
1030
+ app.toggleNotification({ state: false });
1031
+ }
1032
+
1033
+ render();
1034
+ }
1035
+ });
1036
+
1037
+ function escapeCssKey(str: string): string {
1038
+ if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
1039
+ return CSS.escape(str);
1040
+ }
1041
+ return str.replace(/([!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, '\\$1');
1042
+ }
1043
+
1044
+ function focusFieldRow(key: string) {
1045
+ if (!key) return;
1046
+ const targetRow = container.querySelector(
1047
+ `[data-field-key="${escapeCssKey(key)}"]`,
1048
+ ) as HTMLElement | null;
1049
+ if (targetRow) {
1050
+ targetRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
1051
+ targetRow.classList.remove('field-row-highlighted');
1052
+ // Force layout reflow to restart CSS animation
1053
+ void targetRow.offsetWidth;
1054
+ targetRow.classList.add('field-row-highlighted');
1055
+
1056
+ const inputEl = targetRow.querySelector(
1057
+ 'input:not([type="checkbox"]), select, textarea, button, a',
1058
+ ) as HTMLInputElement | null;
1059
+ if (inputEl) {
1060
+ setTimeout(() => {
1061
+ inputEl.focus();
1062
+ if (typeof inputEl.select === 'function') {
1063
+ inputEl.select();
1064
+ }
1065
+ }, 150);
1066
+ }
1067
+ }
1068
+ }
1069
+
1070
+ // Document events from bridge-script
1071
+ document.addEventListener('cms:fields-updated', ((e: CustomEvent<RenderedFieldInstance[]>) => {
1072
+ if (Array.isArray(e.detail)) {
1073
+ renderedFieldsList = e.detail;
1074
+ render();
1075
+ }
1076
+ }) as EventListener);
1077
+
1078
+ document.addEventListener('cms:field-selected', ((e: CustomEvent<{ key?: string }>) => {
1079
+ if (e.detail?.key) {
1080
+ focusFieldRow(e.detail.key);
1081
+ }
1082
+ }) as EventListener);
1083
+
1084
+ document.addEventListener('astro:page-load', () => {
1085
+ loadData();
1086
+ });
1087
+
1088
+ // Initial load
1089
+ loadData();
1090
+ },
1091
+ });