Skip to content

Commit d4c9afc

Browse files
author
Corey B
committed
feat(ui): implement unified floating Save Updates banner and fix native body alignment parsing
1 parent 0bed33f commit d4c9afc

2 files changed

Lines changed: 76 additions & 20 deletions

File tree

extension/content.js

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ if (document.contentType === 'text/plain' && document.body.firstElementChild?.ta
1212
const parser = new DOMParser();
1313
originalDocParser = parser.parseFromString(rawPayloadString, "text/html");
1414

15+
// Attempt to carry over original body inline styles explicitly to fix left-align rendering bugs
16+
const bodyStyles = originalDocParser.body.getAttribute('style');
17+
if (bodyStyles) {
18+
document.body.setAttribute('style', bodyStyles);
19+
} else {
20+
document.body.style.cssText = "background: #f1f5f9; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0;";
21+
}
22+
1523
const fddWrapper = originalDocParser.querySelector('fdd-container');
1624
if (fddWrapper) {
1725
document.body.appendChild(fddWrapper.cloneNode(true));
@@ -27,6 +35,7 @@ class ExtensionFDD {
2735
this.vcData = {};
2836
this.mutableData = {};
2937
this.fileHandle = null;
38+
this.hasUnsavedChanges = false;
3039
if (!this.template) throw new Error("No bindings template found");
3140
this.init();
3241
}
@@ -104,7 +113,7 @@ class ExtensionFDD {
104113
setupAutoSave(root) {
105114
const inputs = root.querySelectorAll('[autosave]');
106115
inputs.forEach(input => {
107-
// Prompt user for local file persistence capability on intersection/interaction click
116+
// Prompt user for local file persistence capability on intersection/interaction hover
108117
input.addEventListener('pointerdown', async () => {
109118
if (!this.fileHandle && window.showOpenFilePicker && document.contentType !== 'text/plain') {
110119
try {
@@ -123,26 +132,64 @@ class ExtensionFDD {
123132
}
124133
});
125134

126-
// Commit to persistent disk layer via handler mechanism
127-
input.addEventListener('change', async (e) => {
135+
// Instead of forcing an annoying auto-download immediately on every blur, we accumulate state.
136+
const handleStateChange = (e) => {
128137
const path = e.target.name.split('.');
129138
if (path[0] === 'user-notes' && path[1]) {
130139
this.mutableData[path[1]] = e.target.value;
131-
await this.syncFile();
140+
this.hasUnsavedChanges = true;
141+
this.showSaveBanner();
132142
}
133-
});
143+
};
144+
145+
input.addEventListener('change', handleStateChange);
146+
input.addEventListener('keyup', handleStateChange); // Catches instant notes modifications
147+
});
148+
}
149+
150+
showSaveBanner() {
151+
let banner = document.getElementById('fdd-save-banner');
152+
if (banner) return; // already visible
153+
154+
banner = document.createElement('div');
155+
banner.id = 'fdd-save-banner';
156+
banner.style.cssText = `
157+
position: fixed; bottom: 0; left: 0; right: 0;
158+
background: #001f3f; color: white; padding: 15px 40px;
159+
display: flex; justify-content: space-between; align-items: center;
160+
z-index: 9999; font-family: system-ui; box-shadow: 0 -4px 15px rgba(0,0,0,0.2);
161+
border-top: 3px solid #3b82f6;
162+
`;
134163

135-
input.addEventListener('blur', async (e) => {
136-
const path = e.target.name.split('.');
137-
if (path[0] === 'user-notes' && path[1]) {
138-
this.mutableData[path[1]] = e.target.value;
139-
await this.syncFile();
140-
}
164+
const text = document.createElement('span');
165+
text.innerText = "You have unsaved changes to this document.";
166+
text.style.fontSize = "1.1rem";
167+
text.style.fontWeight = "600";
168+
169+
const btn = document.createElement('button');
170+
btn.innerText = "Save Updates";
171+
btn.style.cssText = `
172+
background: #10b981; color: white; border: none; padding: 10px 24px;
173+
border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 1rem;
174+
transition: transform 0.2s ease, background 0.2s ease;
175+
`;
176+
177+
btn.onmouseover = () => btn.style.background = "#059669";
178+
btn.onmouseout = () => btn.style.background = "#10b981";
179+
btn.onmousedown = () => btn.style.transform = "scale(0.95)";
180+
btn.onmouseup = () => btn.style.transform = "scale(1)";
181+
182+
btn.addEventListener('click', () => {
183+
this.commitSave();
184+
banner.remove();
141185
});
142-
});
186+
187+
banner.appendChild(text);
188+
banner.appendChild(btn);
189+
document.body.appendChild(banner);
143190
}
144191

145-
async syncFile() {
192+
async commitSave() {
146193
console.log("Deploying local disk persistent save...", this.mutableData);
147194
if (!originalDocParser) return;
148195

@@ -152,6 +199,7 @@ class ExtensionFDD {
152199
mutableScript.textContent = "\n " + JSON.stringify(this.mutableData, null, 2) + "\n ";
153200
}
154201

202+
// Completely rebuild the underlying document structure out of isolated components
155203
const content = "<!DOCTYPE html>\n" + originalDocParser.documentElement.outerHTML;
156204

157205
// Primary Write Strategy (Windows / macOS / Chromium Standards)
@@ -160,33 +208,41 @@ class ExtensionFDD {
160208
const writable = await this.fileHandle.createWritable();
161209
await writable.write(content);
162210
await writable.close();
211+
this.hasUnsavedChanges = false;
163212
this.showBadge("✓ FDD Synced Directly to Disk", "#10b981");
164213
return;
165214
} catch (e) {
166215
console.error("Direct FDD Persist Error, falling back...", e);
167216
}
168217
}
169218

170-
// Secondary Linux / Missing FileURL Permission Fallback Strategy
219+
// Secondary Universal Download Fallback / Linux Fallback Strategy
220+
// Tries to seamlessly update existing file via universal download replacement patterns
171221
console.warn("Executing native Download Fallback mapping for persistent storage");
222+
let currentFilename = window.location.pathname.split('/').pop();
223+
if (!currentFilename || currentFilename === '') currentFilename = 'updated_document.fdd';
224+
if (!currentFilename.endsWith('.fdd')) currentFilename += '.fdd';
225+
172226
const blob = new Blob([content], { type: 'text/html' });
173227
const url = URL.createObjectURL(blob);
174228
const a = document.createElement('a');
175229
a.href = url;
176-
a.download = 'updated.fdd';
230+
a.download = decodeURIComponent(currentFilename);
177231
document.body.appendChild(a);
178232
a.click();
233+
234+
// Cleanup
179235
document.body.removeChild(a);
180236
URL.revokeObjectURL(url);
181-
this.showBadge("⚠️ Downloaded New FDD Backup", "#fbbf24");
237+
this.hasUnsavedChanges = false;
238+
this.showBadge("✓ Saved Local Backup of FDD", "#3b82f6");
182239
}
183240

184241
showBadge(msg, color) {
185242
const badge = document.createElement('div');
186243
badge.style.cssText = `position:fixed;bottom:20px;right:20px;background:${color};color:white;padding:10px 18px;border-radius:999px;z-index:9999;font-family:system-ui;font-weight:bold;box-shadow:0 10px 15px -3px rgba(0,0,0,0.1);`;
187244
badge.innerText = msg;
188245

189-
// Since container is a shadowRoot, we append directly to the host body
190246
document.body.appendChild(badge);
191247
setTimeout(() => {
192248
badge.style.transition = 'opacity 0.5s ease';

spec/SPEC.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ The FDD container enforces a **Layered Signature Model** inside the `<fdd-contai
3636

3737
## 3. The "Self-Saving" Mechanism (Persistence)
3838

39-
An `.fdd` file acts as its own state database. Updates to the DOM natively sync back to the originating `.fdd` file on disk via the File System Access API.
39+
An `.fdd` file acts as its own state database. Updates to the DOM natively sync back to the originating `.fdd` file on disk relying on unified persistence strategies.
4040

41-
* When the `.fdd` parser binds a `<textarea name="user-notes.followUp" autosave>` to the local state, a subsequent user edit triggers a local file-write request.
42-
* The `.fdd` spec advocates for browser vendors to support a "Auto-Save Trust" permission (*"Allow this document to update itself"*).
41+
* **Primary Strategy (File System Access API):** The `.fdd` parser bounds interactive changes to an explicit write-lock approval prompt on execution. If granted, mutable components actively rewrite the local file.
42+
* **Universal Fallback (The Save Pattern):** Because native write-locks are routinely restricted (e.g., Linux sandbox environments), the FDD specification standardizes a uniform "Save Updates" interface. When active edits mutate the DOM, a persistent prompt alerts the user. Committing the save leverages ambient URI hooks to default the OS download to exactly match the originating `.fdd`'s filename and route.
4343

4444
## 4. No-JS Security Model
4545
Since executable scripts are blocked, interactions scale through:

0 commit comments

Comments
 (0)