@caretcms/core 0.1.1 → 0.1.2

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 (122) hide show
  1. package/README.md +82 -17
  2. package/dist/browser-runtime.d.ts +16 -0
  3. package/dist/browser-runtime.d.ts.map +1 -1
  4. package/dist/browser-runtime.js +8 -3
  5. package/dist/browser-runtime.js.map +1 -1
  6. package/dist/index.d.ts +23 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +141 -3
  9. package/dist/index.js.map +1 -1
  10. package/dist/runtime/providers.d.ts +9 -0
  11. package/dist/runtime/providers.d.ts.map +1 -1
  12. package/dist/runtime/providers.js +6 -0
  13. package/dist/runtime/providers.js.map +1 -1
  14. package/dist/runtime/publish.d.ts +2 -0
  15. package/dist/runtime/publish.d.ts.map +1 -1
  16. package/dist/runtime/publish.js +4 -0
  17. package/dist/runtime/publish.js.map +1 -1
  18. package/dist/runtime/rebuild-webhook.d.ts +29 -0
  19. package/dist/runtime/rebuild-webhook.d.ts.map +1 -0
  20. package/dist/runtime/rebuild-webhook.js +37 -0
  21. package/dist/runtime/rebuild-webhook.js.map +1 -0
  22. package/dist/runtime/rewrite.d.ts +18 -0
  23. package/dist/runtime/rewrite.d.ts.map +1 -1
  24. package/dist/runtime/rewrite.js +9 -1
  25. package/dist/runtime/rewrite.js.map +1 -1
  26. package/dist/runtime/rich-allowlist.d.ts +3 -2
  27. package/dist/runtime/rich-allowlist.d.ts.map +1 -1
  28. package/dist/runtime/rich-allowlist.js +9 -3
  29. package/dist/runtime/rich-allowlist.js.map +1 -1
  30. package/dist/runtime/routes/_helpers.d.ts +7 -0
  31. package/dist/runtime/routes/_helpers.d.ts.map +1 -1
  32. package/dist/runtime/routes/_helpers.js +34 -1
  33. package/dist/runtime/routes/_helpers.js.map +1 -1
  34. package/dist/runtime/routes/admin-entry.d.ts +1 -0
  35. package/dist/runtime/routes/admin-entry.d.ts.map +1 -1
  36. package/dist/runtime/routes/admin-entry.js +3 -2
  37. package/dist/runtime/routes/admin-entry.js.map +1 -1
  38. package/dist/runtime/routes/admin.d.ts +1 -0
  39. package/dist/runtime/routes/admin.d.ts.map +1 -1
  40. package/dist/runtime/routes/admin.js +15 -12
  41. package/dist/runtime/routes/admin.js.map +1 -1
  42. package/dist/runtime/routes/auth-login.d.ts +1 -0
  43. package/dist/runtime/routes/auth-login.d.ts.map +1 -1
  44. package/dist/runtime/routes/auth-login.js +1 -0
  45. package/dist/runtime/routes/auth-login.js.map +1 -1
  46. package/dist/runtime/routes/auth-logout.d.ts +1 -0
  47. package/dist/runtime/routes/auth-logout.d.ts.map +1 -1
  48. package/dist/runtime/routes/auth-logout.js +1 -0
  49. package/dist/runtime/routes/auth-logout.js.map +1 -1
  50. package/dist/runtime/routes/auth-session.d.ts +1 -0
  51. package/dist/runtime/routes/auth-session.d.ts.map +1 -1
  52. package/dist/runtime/routes/auth-session.js +1 -0
  53. package/dist/runtime/routes/auth-session.js.map +1 -1
  54. package/dist/runtime/routes/collection-list.d.ts +1 -0
  55. package/dist/runtime/routes/collection-list.d.ts.map +1 -1
  56. package/dist/runtime/routes/collection-list.js +3 -2
  57. package/dist/runtime/routes/collection-list.js.map +1 -1
  58. package/dist/runtime/routes/collections-metadata.d.ts +1 -0
  59. package/dist/runtime/routes/collections-metadata.d.ts.map +1 -1
  60. package/dist/runtime/routes/collections-metadata.js +3 -2
  61. package/dist/runtime/routes/collections-metadata.js.map +1 -1
  62. package/dist/runtime/routes/draft.d.ts +5 -0
  63. package/dist/runtime/routes/draft.d.ts.map +1 -1
  64. package/dist/runtime/routes/draft.js +18 -1
  65. package/dist/runtime/routes/draft.js.map +1 -1
  66. package/dist/runtime/routes/editor-assets.d.ts +1 -0
  67. package/dist/runtime/routes/editor-assets.d.ts.map +1 -1
  68. package/dist/runtime/routes/editor-assets.js +1 -0
  69. package/dist/runtime/routes/editor-assets.js.map +1 -1
  70. package/dist/runtime/routes/entries.d.ts +1 -0
  71. package/dist/runtime/routes/entries.d.ts.map +1 -1
  72. package/dist/runtime/routes/entries.js +3 -2
  73. package/dist/runtime/routes/entries.js.map +1 -1
  74. package/dist/runtime/routes/history.d.ts +1 -0
  75. package/dist/runtime/routes/history.d.ts.map +1 -1
  76. package/dist/runtime/routes/history.js +4 -3
  77. package/dist/runtime/routes/history.js.map +1 -1
  78. package/dist/runtime/routes/mutate.d.ts +1 -0
  79. package/dist/runtime/routes/mutate.d.ts.map +1 -1
  80. package/dist/runtime/routes/mutate.js +3 -2
  81. package/dist/runtime/routes/mutate.js.map +1 -1
  82. package/dist/runtime/routes/publish.d.ts +1 -0
  83. package/dist/runtime/routes/publish.d.ts.map +1 -1
  84. package/dist/runtime/routes/publish.js +11 -2
  85. package/dist/runtime/routes/publish.js.map +1 -1
  86. package/dist/runtime/routes/schema.d.ts +1 -0
  87. package/dist/runtime/routes/schema.d.ts.map +1 -1
  88. package/dist/runtime/routes/schema.js +3 -2
  89. package/dist/runtime/routes/schema.js.map +1 -1
  90. package/dist/runtime/routes/studio-home.d.ts +1 -0
  91. package/dist/runtime/routes/studio-home.d.ts.map +1 -1
  92. package/dist/runtime/routes/studio-home.js +10 -9
  93. package/dist/runtime/routes/studio-home.js.map +1 -1
  94. package/dist/runtime/routes/theme-stylesheet.d.ts +1 -0
  95. package/dist/runtime/routes/theme-stylesheet.d.ts.map +1 -1
  96. package/dist/runtime/routes/theme-stylesheet.js +1 -0
  97. package/dist/runtime/routes/theme-stylesheet.js.map +1 -1
  98. package/dist/runtime/routes/upload.d.ts +1 -0
  99. package/dist/runtime/routes/upload.d.ts.map +1 -1
  100. package/dist/runtime/routes/upload.js +1 -0
  101. package/dist/runtime/routes/upload.js.map +1 -1
  102. package/dist/runtime/static-bake.d.ts +20 -0
  103. package/dist/runtime/static-bake.d.ts.map +1 -0
  104. package/dist/runtime/static-bake.js +58 -0
  105. package/dist/runtime/static-bake.js.map +1 -0
  106. package/dist/runtime/storage/frontmatter-codec.d.ts.map +1 -1
  107. package/dist/runtime/storage/frontmatter-codec.js +8 -0
  108. package/dist/runtime/storage/frontmatter-codec.js.map +1 -1
  109. package/dist/runtime/views/studio-layout.d.ts.map +1 -1
  110. package/dist/runtime/views/studio-layout.js +11 -10
  111. package/dist/runtime/views/studio-layout.js.map +1 -1
  112. package/package.json +1 -1
  113. package/static/cms/dev-toolbar/app.js +3 -1
  114. package/static/cms/editor/config.js +10 -0
  115. package/static/cms/editor/helpers.js +5 -1
  116. package/static/cms/editor/highlight.js +4 -6
  117. package/static/cms/editor/sanitize.js +9 -5
  118. package/static/cms/editor/toolbar.css +60 -5
  119. package/static/cms/editor/toolbar.js +133 -65
  120. package/static/cms/editor.js +3 -1
  121. package/static/cms/studio-login.css +70 -0
  122. package/static/cms/studio.css +80 -13
@@ -1,5 +1,5 @@
1
1
  import { toggleHighlight } from './highlight.js';
2
- import { buildCmsUrl } from './config.js';
2
+ import { buildCmsUrl, isStaticDelivery } from './config.js';
3
3
 
4
4
  const PREVIEW_COOKIE = 'caret_preview';
5
5
 
@@ -13,6 +13,26 @@ function setPreviewCookie(on) {
13
13
  : `${PREVIEW_COOKIE}=; path=/; Max-Age=0; SameSite=Lax`;
14
14
  }
15
15
 
16
+ /** Static delivery always drafts — turn preview on once so saves stay unpublished. */
17
+ export function ensureStaticPreviewMode() {
18
+ if (!isStaticDelivery()) return false;
19
+ if (previewActive()) return false;
20
+ setPreviewCookie(true);
21
+ window.location.reload();
22
+ return true;
23
+ }
24
+
25
+ /** Align preview cookie with delivery mode (static on, server off). */
26
+ export function normalizePreviewForDelivery() {
27
+ if (isStaticDelivery()) return ensureStaticPreviewMode();
28
+ if (previewActive()) {
29
+ setPreviewCookie(false);
30
+ window.location.reload();
31
+ return true;
32
+ }
33
+ return false;
34
+ }
35
+
16
36
  function getToolbarNavLinks(pagePath) {
17
37
  const links = [];
18
38
  const headerNav = document.querySelector('#main-header nav');
@@ -29,7 +49,22 @@ function getToolbarNavLinks(pagePath) {
29
49
  return links;
30
50
  }
31
51
 
32
- function renderToolbar(navLinks) {
52
+ function renderDraftControls(staticDelivery) {
53
+ if (!staticDelivery) return '';
54
+
55
+ 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>`;
65
+ }
66
+
67
+ function renderToolbar(navLinks, staticDelivery) {
33
68
  const navLinksHtml = navLinks
34
69
  .map(
35
70
  ({ href, label, isActive }) =>
@@ -37,14 +72,20 @@ function renderToolbar(navLinks) {
37
72
  )
38
73
  .join('');
39
74
 
75
+ const badgeLabel = staticDelivery ? 'Draft' : 'Live';
76
+ const badgeHint = staticDelivery
77
+ ? 'Not on your site until you go live'
78
+ : 'Changes save directly to your site';
79
+
40
80
  const toolbar = document.createElement('div');
41
81
  toolbar.className = 'cms-toolbar';
82
+ toolbar.dataset.delivery = staticDelivery ? 'static' : 'server';
42
83
  toolbar.innerHTML = `
43
84
  <div class="cms-toolbar-inner">
44
85
  <div class="cms-toolbar-left">
45
- <div class="cms-toolbar-badge">
86
+ <div class="cms-toolbar-badge" title="${badgeHint}">
46
87
  <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 9l10 13 10-13L12 2zm0 3.84L18.26 9 12 17.65 5.74 9 12 5.84z"/></svg>
47
- <span class="cms-toolbar-badge-text">Editor</span>
88
+ <span class="cms-toolbar-badge-text">${badgeLabel}</span>
48
89
  </div>
49
90
  <div class="cms-toolbar-divider"></div>
50
91
  <div class="cms-status-group">
@@ -54,19 +95,7 @@ function renderToolbar(navLinks) {
54
95
  </div>
55
96
  ${navLinksHtml ? `<div class="cms-toolbar-nav">${navLinksHtml}</div>` : ''}
56
97
  <div class="cms-toolbar-right">
57
- <button class="cms-preview-btn" type="button" title="Toggle draft preview — edit without publishing to the live site">
58
- <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="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
59
- Preview
60
- </button>
61
- <button class="cms-publish-btn" type="button" title="Publish your draft changes to the live site" hidden>
62
- <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>
63
- Publish
64
- </button>
65
- <button class="cms-discard-btn" type="button" title="Discard your draft changes" hidden>
66
- <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>
67
- Discard
68
- </button>
69
- <div class="cms-toolbar-divider"></div>
98
+ ${renderDraftControls(staticDelivery)}
70
99
  <button type="button" class="cms-studio-btn" title="Toggle Content Studio panel">
71
100
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="9" y1="9" x2="21" y2="9"/></svg>
72
101
  Studio
@@ -79,12 +108,12 @@ function renderToolbar(navLinks) {
79
108
  <div class="cms-toolbar-divider"></div>
80
109
  <button class="cms-highlight-btn" type="button" title="Show all editable regions">
81
110
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
82
- Show All
111
+ <span class="cms-highlight-label">Show all</span>
83
112
  </button>
84
113
  <div class="cms-toolbar-divider"></div>
85
- <button class="cms-exit-btn" type="button">
114
+ <button class="cms-exit-btn" type="button" title="Sign out">
86
115
  <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="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
87
- Log Out
116
+ Sign out
88
117
  </button>
89
118
  </div>
90
119
  </div>
@@ -94,8 +123,9 @@ function renderToolbar(navLinks) {
94
123
  }
95
124
 
96
125
  export function mountToolbar({ showToast, clearDirty, onLogout }) {
126
+ const staticDelivery = isStaticDelivery();
97
127
  const navLinks = getToolbarNavLinks(window.location.pathname);
98
- const toolbar = renderToolbar(navLinks);
128
+ const toolbar = renderToolbar(navLinks, staticDelivery);
99
129
 
100
130
  const statusDot = toolbar.querySelector('.cms-status-dot');
101
131
  const statusText = toolbar.querySelector('.cms-status-text');
@@ -114,40 +144,6 @@ export function mountToolbar({ showToast, clearDirty, onLogout }) {
114
144
  toggleHighlight(highlightBtn, showToast),
115
145
  );
116
146
 
117
- toolbar.querySelector('.cms-exit-btn')?.addEventListener('click', async () => {
118
- clearDirty();
119
- await onLogout();
120
- });
121
-
122
- // --- Draft preview + publish/discard ---
123
- const previewBtn = toolbar.querySelector('.cms-preview-btn');
124
- const publishBtn = toolbar.querySelector('.cms-publish-btn');
125
- const discardBtn = toolbar.querySelector('.cms-discard-btn');
126
- const badgeText = toolbar.querySelector('.cms-toolbar-badge-text');
127
-
128
- function reflectPreview() {
129
- const on = previewActive();
130
- previewBtn?.classList.toggle('cms-preview-btn-active', on);
131
- if (publishBtn) publishBtn.hidden = !on;
132
- if (discardBtn) discardBtn.hidden = !on;
133
- if (badgeText) badgeText.textContent = on ? 'Draft' : 'Editor';
134
- const label = previewBtn
135
- && Array.from(previewBtn.childNodes).find((n) => n.nodeType === 3 && n.textContent?.trim());
136
- if (label) label.textContent = on ? ' Previewing' : ' Preview';
137
- }
138
- reflectPreview();
139
-
140
- previewBtn?.addEventListener('click', () => {
141
- const turningOn = !previewActive();
142
- setPreviewCookie(turningOn);
143
- showToast(
144
- turningOn ? 'Draft preview on — your edits stay unpublished' : 'Draft preview off',
145
- 'success',
146
- );
147
- // Reload so the server installs (or drops) the per-editor draft overlay.
148
- window.location.reload();
149
- });
150
-
151
147
  async function draftRequest(method, path) {
152
148
  return fetch(buildCmsUrl(path), {
153
149
  method,
@@ -157,30 +153,102 @@ export function mountToolbar({ showToast, clearDirty, onLogout }) {
157
153
  });
158
154
  }
159
155
 
160
- publishBtn?.addEventListener('click', async () => {
161
- if (!window.confirm('Publish all your draft changes to the live site?')) return;
162
- setStatus('saving', 'Publishing…');
156
+ async function fetchDraftCount() {
157
+ try {
158
+ const res = await fetch(buildCmsUrl('/draft'), {
159
+ credentials: 'same-origin',
160
+ headers: { 'x-caret-request': '1' },
161
+ });
162
+ if (!res.ok) return 0;
163
+ const data = await res.json();
164
+ return typeof data.count === 'number' ? data.count : 0;
165
+ } catch {
166
+ return 0;
167
+ }
168
+ }
169
+
170
+ async function publishDrafts() {
171
+ setStatus('saving', 'Going live…');
163
172
  try {
164
173
  const res = await draftRequest('POST', '/publish');
165
174
  if (!res.ok) throw new Error('publish failed');
166
175
  const data = await res.json();
167
176
  const n = Array.isArray(data.published) ? data.published.length : 0;
168
- // Drafts are flushed; leave preview so the editor sees the published site.
169
- setPreviewCookie(false);
170
- showToast(`Published ${n} change(s)`, 'success');
177
+ if (data.rebuild?.triggered && data.rebuild.ok === false) {
178
+ setStatus('error', 'Live, but deploy failed');
179
+ showToast(
180
+ 'Changes are saved, but the deploy webhook failed. Check your CI settings.',
181
+ 'error',
182
+ );
183
+ return false;
184
+ }
185
+ if (data.rebuild?.triggered) {
186
+ showToast(
187
+ n > 0
188
+ ? `Went live with ${n} change(s) — your site is rebuilding`
189
+ : 'Your site is rebuilding',
190
+ 'success',
191
+ );
192
+ } else {
193
+ showToast(n > 0 ? `Went live with ${n} change(s)` : 'Already up to date', 'success');
194
+ }
171
195
  window.location.reload();
196
+ return true;
172
197
  } catch {
173
- setStatus('error', 'Publish failed');
174
- showToast('Publish failed', 'error');
198
+ setStatus('error', 'Go live failed');
199
+ showToast('Go live failed', 'error');
200
+ return false;
201
+ }
202
+ }
203
+
204
+ toolbar.querySelector('.cms-exit-btn')?.addEventListener('click', async () => {
205
+ if (staticDelivery) {
206
+ const count = await fetchDraftCount();
207
+ if (count > 0) {
208
+ const goLive = window.confirm(
209
+ `You have ${count} unpublished change${count === 1 ? '' : 's'}. Go live before signing out?`,
210
+ );
211
+ if (goLive) {
212
+ await publishDrafts();
213
+ return;
214
+ }
215
+ const discard = window.confirm(
216
+ 'Discard unpublished changes and sign out?',
217
+ );
218
+ if (!discard) return;
219
+ try {
220
+ const res = await draftRequest('DELETE', '/draft');
221
+ if (!res.ok) throw new Error('discard failed');
222
+ } catch {
223
+ showToast('Could not discard drafts', 'error');
224
+ return;
225
+ }
226
+ }
227
+ }
228
+
229
+ clearDirty();
230
+ await onLogout();
231
+ });
232
+
233
+ const publishBtn = toolbar.querySelector('.cms-publish-btn');
234
+ publishBtn?.addEventListener('click', async () => {
235
+ if (
236
+ !window.confirm(
237
+ staticDelivery
238
+ ? 'Push all your draft changes to the live site?'
239
+ : 'Publish all your draft changes to the live site?',
240
+ )
241
+ ) {
242
+ return;
175
243
  }
244
+ await publishDrafts();
176
245
  });
177
246
 
178
- discardBtn?.addEventListener('click', async () => {
247
+ toolbar.querySelector('.cms-discard-btn')?.addEventListener('click', async () => {
179
248
  if (!window.confirm('Discard all your draft changes? This cannot be undone.')) return;
180
249
  try {
181
250
  const res = await draftRequest('DELETE', '/draft');
182
251
  if (!res.ok) throw new Error('discard failed');
183
- setPreviewCookie(false);
184
252
  showToast('Draft discarded', 'success');
185
253
  window.location.reload();
186
254
  } catch {
@@ -16,7 +16,7 @@ import { createSaveField } from './editor/save-queue.js';
16
16
  import { compressImage, updateEmblaCarousel } from './editor/image-utils.js';
17
17
  import { createToast } from './editor/toast.js';
18
18
  import { mountEditorGuards } from './editor/guards.js';
19
- import { mountToolbar } from './editor/toolbar.js';
19
+ import { mountToolbar, normalizePreviewForDelivery } from './editor/toolbar.js';
20
20
  import { mountStudioPanel } from './editor/panel.js';
21
21
  import { mountStudioSync } from './editor/sync.js';
22
22
  import { mountTextEditors } from './editor/text-edit.js';
@@ -33,6 +33,8 @@ function redirectToEditorLogin() {
33
33
  }
34
34
 
35
35
  function boot() {
36
+ if (normalizePreviewForDelivery()) return;
37
+
36
38
  const state = {
37
39
  dirtyEl: null,
38
40
  linkPopupEl: null,
@@ -298,3 +298,73 @@
298
298
  }
299
299
 
300
300
  .studio-login-footer a:hover { opacity: 0.7; }
301
+
302
+ /* Setup helper (no password / dev password) */
303
+ .studio-login-setup {
304
+ margin-top: 1.25rem;
305
+ padding: 1rem 1.1rem;
306
+ border: 1px solid rgba(127, 127, 127, 0.28);
307
+ border-radius: 10px;
308
+ text-align: left;
309
+ font-size: 0.85rem;
310
+ line-height: 1.55;
311
+ }
312
+
313
+ .studio-login-setup--dev {
314
+ margin-top: 1rem;
315
+ padding: 0.8rem 1rem;
316
+ border-style: dashed;
317
+ font-size: 0.82rem;
318
+ }
319
+
320
+ .studio-login-setup-title {
321
+ margin: 0 0 0.6rem;
322
+ font-weight: 600;
323
+ }
324
+
325
+ .studio-login-setup ol {
326
+ margin: 0;
327
+ padding-left: 1.2rem;
328
+ display: grid;
329
+ gap: 0.45rem;
330
+ }
331
+
332
+ .studio-login-setup-snippet-row {
333
+ display: flex;
334
+ gap: 0.4rem;
335
+ align-items: center;
336
+ margin-top: 0.35rem;
337
+ }
338
+
339
+ .studio-login-setup-snippet {
340
+ flex: 1;
341
+ padding: 0.4rem 0.55rem;
342
+ border-radius: 6px;
343
+ background: rgba(0, 0, 0, 0.28);
344
+ overflow: auto;
345
+ white-space: nowrap;
346
+ font-family: var(--studio-font-mono);
347
+ font-size: 0.85em;
348
+ }
349
+
350
+ .studio-login-setup-copy {
351
+ appearance: none;
352
+ cursor: pointer;
353
+ border: 1px solid currentColor;
354
+ background: transparent;
355
+ color: inherit;
356
+ border-radius: 6px;
357
+ padding: 0.4rem 0.65rem;
358
+ font-family: var(--studio-font);
359
+ font-size: 0.85em;
360
+ font-weight: 500;
361
+ line-height: 1.2;
362
+ flex-shrink: 0;
363
+ }
364
+
365
+ .studio-login-setup-copy:hover { opacity: 0.85; }
366
+
367
+ .studio-login-setup-link {
368
+ display: inline-block;
369
+ margin-top: 0.7rem;
370
+ }
@@ -55,7 +55,6 @@ a {
55
55
  .studio-header {
56
56
  display: flex;
57
57
  align-items: center;
58
- justify-content: space-between;
59
58
  gap: var(--space-theme-md);
60
59
  padding: var(--space-theme-md) var(--space-theme-lg);
61
60
  border-bottom: 1px solid var(--studio-border);
@@ -65,6 +64,29 @@ a {
65
64
  z-index: 10;
66
65
  }
67
66
 
67
+ .studio-breadcrumb {
68
+ flex: 1;
69
+ min-width: 0;
70
+ display: flex;
71
+ align-items: center;
72
+ gap: var(--space-theme-sm);
73
+ font-family: var(--studio-font);
74
+ font-size: 11px;
75
+ font-weight: 500;
76
+ line-height: 1.2;
77
+ letter-spacing: 0.08em;
78
+ text-transform: uppercase;
79
+ color: var(--studio-text-dim);
80
+ }
81
+
82
+ .studio-header-nav {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: var(--space-theme-xs);
86
+ margin-left: auto;
87
+ flex-shrink: 0;
88
+ }
89
+
68
90
  .studio-brand {
69
91
  display: flex;
70
92
  align-items: center;
@@ -73,24 +95,18 @@ a {
73
95
  font-size: 1rem;
74
96
  letter-spacing: 0.02em;
75
97
  color: var(--studio-text);
98
+ transition: color 0.15s;
99
+ flex-shrink: 0;
76
100
  }
77
101
 
102
+ .studio-brand:hover { color: var(--studio-text-muted); }
103
+
78
104
  .studio-brand-mark {
79
105
  width: 24px;
80
106
  height: 24px;
81
107
  color: var(--studio-accent);
82
108
  }
83
109
 
84
- .studio-breadcrumb {
85
- display: flex;
86
- align-items: center;
87
- gap: var(--space-theme-sm);
88
- font-size: 11px;
89
- letter-spacing: 0.08em;
90
- text-transform: uppercase;
91
- color: var(--studio-text-dim);
92
- }
93
-
94
110
  .studio-breadcrumb a:hover { color: var(--studio-text-muted); }
95
111
  .studio-breadcrumb-sep { opacity: 0.4; }
96
112
  .studio-breadcrumb-current { color: var(--studio-text-muted); }
@@ -101,6 +117,37 @@ a {
101
117
  overflow-y: auto;
102
118
  }
103
119
 
120
+ .studio-home {
121
+ max-width: 64rem;
122
+ margin: 0 auto;
123
+ }
124
+
125
+ .studio-page-intro {
126
+ text-align: center;
127
+ margin: 0 0 2.5rem;
128
+ font-size: 0.85rem;
129
+ color: var(--studio-text-dim);
130
+ }
131
+
132
+ .studio-home-grid {
133
+ display: grid;
134
+ gap: 1rem;
135
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
136
+ }
137
+
138
+ @media (max-width: 640px) {
139
+ .studio-header {
140
+ flex-wrap: wrap;
141
+ padding: var(--space-theme-sm) var(--space-theme-md);
142
+ }
143
+
144
+ .studio-breadcrumb {
145
+ order: 3;
146
+ flex-basis: 100%;
147
+ padding-top: var(--space-theme-xs);
148
+ }
149
+ }
150
+
104
151
  /* ─── Drop zone ─────────────────────────────────────────────────────── */
105
152
 
106
153
  .studio-dropzone {
@@ -259,9 +306,14 @@ textarea.studio-input {
259
306
  /* ─── Header nav links ──────────────────────────────────────────────── */
260
307
 
261
308
  .studio-nav-link {
309
+ display: inline-flex;
310
+ align-items: center;
262
311
  padding: 4px 12px;
263
- font-size: 10px;
264
- letter-spacing: 0.1em;
312
+ font-family: var(--studio-font);
313
+ font-size: 11px;
314
+ font-weight: 500;
315
+ line-height: 1.2;
316
+ letter-spacing: 0.08em;
265
317
  text-transform: uppercase;
266
318
  text-decoration: none;
267
319
  color: var(--studio-text-dim);
@@ -269,6 +321,21 @@ textarea.studio-input {
269
321
  border-radius: var(--radius-theme-xs);
270
322
  }
271
323
 
324
+ button.studio-nav-link {
325
+ appearance: none;
326
+ border: none;
327
+ cursor: pointer;
328
+ background: transparent;
329
+ margin: 0;
330
+ font-family: var(--studio-font);
331
+ font-size: 11px;
332
+ font-weight: 500;
333
+ line-height: 1.2;
334
+ letter-spacing: 0.08em;
335
+ text-transform: uppercase;
336
+ color: var(--studio-text-dim);
337
+ }
338
+
272
339
  .studio-nav-link:hover {
273
340
  color: var(--studio-text-muted);
274
341
  background: var(--color-theme-surface-muted);