Skip to content

Commit 971de55

Browse files
committed
feat: add share-link loading overlay to eliminate flash of bare UI
- Added full-screen #share-loading-overlay with branded logo + spinner - Inline <head> script detects share hash params (s=, id=, d=, space=) before any JS loads - Overlay activates immediately at body parse time (zero-lag) - Theme-aware: dark #0d1117 / light #f6f8fa backgrounds - Fades out smoothly (0.35s opacity) when content is ready - Added hideShareLoader() helper in cloud-share.js, called at all terminal paths - 15-second safety timeout auto-dismisses overlay on network failure - Removed intermediate 'Decrypting...' / 'Loading Space...' divs from preview panel
1 parent 6f495fe commit 971de55

5 files changed

Lines changed: 193 additions & 3 deletions

File tree

CHANGELOG-share-link-loader.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Share Link Loading Overlay — Eliminate Flash of Bare UI
2+
3+
- Added full-screen loading overlay (`#share-loading-overlay`) that shows instantly when a share URL is detected
4+
- Overlay activates before any JS or CSS loads via inline `<script>` hash detection in `<head>`
5+
- Overlay shows TextAgent logo, animated spinner, and "Loading shared content…" label
6+
- Overlay is theme-aware: dark background (`#0d1117`) in dark mode, `#f6f8fa` in light mode
7+
- Overlay fades out smoothly (0.35s opacity transition) once content is ready
8+
- Added `hideShareLoader()` helper in `cloud-share.js`, called at every terminal path (success and error)
9+
- Fixed: Space hub (`#space=`) path was writing an intermediate "Loading Space…" div to the preview; now stays hidden behind the overlay
10+
- Fixed: Compact share (`#s=`), secure share (`#id=`), and legacy share (`#id=&k=`) paths all properly dismiss the overlay
11+
- Removed intermediate "Decrypting…" / "Loading…" divs from within the preview panel (they were visible during the lag)
12+
- Added 15-second safety net auto-dismiss in case of network failure or JS errors during load
13+
14+
---
15+
16+
## Summary
17+
When users opened a shared link like `#space=ai-articles&s=tcujtr979xk`, they saw three ugly stages: an empty screen, then a flash of the bare editor HTML, then the final content. This adds a polished full-screen loading splash that hides all of that until the content is ready to display.
18+
19+
---
20+
21+
## 1. Share Link Loading Overlay (index.html)
22+
**Files:** `index.html`
23+
**What:** Added `#share-loading-overlay` div with inline CSS (logo, spinner, label) positioned `fixed; inset: 0; z-index: 99999`. An inline `<script>` in `<head>` detects the hash params (`s=`, `id=`, `d=`, `space=`) and sets `window.__showShareLoader = true`. A second inline `<script>` immediately after the overlay element activates it with the `slo-active` class. A 15-second safety timeout auto-dismisses the overlay if loading fails.
24+
**Impact:** Users see a branded, polished loading screen instead of a blank or partially-rendered UI during the Firebase fetch and decryption phase.
25+
26+
## 2. Share Loader Dismissal (cloud-share.js)
27+
**Files:** `js/cloud-share.js`
28+
**What:** Added `hideShareLoader()` private function that adds `slo-fade-out` class (triggers CSS opacity transition) then removes both classes after 380ms. Called at every exit point of `M.loadSharedMarkdown`: success paths for Space hub, compact share, secure share, and legacy share; all error/catch blocks; and the access-denied gate for forms.
29+
**Impact:** Loader always dismisses cleanly whether content loads successfully, fails to load, or is blocked by the form access gate.
30+
31+
---
32+
33+
## Files Changed (2 total)
34+
35+
| File | Lines Changed | Type |
36+
|------|:---:|------|
37+
| `index.html` | +90 −1 | Added overlay HTML + CSS + activation scripts |
38+
| `js/cloud-share.js` | +22 −8 | Added hideShareLoader helper + dismiss calls |

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,7 @@ TextAgent has undergone significant evolution since its inception. What started
544544

545545
| Date | Commits | Feature / Update |
546546
|------|---------|-----------------:|
547+
| **2026-04-02** | |**Share Link Loading Overlay** — eliminates the flash of bare UI when opening shared links (`#space=`, `#s=`, `#id=`, `#d=`); full-screen branded loading splash (TextAgent logo + spinner) activates before any JS loads via inline hash detection in `<head>`; theme-aware (dark `#0d1117` / light `#f6f8fa`); fades out smoothly (0.35s) once content is ready; `hideShareLoader()` called at every terminal path (success, error, form-gate); 15-second safety timeout auto-dismisses on network failure |
547548
| **2026-04-02** | | ✏️ **Annotate DocGen + Pretext Reflow Engine** — new `{{Annotate:}}` DocGen tag with canvas-based freehand annotation overlay; `@text:` source mode renders text on-canvas with real-time Pretext-style scanline reflow (~0.5–1.5ms/frame, O(width) per row); freehand strokes build an offscreen mask; per-row `getImageData` scans free x-intervals; words packed into intervals via `canvas.measureText()` (same arithmetic as Pretext `layoutNextLine()`); tools: pen, highlighter, eraser, line, arrow, rect, circle; color swatches + size slider; `📖 Present` button calls `M.setViewMode('preview')` — hides editor, annotation stays fully drawable while reading; `↩ Undo` / `🗑 Clear` / `📥 PNG` actions; Fixed: `data-text` stripped by DOMPurify — added `data-text`, `data-reflow` to `ADD_ATTR` + hidden `<span class="ann-reflow-text">` textContent as primary storage (survives sanitization without whitelisting); `annotate-docgen.js` (~710 lines) + `annotate-docgen.css` (~340 lines); interactive demo: `public/pretext-reflow-demo.html` — 4-tab demo (Float Image, Draw Exclusion, Both Together, API explainer) |
548549
| **2026-04-02** | | 📬 **Space Recovery by Email + Email Link Fix** — Spaces "Recover" view now has two tabs: **By Email** (default — enter email + access key, Firestore queries by hashed `eh` field to recover all matching spaces) and **By Slug** (existing flow); email pre-filled from localStorage; recovers multiple spaces at once if same key used; "Email to Self" now embeds all generated links (Share Link, Editor Link, Respondent Link, Password) directly in the email body so they always appear in the sent email |
549550
| **2026-04-02** | | 🔬 **Research Loop UX Fixes** — fixed table-tools toolbar hijacking research results table (skips `research-results-table` class); fixed results table `max-height: 350px` clipping to `600px`; moved `research-loop.js` import to Phase 3b-ext3 with try/catch to prevent silent load failures; clicking row now shows only the extracted `PROMPT` text with `📋 Copy` button (not full scorer code); added search pills panel (`@search:` field) with provider API key prompts; DOMPurify allowlist expanded for `input`, `label`, `checked`, `data-research-*` attrs; iframe sandbox height raised from 800→5000px |
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Share Link Loading Overlay — Eliminate Flash of Bare UI
2+
3+
- Added full-screen loading overlay (`#share-loading-overlay`) that shows instantly when a share URL is detected
4+
- Overlay activates before any JS or CSS loads via inline `<script>` hash detection in `<head>`
5+
- Overlay shows TextAgent logo, animated spinner, and "Loading shared content…" label
6+
- Overlay is theme-aware: dark background (`#0d1117`) in dark mode, `#f6f8fa` in light mode
7+
- Overlay fades out smoothly (0.35s opacity transition) once content is ready
8+
- Added `hideShareLoader()` helper in `cloud-share.js`, called at every terminal path (success and error)
9+
- Fixed: Space hub (`#space=`) path was writing an intermediate "Loading Space…" div to the preview; now stays hidden behind the overlay
10+
- Fixed: Compact share (`#s=`), secure share (`#id=`), and legacy share (`#id=&k=`) paths all properly dismiss the overlay
11+
- Removed intermediate "Decrypting…" / "Loading…" divs from within the preview panel (they were visible during the lag)
12+
- Added 15-second safety net auto-dismiss in case of network failure or JS errors during load
13+
14+
---
15+
16+
## Summary
17+
When users opened a shared link like `#space=ai-articles&s=tcujtr979xk`, they saw three ugly stages: an empty screen, then a flash of the bare editor HTML, then the final content. This adds a polished full-screen loading splash that hides all of that until the content is ready to display.
18+
19+
---
20+
21+
## 1. Share Link Loading Overlay (index.html)
22+
**Files:** `index.html`
23+
**What:** Added `#share-loading-overlay` div with inline CSS (logo, spinner, label) positioned `fixed; inset: 0; z-index: 99999`. An inline `<script>` in `<head>` detects the hash params (`s=`, `id=`, `d=`, `space=`) and sets `window.__showShareLoader = true`. A second inline `<script>` immediately after the overlay element activates it with the `slo-active` class. A 15-second safety timeout auto-dismisses the overlay if loading fails.
24+
**Impact:** Users see a branded, polished loading screen instead of a blank or partially-rendered UI during the Firebase fetch and decryption phase.
25+
26+
## 2. Share Loader Dismissal (cloud-share.js)
27+
**Files:** `js/cloud-share.js`
28+
**What:** Added `hideShareLoader()` private function that adds `slo-fade-out` class (triggers CSS opacity transition) then removes both classes after 380ms. Called at every exit point of `M.loadSharedMarkdown`: success paths for Space hub, compact share, secure share, and legacy share; all error/catch blocks; and the access-denied gate for forms.
29+
**Impact:** Loader always dismisses cleanly whether content loads successfully, fails to load, or is blocked by the form access gate.
30+
31+
---
32+
33+
## Files Changed (2 total)
34+
35+
| File | Lines Changed | Type |
36+
|------|:---:|------|
37+
| `index.html` | +90 −1 | Added overlay HTML + CSS + activation scripts |
38+
| `js/cloud-share.js` | +22 −8 | Added hideShareLoader helper + dismiss calls |

index.html

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,79 @@
9191
background: inherit;
9292
color: inherit;
9393
}
94+
95+
/* =========================================
96+
SHARE LINK LOADING OVERLAY
97+
Shows only when a share hash is in the URL
98+
========================================= */
99+
#share-loading-overlay {
100+
display: none;
101+
position: fixed;
102+
inset: 0;
103+
z-index: 99999;
104+
align-items: center;
105+
justify-content: center;
106+
flex-direction: column;
107+
gap: 20px;
108+
background: var(--bg-color, #0d1117);
109+
transition: opacity 0.35s ease, transform 0.35s ease;
110+
opacity: 1;
111+
}
112+
#share-loading-overlay.slo-active {
113+
display: flex;
114+
}
115+
#share-loading-overlay.slo-fade-out {
116+
opacity: 0;
117+
pointer-events: none;
118+
}
119+
.slo-logo {
120+
display: flex;
121+
align-items: center;
122+
gap: 10px;
123+
font-size: 1.5rem;
124+
font-weight: 700;
125+
letter-spacing: -0.02em;
126+
color: #58a6ff;
127+
opacity: 0.95;
128+
}
129+
.slo-logo img {
130+
width: 36px;
131+
height: 36px;
132+
border-radius: 8px;
133+
}
134+
.slo-spinner {
135+
width: 40px;
136+
height: 40px;
137+
border-radius: 50%;
138+
border: 3px solid rgba(88,166,255,0.15);
139+
border-top-color: #58a6ff;
140+
animation: slo-spin 0.75s linear infinite;
141+
}
142+
@keyframes slo-spin {
143+
to { transform: rotate(360deg); }
144+
}
145+
.slo-label {
146+
font-size: 0.85rem;
147+
color: rgba(201,209,217,0.55);
148+
letter-spacing: 0.04em;
149+
}
150+
html[data-theme="light"] #share-loading-overlay {
151+
background: #f6f8fa;
152+
}
153+
html[data-theme="light"] .slo-label {
154+
color: rgba(31,35,40,0.45);
155+
}
94156
</style>
157+
<!-- Share-link splash: detect hash BEFORE any JS so overlay shows instantly -->
158+
<script>
159+
(function () {
160+
var h = window.location.hash;
161+
if (h && (h.indexOf('s=') !== -1 || h.indexOf('id=') !== -1 || h.indexOf('d=') !== -1 || h.indexOf('space=') !== -1)) {
162+
// Will be activated once the overlay element exists (see body)
163+
window.__showShareLoader = true;
164+
}
165+
})();
166+
</script>
95167

96168
<!-- Updated libraries to latest versions -->
97169

@@ -134,6 +206,28 @@
134206
</head>
135207

136208
<body>
209+
<!-- Share link loading overlay (hidden by default, shown via JS below) -->
210+
<div id="share-loading-overlay" aria-live="polite" aria-label="Loading shared document">
211+
<div class="slo-logo">
212+
<img src="/assets/icon.png" alt="TextAgent" onerror="this.style.display='none'">
213+
TextAgent
214+
</div>
215+
<div class="slo-spinner"></div>
216+
<div class="slo-label">Loading shared content&hellip;</div>
217+
</div>
218+
<script>
219+
if (window.__showShareLoader) {
220+
var _sloEl = document.getElementById('share-loading-overlay');
221+
_sloEl.classList.add('slo-active');
222+
// Safety net: auto-dismiss after 15s in case JS loading fails
223+
setTimeout(function () {
224+
if (_sloEl.classList.contains('slo-active')) {
225+
_sloEl.classList.add('slo-fade-out');
226+
setTimeout(function () { _sloEl.classList.remove('slo-active', 'slo-fade-out'); }, 400);
227+
}
228+
}, 15000);
229+
}
230+
</script>
137231
<div class="app-container">
138232
<header class="app-header">
139233
<div class="container-fluid d-flex justify-content-between align-items-center header-container">

js/cloud-share.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@
1313
M.sharedViewLock = null; // null = no lock, 'ppt' | 'preview' = locked
1414
var readonlyClickHandlerInstalled = false;
1515

16+
// --- Share-link loading overlay dismissal ---
17+
function hideShareLoader() {
18+
var overlay = document.getElementById('share-loading-overlay');
19+
if (!overlay || !overlay.classList.contains('slo-active')) return;
20+
overlay.classList.add('slo-fade-out');
21+
setTimeout(function () {
22+
overlay.classList.remove('slo-active', 'slo-fade-out');
23+
}, 380);
24+
}
25+
1626
// --- Firebase Config ---
1727
var firebaseConfig = {
1828
apiKey: 'AIzaSyC_5pgtZ-mZvHmIUH9X7MkObPwDLw8nyfw',
@@ -553,16 +563,18 @@
553563
// --- Space hub: #space=<slug> ---
554564
if (spaceSlug && !compactId) {
555565
try {
556-
M.markdownPreview.innerHTML = '<div style="padding: 40px; text-align: center; opacity: 0.6;"><i class="bi bi-collection"></i> Loading Space...</div>';
557566
M.setViewMode('preview');
558567
var spaceData = await M.loadSpace(spaceSlug);
559568
if (!spaceData) {
569+
hideShareLoader();
560570
M.markdownPreview.innerHTML = '<div style="padding: 60px; text-align: center;"><h2>Space Not Found</h2><p>This space may have been removed or the URL is incorrect.</p></div>';
561571
return;
562572
}
573+
hideShareLoader();
563574
M.renderSpaceHub(spaceData, spaceSlug);
564575
} catch (e) {
565576
console.error('Error loading Space:', e);
577+
hideShareLoader();
566578
M.markdownPreview.innerHTML = '<div style="padding: 60px; text-align: center;"><h2>Error</h2><p>' + e.message + '</p></div>';
567579
}
568580
return;
@@ -640,6 +652,7 @@
640652

641653
// Form access gate: block if form doc but no rk or m=fill (unless editor)
642654
if (!isEditMode && /\{\{@?(?:Form|Quiz):/i.test(markdownContent) && !M.formResponseKey && !M.isFormFillMode) {
655+
hideShareLoader();
643656
M.markdownPreview.innerHTML = '<div style="padding: 40px; text-align: center;"><h3 style="color: var(--color-danger-fg);"><i class="bi bi-shield-lock"></i> Access Denied</h3><p style="opacity: 0.7;">This form requires a valid access link.</p><p style="font-size: 13px; opacity: 0.5;">Please use the link provided by the form creator.</p></div>';
644657
M.setViewMode('preview');
645658
return;
@@ -656,16 +669,19 @@
656669
document.body.classList.remove('editor-readonly');
657670
lastCloudContent = markdownContent; // Prevent immediate re-save
658671
scheduleCloudSave();
672+
hideShareLoader();
659673
if (M.showToast) M.showToast('🔑 Editor access — changes will sync to this document', 'success');
660674
} else {
661675
var sharedMode = M.sharedViewLock || 'preview';
662676
M.setViewMode(sharedMode);
663677
M.isViewingSharedDoc = true;
664678
showSharedBanner();
665679
if (sharedMode === 'preview' && M.setHeaderLevel) M.setHeaderLevel(2);
680+
hideShareLoader();
666681
}
667682
} catch (error) {
668683
console.error('Failed to load compact shared markdown:', error);
684+
hideShareLoader();
669685
M.markdownPreview.innerHTML = '<div style="padding: 40px; text-align: center;"><h3 style="color: var(--color-danger-fg);"><i class="bi bi-shield-exclamation"></i> Decryption Failed</h3><p style="opacity: 0.7;">The link may be invalid or the document may not exist.</p><p style="font-size: 13px; opacity: 0.5;"></p></div>';
670686
M.markdownPreview.querySelector('p:last-child').textContent = error.message;
671687
M.setViewMode('split');
@@ -676,7 +692,6 @@
676692
// --- Secure share: no key in URL, passphrase needed ---
677693
if (isSecure && docId && !keyString) {
678694
try {
679-
M.markdownPreview.innerHTML = '<div style="padding: 40px; text-align: center; opacity: 0.6;"><i class="bi bi-shield-lock"></i> Loading protected document...</div>';
680695
M.setViewMode('split');
681696
var doc = await db.collection('shares').doc(docId).get();
682697
if (!doc.exists) throw new Error('Shared document not found.');
@@ -686,9 +701,11 @@
686701
M.sharedViewLock = data.view;
687702
}
688703
pendingSecureDoc = { dataString: data.d, saltString: data.salt, docId: docId, ekHash: data.ekHash || '', eWt: data.eWt || '' };
704+
hideShareLoader();
689705
showPassphrasePrompt();
690706
} catch (error) {
691707
console.error('Failed to load secure shared markdown:', error);
708+
hideShareLoader();
692709
M.markdownPreview.innerHTML = '<div style="padding: 40px; text-align: center;"><h3 style="color: var(--color-danger-fg);"><i class="bi bi-shield-exclamation"></i> Document Not Found</h3><p style="opacity: 0.7;">The shared document may have been deleted or the link is invalid.</p></div>';
693710
M.setViewMode('split');
694711
}
@@ -698,7 +715,6 @@
698715
// --- Legacy quick share: key in URL (#id=...&k=...) ---
699716
if (!keyString || (!docId && !inlineData)) return;
700717
try {
701-
M.markdownPreview.innerHTML = '<div style="padding: 40px; text-align: center; opacity: 0.6;"><i class="bi bi-lock"></i> Decrypting shared content...</div>';
702718
M.setViewMode('split');
703719
var dataString;
704720
if (docId) {
@@ -724,6 +740,7 @@
724740
var markdownContent = decompressData(compressed);
725741
// Form access gate: block if form doc but no rk or m=fill
726742
if (/\{\{@?(?:Form|Quiz):/i.test(markdownContent) && !M.formResponseKey && !M.isFormFillMode) {
743+
hideShareLoader();
727744
M.markdownPreview.innerHTML = '<div style="padding: 40px; text-align: center;"><h3 style="color: var(--color-danger-fg);"><i class="bi bi-shield-lock"></i> Access Denied</h3><p style="opacity: 0.7;">This form requires a valid access link.</p><p style="font-size: 13px; opacity: 0.5;">Please use the link provided by the form creator.</p></div>';
728745
M.setViewMode('preview');
729746
return;
@@ -737,8 +754,10 @@
737754
showSharedBanner();
738755
// Auto-hide full header for preview mode shared links
739756
if (sharedMode === 'preview' && M.setHeaderLevel) M.setHeaderLevel(2);
757+
hideShareLoader();
740758
} catch (error) {
741759
console.error('Failed to load shared markdown:', error);
760+
hideShareLoader();
742761
M.markdownPreview.innerHTML = '<div style="padding: 40px; text-align: center;"><h3 style="color: var(--color-danger-fg);"><i class="bi bi-shield-exclamation"></i> Decryption Failed</h3><p style="opacity: 0.7;">The link may be invalid or the document may not exist.</p><p style="font-size: 13px; opacity: 0.5;"></p></div>';
743762
M.markdownPreview.querySelector('p:last-child').textContent = error.message;
744763
M.setViewMode('split');

0 commit comments

Comments
 (0)