tfrere HF Staff Cursor commited on
Commit
44626d0
·
1 Parent(s): df28ae4

feat(publisher): editor quick-link for signed-in editors

Browse files

The published article and the editor share the same Space, but a
visitor who already has edit rights had no way to jump from one
to the other without retyping \`/editor\` in the URL bar. Add a
small pencil icon top-right, mirroring the existing theme toggle
on the top-left, that links to /editor.

Visibility:
- Hidden by default (\`<a id="edit-link" ... hidden>\`) so non-
editors / anonymous visitors never see a flash.
- An inline script probes \`/api/auth/status\` on load and removes
the \`hidden\` attribute when \`canEdit\` is true. Fetch failures
(offline preview, exported HTML opened from disk, etc.) are
silently swallowed - the button just stays hidden.
- Hidden under 1100px (same breakpoint as the theme toggle) so
mobile / tablet layouts aren't cluttered.

Styling: matches the theme toggle (40px circle, page-bg fill,
1px border, soft shadow) so the page stays balanced visually.
\`mix(--primary-color)\` tint on hover hints at the destination.

Snapshot updated to reflect the new markup + script block.

Co-authored-by: Cursor <cursoragent@cursor.com>

backend/src/publisher/html-renderer.ts CHANGED
@@ -197,6 +197,19 @@ ${renderPrimaryColorOverride(meta.primaryHue)}
197
  </span>
198
  </button>
199
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  <!-- Mobile TOC toggle -->
201
  <button class="toc-mobile-toggle" aria-label="Open table of contents" aria-expanded="false">
202
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -698,6 +711,30 @@ ${renderPrimaryColorOverride(meta.primaryHue)}
698
  if (h) { var t = document.querySelector(h); if (t) t.scrollIntoView({block:'start'}); }
699
  else window.scrollTo({top:0});
700
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
701
  })();
702
  </script>
703
  </body>
 
197
  </span>
198
  </button>
199
 
200
+ <!--
201
+ Quick link to the editor, shown only for visitors whose
202
+ /api/auth/status response carries canEdit=true. Hidden by
203
+ default so non-editors never see a flash. Same shape as the
204
+ theme toggle, mirrored to the right edge of the viewport.
205
+ -->
206
+ <a id="edit-link" href="/editor" aria-label="Open the editor" title="Open the editor" hidden>
207
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
208
+ <path d="M12 20h9"/>
209
+ <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
210
+ </svg>
211
+ </a>
212
+
213
  <!-- Mobile TOC toggle -->
214
  <button class="toc-mobile-toggle" aria-label="Open table of contents" aria-expanded="false">
215
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
 
711
  if (h) { var t = document.querySelector(h); if (t) t.scrollIntoView({block:'start'}); }
712
  else window.scrollTo({top:0});
713
  });
714
+
715
+ // ----------------------------------------------------------------
716
+ // Editor quick-link
717
+ // ----------------------------------------------------------------
718
+ // The published page lives on the same Space as the editor, so we
719
+ // can probe /api/auth/status to know if the current visitor has
720
+ // edit rights. When they do, surface a small "open editor" pencil
721
+ // top-right; otherwise the button stays \`hidden\` and is removed
722
+ // on the next layout pass (no flicker for read-only visitors).
723
+ //
724
+ // A network failure (offline preview, exported HTML opened from
725
+ // disk, ...) is silently ignored - the button just stays hidden.
726
+ (function() {
727
+ var link = document.getElementById('edit-link');
728
+ if (!link) return;
729
+ fetch('/api/auth/status', { credentials: 'same-origin' })
730
+ .then(function(r) { return r.ok ? r.json() : null; })
731
+ .then(function(data) {
732
+ if (data && data.canEdit) {
733
+ link.removeAttribute('hidden');
734
+ }
735
+ })
736
+ .catch(function() {});
737
+ })();
738
  })();
739
  </script>
740
  </body>
backend/tests/__snapshots__/html-renderer-snapshot.test.ts.snap CHANGED
@@ -125,6 +125,19 @@ exports[`snapshot - full render > matches snapshot for a typical article 1`] = `
125
  </span>
126
  </button>
127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  <!-- Mobile TOC toggle -->
129
  <button class="toc-mobile-toggle" aria-label="Open table of contents" aria-expanded="false">
130
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -641,6 +654,30 @@ exports[`snapshot - full render > matches snapshot for a typical article 1`] = `
641
  if (h) { var t = document.querySelector(h); if (t) t.scrollIntoView({block:'start'}); }
642
  else window.scrollTo({top:0});
643
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
644
  })();
645
  </script>
646
  </body>
 
125
  </span>
126
  </button>
127
 
128
+ <!--
129
+ Quick link to the editor, shown only for visitors whose
130
+ /api/auth/status response carries canEdit=true. Hidden by
131
+ default so non-editors never see a flash. Same shape as the
132
+ theme toggle, mirrored to the right edge of the viewport.
133
+ -->
134
+ <a id="edit-link" href="/editor" aria-label="Open the editor" title="Open the editor" hidden>
135
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
136
+ <path d="M12 20h9"/>
137
+ <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
138
+ </svg>
139
+ </a>
140
+
141
  <!-- Mobile TOC toggle -->
142
  <button class="toc-mobile-toggle" aria-label="Open table of contents" aria-expanded="false">
143
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
 
654
  if (h) { var t = document.querySelector(h); if (t) t.scrollIntoView({block:'start'}); }
655
  else window.scrollTo({top:0});
656
  });
657
+
658
+ // ----------------------------------------------------------------
659
+ // Editor quick-link
660
+ // ----------------------------------------------------------------
661
+ // The published page lives on the same Space as the editor, so we
662
+ // can probe /api/auth/status to know if the current visitor has
663
+ // edit rights. When they do, surface a small "open editor" pencil
664
+ // top-right; otherwise the button stays \`hidden\` and is removed
665
+ // on the next layout pass (no flicker for read-only visitors).
666
+ //
667
+ // A network failure (offline preview, exported HTML opened from
668
+ // disk, ...) is silently ignored - the button just stays hidden.
669
+ (function() {
670
+ var link = document.getElementById('edit-link');
671
+ if (!link) return;
672
+ fetch('/api/auth/status', { credentials: 'same-origin' })
673
+ .then(function(r) { return r.ok ? r.json() : null; })
674
+ .then(function(data) {
675
+ if (data && data.canEdit) {
676
+ link.removeAttribute('hidden');
677
+ }
678
+ })
679
+ .catch(function() {});
680
+ })();
681
  })();
682
  </script>
683
  </body>
frontend/src/styles/_layout.css CHANGED
@@ -138,8 +138,44 @@
138
  transform: scale(0.92);
139
  }
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  @media (max-width: 1100px) {
142
- #theme-toggle {
 
143
  display: none;
144
  }
145
  }
 
138
  transform: scale(0.92);
139
  }
140
 
141
+ /* Editor quick-link (published page only). Mirrors the theme toggle
142
+ on the opposite side. The button itself ships with `hidden` and is
143
+ revealed by inline JS in the published page only when
144
+ /api/auth/status returns canEdit=true. */
145
+ #edit-link {
146
+ position: absolute;
147
+ top: var(--spacing-4);
148
+ right: var(--spacing-4);
149
+ margin: 0;
150
+ z-index: var(--z-overlay);
151
+ width: 40px;
152
+ height: 40px;
153
+ border-radius: 50%;
154
+ border: 1px solid var(--border-color);
155
+ background: var(--page-bg);
156
+ box-shadow: 0 2px 12px rgba(0,0,0,.08);
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: center;
160
+ padding: 0;
161
+ color: var(--text-color);
162
+ text-decoration: none;
163
+ transition: transform 150ms ease, box-shadow 150ms ease,
164
+ background 150ms ease;
165
+ }
166
+ #edit-link:hover {
167
+ background: color-mix(in srgb, var(--primary-color) 12%, var(--page-bg));
168
+ }
169
+ #edit-link:active {
170
+ transform: scale(0.92);
171
+ }
172
+ #edit-link[hidden] {
173
+ display: none;
174
+ }
175
+
176
  @media (max-width: 1100px) {
177
+ #theme-toggle,
178
+ #edit-link {
179
  display: none;
180
  }
181
  }