@caretcms/core 0.1.2 → 0.3.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 (190) hide show
  1. package/README.md +109 -13
  2. package/dist/contracts.d.ts +1 -0
  3. package/dist/contracts.d.ts.map +1 -1
  4. package/dist/contracts.js +1 -0
  5. package/dist/contracts.js.map +1 -1
  6. package/dist/index.d.ts +31 -9
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +146 -30
  9. package/dist/index.js.map +1 -1
  10. package/dist/loader.d.ts +17 -6
  11. package/dist/loader.d.ts.map +1 -1
  12. package/dist/loader.js +57 -14
  13. package/dist/loader.js.map +1 -1
  14. package/dist/markdown/block-context.d.ts +21 -0
  15. package/dist/markdown/block-context.d.ts.map +1 -0
  16. package/dist/markdown/block-context.js +27 -0
  17. package/dist/markdown/block-context.js.map +1 -0
  18. package/dist/markdown/canonical-body.d.ts +30 -0
  19. package/dist/markdown/canonical-body.d.ts.map +1 -0
  20. package/dist/markdown/canonical-body.js +54 -0
  21. package/dist/markdown/canonical-body.js.map +1 -0
  22. package/dist/markdown/contracts.d.ts +66 -0
  23. package/dist/markdown/contracts.d.ts.map +1 -0
  24. package/dist/markdown/contracts.js +102 -0
  25. package/dist/markdown/contracts.js.map +1 -0
  26. package/dist/markdown/html-to-snodes.d.ts +26 -0
  27. package/dist/markdown/html-to-snodes.d.ts.map +1 -0
  28. package/dist/markdown/html-to-snodes.js +118 -0
  29. package/dist/markdown/html-to-snodes.js.map +1 -0
  30. package/dist/markdown/remark.d.ts +35 -0
  31. package/dist/markdown/remark.d.ts.map +1 -0
  32. package/dist/markdown/remark.js +66 -0
  33. package/dist/markdown/remark.js.map +1 -0
  34. package/dist/markdown/satteri.d.ts +19 -0
  35. package/dist/markdown/satteri.d.ts.map +1 -0
  36. package/dist/markdown/satteri.js +144 -0
  37. package/dist/markdown/satteri.js.map +1 -0
  38. package/dist/markdown/serialize.d.ts +60 -0
  39. package/dist/markdown/serialize.d.ts.map +1 -0
  40. package/dist/markdown/serialize.js +264 -0
  41. package/dist/markdown/serialize.js.map +1 -0
  42. package/dist/markdown/splice.d.ts +32 -0
  43. package/dist/markdown/splice.d.ts.map +1 -0
  44. package/dist/markdown/splice.js +44 -0
  45. package/dist/markdown/splice.js.map +1 -0
  46. package/dist/markdown/stamp.d.ts +68 -0
  47. package/dist/markdown/stamp.d.ts.map +1 -0
  48. package/dist/markdown/stamp.js +131 -0
  49. package/dist/markdown/stamp.js.map +1 -0
  50. package/dist/runtime/auth/rate-limiter.d.ts.map +1 -1
  51. package/dist/runtime/auth/rate-limiter.js +5 -0
  52. package/dist/runtime/auth/rate-limiter.js.map +1 -1
  53. package/dist/runtime/auth/session.d.ts +3 -3
  54. package/dist/runtime/auth/session.d.ts.map +1 -1
  55. package/dist/runtime/auth/session.js +68 -21
  56. package/dist/runtime/auth/session.js.map +1 -1
  57. package/dist/runtime/config.d.ts +3 -0
  58. package/dist/runtime/config.d.ts.map +1 -1
  59. package/dist/runtime/config.js +4 -1
  60. package/dist/runtime/config.js.map +1 -1
  61. package/dist/runtime/content.d.ts.map +1 -1
  62. package/dist/runtime/content.js +4 -2
  63. package/dist/runtime/content.js.map +1 -1
  64. package/dist/runtime/i18n.d.ts +86 -0
  65. package/dist/runtime/i18n.d.ts.map +1 -0
  66. package/dist/runtime/i18n.js +168 -0
  67. package/dist/runtime/i18n.js.map +1 -0
  68. package/dist/runtime/middleware.d.ts.map +1 -1
  69. package/dist/runtime/middleware.js +61 -6
  70. package/dist/runtime/middleware.js.map +1 -1
  71. package/dist/runtime/mutations/contracts.d.ts +28 -1
  72. package/dist/runtime/mutations/contracts.d.ts.map +1 -1
  73. package/dist/runtime/mutations/contracts.js +76 -0
  74. package/dist/runtime/mutations/contracts.js.map +1 -1
  75. package/dist/runtime/mutations/engine.d.ts +6 -1
  76. package/dist/runtime/mutations/engine.d.ts.map +1 -1
  77. package/dist/runtime/mutations/engine.js +271 -51
  78. package/dist/runtime/mutations/engine.js.map +1 -1
  79. package/dist/runtime/providers.d.ts +2 -1
  80. package/dist/runtime/providers.d.ts.map +1 -1
  81. package/dist/runtime/providers.js +7 -1
  82. package/dist/runtime/providers.js.map +1 -1
  83. package/dist/runtime/publish.d.ts +13 -1
  84. package/dist/runtime/publish.d.ts.map +1 -1
  85. package/dist/runtime/publish.js +80 -4
  86. package/dist/runtime/publish.js.map +1 -1
  87. package/dist/runtime/request-context.d.ts +14 -2
  88. package/dist/runtime/request-context.d.ts.map +1 -1
  89. package/dist/runtime/request-context.js +7 -1
  90. package/dist/runtime/request-context.js.map +1 -1
  91. package/dist/runtime/rewrite.d.ts.map +1 -1
  92. package/dist/runtime/rewrite.js +109 -3
  93. package/dist/runtime/rewrite.js.map +1 -1
  94. package/dist/runtime/rich-allowlist.d.ts.map +1 -1
  95. package/dist/runtime/rich-allowlist.js +5 -1
  96. package/dist/runtime/rich-allowlist.js.map +1 -1
  97. package/dist/runtime/routes/admin-entry.d.ts.map +1 -1
  98. package/dist/runtime/routes/admin-entry.js +114 -18
  99. package/dist/runtime/routes/admin-entry.js.map +1 -1
  100. package/dist/runtime/routes/admin.d.ts.map +1 -1
  101. package/dist/runtime/routes/admin.js +33 -15
  102. package/dist/runtime/routes/admin.js.map +1 -1
  103. package/dist/runtime/routes/auth-login.d.ts.map +1 -1
  104. package/dist/runtime/routes/auth-login.js +4 -0
  105. package/dist/runtime/routes/auth-login.js.map +1 -1
  106. package/dist/runtime/routes/auth-logout.d.ts.map +1 -1
  107. package/dist/runtime/routes/auth-logout.js +7 -2
  108. package/dist/runtime/routes/auth-logout.js.map +1 -1
  109. package/dist/runtime/routes/auth-session.d.ts.map +1 -1
  110. package/dist/runtime/routes/auth-session.js +4 -2
  111. package/dist/runtime/routes/auth-session.js.map +1 -1
  112. package/dist/runtime/routes/collection-list.d.ts.map +1 -1
  113. package/dist/runtime/routes/collection-list.js +64 -41
  114. package/dist/runtime/routes/collection-list.js.map +1 -1
  115. package/dist/runtime/routes/entries.d.ts.map +1 -1
  116. package/dist/runtime/routes/entries.js +16 -6
  117. package/dist/runtime/routes/entries.js.map +1 -1
  118. package/dist/runtime/routes/history.d.ts.map +1 -1
  119. package/dist/runtime/routes/history.js +50 -22
  120. package/dist/runtime/routes/history.js.map +1 -1
  121. package/dist/runtime/routes/mutate.d.ts.map +1 -1
  122. package/dist/runtime/routes/mutate.js +40 -6
  123. package/dist/runtime/routes/mutate.js.map +1 -1
  124. package/dist/runtime/routes/publish.d.ts.map +1 -1
  125. package/dist/runtime/routes/publish.js +5 -2
  126. package/dist/runtime/routes/publish.js.map +1 -1
  127. package/dist/runtime/routes/schema.d.ts.map +1 -1
  128. package/dist/runtime/routes/schema.js +11 -3
  129. package/dist/runtime/routes/schema.js.map +1 -1
  130. package/dist/runtime/routes/studio-home.d.ts.map +1 -1
  131. package/dist/runtime/routes/studio-home.js +38 -19
  132. package/dist/runtime/routes/studio-home.js.map +1 -1
  133. package/dist/runtime/routes/theme-stylesheet.d.ts.map +1 -1
  134. package/dist/runtime/routes/theme-stylesheet.js +11 -1
  135. package/dist/runtime/routes/theme-stylesheet.js.map +1 -1
  136. package/dist/runtime/schema-registry.d.ts +8 -1
  137. package/dist/runtime/schema-registry.d.ts.map +1 -1
  138. package/dist/runtime/schema-registry.js +47 -3
  139. package/dist/runtime/schema-registry.js.map +1 -1
  140. package/dist/runtime/storage/atomic-write.d.ts.map +1 -1
  141. package/dist/runtime/storage/atomic-write.js +8 -3
  142. package/dist/runtime/storage/atomic-write.js.map +1 -1
  143. package/dist/runtime/storage/markdown-adapter.d.ts +19 -0
  144. package/dist/runtime/storage/markdown-adapter.d.ts.map +1 -1
  145. package/dist/runtime/storage/markdown-adapter.js +41 -0
  146. package/dist/runtime/storage/markdown-adapter.js.map +1 -1
  147. package/dist/runtime/storage/session-overlay-adapter.d.ts +8 -0
  148. package/dist/runtime/storage/session-overlay-adapter.d.ts.map +1 -1
  149. package/dist/runtime/storage/session-overlay-adapter.js +24 -2
  150. package/dist/runtime/storage/session-overlay-adapter.js.map +1 -1
  151. package/dist/runtime/themes/preset-tokens.js +4 -4
  152. package/dist/runtime/themes/preset-tokens.js.map +1 -1
  153. package/dist/runtime/utils.d.ts +10 -0
  154. package/dist/runtime/utils.d.ts.map +1 -1
  155. package/dist/runtime/utils.js +16 -0
  156. package/dist/runtime/utils.js.map +1 -1
  157. package/dist/runtime/views/studio-layout.d.ts +3 -0
  158. package/dist/runtime/views/studio-layout.d.ts.map +1 -1
  159. package/dist/runtime/views/studio-layout.js +36 -13
  160. package/dist/runtime/views/studio-layout.js.map +1 -1
  161. package/dist/schema-utils.d.ts +7 -0
  162. package/dist/schema-utils.d.ts.map +1 -1
  163. package/dist/schema-utils.js +143 -0
  164. package/dist/schema-utils.js.map +1 -1
  165. package/dist/types.d.ts +83 -1
  166. package/dist/types.d.ts.map +1 -1
  167. package/package.json +2 -2
  168. package/static/cms/admin-entry.css +135 -13
  169. package/static/cms/admin-entry.js +509 -86
  170. package/static/cms/editor/guards.js +1 -1
  171. package/static/cms/editor/image-edit.js +42 -11
  172. package/static/cms/editor/image.css +7 -1
  173. package/static/cms/editor/md-block-edit.js +176 -0
  174. package/static/cms/editor/md-serialize.js +301 -0
  175. package/static/cms/editor/panel.css +69 -25
  176. package/static/cms/editor/panel.js +58 -1
  177. package/static/cms/editor/rich-toolbar.js +4 -3
  178. package/static/cms/editor/sanitize.js +2 -1
  179. package/static/cms/editor/sync.js +277 -56
  180. package/static/cms/editor/text-edit.js +68 -17
  181. package/static/cms/editor/text-link-interactions.js +4 -4
  182. package/static/cms/editor/toast.css +114 -2
  183. package/static/cms/editor/toast.js +121 -22
  184. package/static/cms/editor/tokens-and-text.css +73 -3
  185. package/static/cms/editor/toolbar.css +19 -6
  186. package/static/cms/editor/toolbar.js +64 -23
  187. package/static/cms/editor.css +1 -1
  188. package/static/cms/editor.js +14 -3
  189. package/static/cms/studio-tokens.css +4 -4
  190. package/static/cms/studio.css +24 -0
@@ -50,18 +50,26 @@ function getToolbarNavLinks(pagePath) {
50
50
  }
51
51
 
52
52
  function renderDraftControls(staticDelivery) {
53
- if (!staticDelivery) return '';
54
-
53
+ // Static delivery is always drafting, so the controls stay visible. Server
54
+ // delivery saves field edits straight to the site, but inline body-block
55
+ // edits are ALWAYS drafts (they splice into source files only at publish), so
56
+ // the controls render here too — hidden until at least one draft is pending
57
+ // (toggled by refreshDraftControls) so the toolbar stays quiet otherwise.
58
+ const publishTitle = staticDelivery
59
+ ? 'Publish drafts; static visitors update after rebuild and deploy'
60
+ : 'Publish your body-block drafts into the source files';
55
61
  return `
56
- <button class="cms-publish-btn cms-go-live-btn" type="button" title="Push your draft changes to the live site">
57
- <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="M12 19V5"/><polyline points="5 12 12 5 19 12"/></svg>
58
- <span class="cms-go-live-label">Go live</span>
59
- </button>
60
- <button class="cms-discard-btn" type="button" title="Discard your draft changes">
61
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
62
- Discard
63
- </button>
64
- <div class="cms-toolbar-divider"></div>`;
62
+ <span class="cms-draft-controls"${staticDelivery ? '' : ' hidden'}>
63
+ <button class="cms-publish-btn cms-go-live-btn" type="button" title="${publishTitle}">
64
+ <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="M12 19V5"/><polyline points="5 12 12 5 19 12"/></svg>
65
+ <span class="cms-go-live-label">Publish</span>
66
+ </button>
67
+ <button class="cms-discard-btn" type="button" title="Discard your draft changes">
68
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
69
+ Discard
70
+ </button>
71
+ <div class="cms-toolbar-divider"></div>
72
+ </span>`;
65
73
  }
66
74
 
67
75
  function renderToolbar(navLinks, staticDelivery) {
@@ -74,7 +82,7 @@ function renderToolbar(navLinks, staticDelivery) {
74
82
 
75
83
  const badgeLabel = staticDelivery ? 'Draft' : 'Live';
76
84
  const badgeHint = staticDelivery
77
- ? 'Not on your site until you go live'
85
+ ? 'Draft preview; visitors update after publish, rebuild, and deploy'
78
86
  : 'Changes save directly to your site';
79
87
 
80
88
  const toolbar = document.createElement('div');
@@ -88,8 +96,8 @@ function renderToolbar(navLinks, staticDelivery) {
88
96
  <span class="cms-toolbar-badge-text">${badgeLabel}</span>
89
97
  </div>
90
98
  <div class="cms-toolbar-divider"></div>
91
- <div class="cms-status-group">
92
- <span class="cms-status-dot cms-status-idle"></span>
99
+ <div class="cms-status-group" role="status" aria-live="polite">
100
+ <span class="cms-status-dot cms-status-idle" aria-hidden="true"></span>
93
101
  <span class="cms-status-text">Ready</span>
94
102
  </div>
95
103
  </div>
@@ -168,16 +176,30 @@ export function mountToolbar({ showToast, clearDirty, onLogout }) {
168
176
  }
169
177
 
170
178
  async function publishDrafts() {
171
- setStatus('saving', 'Going live…');
179
+ setStatus('saving', 'Publishing…');
172
180
  try {
173
181
  const res = await draftRequest('POST', '/publish');
174
182
  if (!res.ok) throw new Error('publish failed');
175
183
  const data = await res.json();
176
184
  const n = Array.isArray(data.published) ? data.published.length : 0;
185
+ const conflicts = Array.isArray(data.conflicts) ? data.conflicts.length : 0;
186
+ if (conflicts > 0) {
187
+ // Stale body drafts: preserved server-side, NOT published. Reload so
188
+ // fresh stamps arrive and the editor can re-apply them.
189
+ setStatus('error', 'Some drafts conflicted');
190
+ showToast(
191
+ n > 0
192
+ ? `Published ${n} change(s); ${conflicts} draft(s) conflicted with newer content — reloading so you can re-apply.`
193
+ : `${conflicts} draft(s) conflicted with newer content — reloading so you can re-apply.`,
194
+ 'error',
195
+ );
196
+ window.location.reload();
197
+ return n > 0;
198
+ }
177
199
  if (data.rebuild?.triggered && data.rebuild.ok === false) {
178
- setStatus('error', 'Live, but deploy failed');
200
+ setStatus('error', 'Published, deploy failed');
179
201
  showToast(
180
- 'Changes are saved, but the deploy webhook failed. Check your CI settings.',
202
+ 'Changes are published, but the deploy webhook failed. Check your CI settings.',
181
203
  'error',
182
204
  );
183
205
  return false;
@@ -185,18 +207,23 @@ export function mountToolbar({ showToast, clearDirty, onLogout }) {
185
207
  if (data.rebuild?.triggered) {
186
208
  showToast(
187
209
  n > 0
188
- ? `Went live with ${n} change(s) — your site is rebuilding`
210
+ ? `Published ${n} change(s) — rebuild started`
189
211
  : 'Your site is rebuilding',
190
212
  'success',
191
213
  );
192
214
  } else {
193
- showToast(n > 0 ? `Went live with ${n} change(s)` : 'Already up to date', 'success');
215
+ showToast(
216
+ n > 0
217
+ ? `Published ${n} change(s) — rebuild and deploy to update visitors`
218
+ : 'Already up to date',
219
+ 'success',
220
+ );
194
221
  }
195
222
  window.location.reload();
196
223
  return true;
197
224
  } catch {
198
- setStatus('error', 'Go live failed');
199
- showToast('Go live failed', 'error');
225
+ setStatus('error', 'Publish failed');
226
+ showToast('Publish failed', 'error');
200
227
  return false;
201
228
  }
202
229
  }
@@ -206,7 +233,7 @@ export function mountToolbar({ showToast, clearDirty, onLogout }) {
206
233
  const count = await fetchDraftCount();
207
234
  if (count > 0) {
208
235
  const goLive = window.confirm(
209
- `You have ${count} unpublished change${count === 1 ? '' : 's'}. Go live before signing out?`,
236
+ `You have ${count} unpublished change${count === 1 ? '' : 's'}. Publish before signing out?`,
210
237
  );
211
238
  if (goLive) {
212
239
  await publishDrafts();
@@ -235,7 +262,7 @@ export function mountToolbar({ showToast, clearDirty, onLogout }) {
235
262
  if (
236
263
  !window.confirm(
237
264
  staticDelivery
238
- ? 'Push all your draft changes to the live site?'
265
+ ? 'Publish all draft changes? Static visitors update after rebuild and deploy.'
239
266
  : 'Publish all your draft changes to the live site?',
240
267
  )
241
268
  ) {
@@ -256,6 +283,20 @@ export function mountToolbar({ showToast, clearDirty, onLogout }) {
256
283
  }
257
284
  });
258
285
 
286
+ // Server delivery hides the draft controls until a body draft exists. Field
287
+ // edits save directly (no draft), so the count reflects pending body blocks.
288
+ // Refresh on mount and whenever a body block is drafted; publish/discard
289
+ // reload the page, so mount re-evaluates from scratch.
290
+ const draftControls = toolbar.querySelector('.cms-draft-controls');
291
+ async function refreshDraftControls() {
292
+ if (staticDelivery || !draftControls) return;
293
+ draftControls.hidden = (await fetchDraftCount()) === 0;
294
+ }
295
+ if (!staticDelivery) {
296
+ refreshDraftControls();
297
+ window.addEventListener('cms:draftSaved', refreshDraftControls);
298
+ }
299
+
259
300
  const studioButton = toolbar.querySelector('.cms-studio-btn');
260
301
  const mapButton = toolbar.querySelector('.cms-map-btn');
261
302
  return {
@@ -1,7 +1,7 @@
1
1
  /* ========================================================================
2
2
  CMS Editor Styles — only loaded when isEditor = true
3
3
  ======================================================================== */
4
- @import "./editor/tokens-and-text.css";
4
+ @import "./editor/tokens-and-text.css?v=studio-upstream-20260819";
5
5
  @import "./editor/image.css";
6
6
  @import "./editor/section-controls.css";
7
7
  @import "./editor/toolbar.css";
@@ -18,12 +18,13 @@ import { createToast } from './editor/toast.js';
18
18
  import { mountEditorGuards } from './editor/guards.js';
19
19
  import { mountToolbar, normalizePreviewForDelivery } from './editor/toolbar.js';
20
20
  import { mountStudioPanel } from './editor/panel.js';
21
- import { mountStudioSync } from './editor/sync.js';
21
+ import { mountStudioSync } from './editor/sync.js?v=studio-upstream-20260819';
22
22
  import { mountTextEditors } from './editor/text-edit.js';
23
23
  import { mountImageEditors } from './editor/image-edit.js';
24
24
  import { mountSectionControls } from './editor/section-controls.js';
25
25
  import { mountContentMap } from './editor/content-map.js';
26
26
  import { mountRichToolbar } from './editor/rich-toolbar.js';
27
+ import { mountMdBlockEditors } from './editor/md-block-edit.js';
27
28
  import { mountLinkFollowAffordances } from './editor/link-follow.js';
28
29
  import { revealAndFade } from './editor/highlight.js';
29
30
  import { hydrateStega } from './editor/stega-hydrate.js';
@@ -36,7 +37,10 @@ function boot() {
36
37
  if (normalizePreviewForDelivery()) return;
37
38
 
38
39
  const state = {
39
- dirtyEl: null,
40
+ // Every element with unsaved edits. A Set (not a single element) so a save
41
+ // completing on field A can't clear the dirty flag for field B the user has
42
+ // since moved to — that would let the beforeunload guard miss B's edits.
43
+ dirtyEls: new Set(),
40
44
  linkPopupEl: null,
41
45
  };
42
46
 
@@ -52,7 +56,7 @@ function boot() {
52
56
  const { studioButton, mapButton, setStatus } = mountToolbar({
53
57
  showToast,
54
58
  clearDirty: () => {
55
- state.dirtyEl = null;
59
+ state.dirtyEls.clear();
56
60
  },
57
61
  onLogout: async () => {
58
62
  await fetch(buildCmsUrl('/auth/logout'), { method: 'POST' });
@@ -94,6 +98,13 @@ function boot() {
94
98
  richToolbarMounted = true;
95
99
  }
96
100
 
101
+ mountMdBlockEditors({
102
+ state,
103
+ flash,
104
+ showToast,
105
+ onUnauthorized: redirectToEditorLogin,
106
+ });
107
+
97
108
  mountImageEditors({
98
109
  parseCaretAttr,
99
110
  flash,
@@ -30,15 +30,15 @@
30
30
  --color-theme-border: rgba(255, 255, 255, 0.10);
31
31
 
32
32
  /* ─── Semantic: Accent ────────────────────────────────────────────── */
33
- --color-theme-accent: #3b82f6;
33
+ --color-theme-accent: #60a5fa;
34
34
  --color-theme-accent-soft: rgba(59, 130, 246, 0.12);
35
35
  --color-theme-accent-strong: rgba(59, 130, 246, 0.24);
36
- --color-theme-on-accent: #ffffff;
36
+ --color-theme-on-accent: #07111f;
37
37
 
38
38
  /* ─── Semantic: Text ──────────────────────────────────────────────── */
39
39
  --color-theme-text: rgba(255, 255, 255, 0.92);
40
- --color-theme-text-muted: rgba(255, 255, 255, 0.55);
41
- --color-theme-text-dim: rgba(255, 255, 255, 0.32);
40
+ --color-theme-text-muted: #b6bac3;
41
+ --color-theme-text-dim: #9298a3;
42
42
 
43
43
  /* ─── Semantic: State ─────────────────────────────────────────────── */
44
44
  --color-theme-success: #22c55e;
@@ -29,6 +29,18 @@ html, body {
29
29
  box-sizing: border-box;
30
30
  }
31
31
 
32
+ .studio-sr-only {
33
+ position: absolute;
34
+ width: 1px;
35
+ height: 1px;
36
+ padding: 0;
37
+ margin: -1px;
38
+ overflow: hidden;
39
+ clip: rect(0, 0, 0, 0);
40
+ white-space: nowrap;
41
+ border: 0;
42
+ }
43
+
32
44
  a {
33
45
  color: inherit;
34
46
  text-decoration: none;
@@ -47,9 +59,11 @@ a {
47
59
  /* ─── Layout: Shell + Header ────────────────────────────────────────── */
48
60
 
49
61
  .studio-shell {
62
+ height: 100dvh;
50
63
  min-height: 100vh;
51
64
  display: flex;
52
65
  flex-direction: column;
66
+ overflow: hidden;
53
67
  }
54
68
 
55
69
  .studio-header {
@@ -87,6 +101,15 @@ a {
87
101
  flex-shrink: 0;
88
102
  }
89
103
 
104
+ .studio-editor-identity {
105
+ max-width: 14rem;
106
+ overflow: hidden;
107
+ text-overflow: ellipsis;
108
+ white-space: nowrap;
109
+ color: var(--studio-text-muted);
110
+ font-size: 0.72rem;
111
+ }
112
+
90
113
  .studio-brand {
91
114
  display: flex;
92
115
  align-items: center;
@@ -113,6 +136,7 @@ a {
113
136
 
114
137
  .studio-main {
115
138
  flex: 1;
139
+ min-height: 0;
116
140
  padding: var(--space-theme-xl) var(--space-theme-lg);
117
141
  overflow-y: auto;
118
142
  }