feat(snapshot): refine recovery choices and snapshot dialog styling (#146)
* Refine snapshot load dialog recovery UI * Make snapshot load recovery choices explicit * Align snapshot picker styling * Respect Comfy theme variables in snapshot dialogs * Document superseded snapshot recovery design * Fix load recovery selection races * Prevent duplicate snapshot dialog actions * Harden snapshot recovery UI fallbacks
This commit is contained in:
+11
-10
@@ -23,16 +23,17 @@ The format is inspired by Keep a Changelog and SemVer.
|
||||
a different library via `Save to…` relabels Save to *"Save to new
|
||||
folder"* before the click commits.
|
||||
|
||||
The Load dialog's auto-save discovery is now inline: when a preset
|
||||
has a newer recovery file (server row-augment now considers
|
||||
`max(periodic.json, pre_load_*.json)` instead of just
|
||||
`periodic.json`), clicking the preset expands a single scoped
|
||||
recovery row directly beneath it. The user picks by clicking the
|
||||
named row (load named) or the scoped row (load the autosave). The
|
||||
YES/NO modal that used to interrupt the flow is gone. Preset
|
||||
deletion is now inline too — clicking × outlines the row red and
|
||||
the bottom Close button transforms to *"Yes — delete '<name>'"*;
|
||||
Escape cancels.
|
||||
The Load dialog's auto-save discovery is now handled in one window:
|
||||
when a preset has a newer recovery file (server row-augment now
|
||||
considers `max(periodic.json, pre_load_*.json)` instead of just
|
||||
`periodic.json`), selecting that preset changes the title to
|
||||
*"Auto-save is newer than the saved version"*, opens the scoped
|
||||
Recovery auto-saves section, and offers explicit *"NO - load saved"*
|
||||
/ *"YES - load latest"* buttons. Presets without a newer recovery file
|
||||
simply select the row and change the footer action to *"Load"*. Preset
|
||||
deletion is now inline too — clicking × outlines the row red and the
|
||||
bottom Close button transforms to *"Yes"*; Escape cancels. The delete
|
||||
buttons stay visually neutral until hover.
|
||||
|
||||
The Settings cog is removed from the Snapshot toolbar (row is now
|
||||
status indicator · Load · Quick Save · Save); library-path
|
||||
|
||||
@@ -21,38 +21,58 @@ Implementation tracked in [issue #137](https://github.com/malkuthro/ComfyUI-Kool
|
||||
folder ↗" top row, `Load from…` in the bottom command bar opening
|
||||
the same folder picker.
|
||||
4. **Autosave-newer affordance:** the YES/NO choice modal is **dropped**.
|
||||
Instead, when a preset has a newer autosave (server row-augment
|
||||
reports `latestAutosaveMtime > mtime`), clicking that preset row
|
||||
in the Load dialog expands a single scoped recovery row directly
|
||||
beneath it. The row carries a kind badge (Pre-load / Periodic) and
|
||||
the timestamp + meta of the newest entry across `periodic.json`
|
||||
and every `pre_load_*.json` in `<preset>_autosave/`. The user
|
||||
chooses by clicking: re-click the named row to load named, click
|
||||
the recovery row to restore the autosave.
|
||||
The earlier scoped-row click design is also superseded. Final
|
||||
decision: when a preset has a newer autosave (server row-augment
|
||||
reports `latestAutosaveMtime > mtime`), selecting that preset keeps
|
||||
the user in the same Load dialog, changes the header to
|
||||
*"Auto-save is newer than the saved version"*, opens the bottom
|
||||
Recovery auto-saves section for that preset only, and shows explicit
|
||||
footer choices: *"NO - load saved"* and *"YES - load latest"*. The
|
||||
recovery row carries a kind badge (Pre-load / Periodic) and the
|
||||
timestamp + meta of the newest entry across `periodic.json` and
|
||||
every `pre_load_*.json` in `<preset>_autosave/`, but row clicks
|
||||
select only; footer buttons commit.
|
||||
5. **Inline delete confirm:** the per-preset `×` button no longer
|
||||
opens a confirm modal; instead it outlines the target row red and
|
||||
transforms the Close button to *"Yes — delete '\<name\>'"* (danger
|
||||
styling). A second click commits; Escape cancels and reverts.
|
||||
transforms the Close button to *"Yes"* (danger styling). A second
|
||||
click commits; Escape or clicking another row cancels and reverts.
|
||||
Delete buttons stay visually neutral until hover.
|
||||
|
||||
## Load flow (post-redesign)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Click preset row] --> B{latestAutosaveMtime > mtime<br/>(server row-augment)?}
|
||||
B -- No --> C[Direct load<br/>(pre-load auto-save written first)]
|
||||
B -- Yes --> D[Expand scoped recovery row<br/>under the clicked preset]
|
||||
B -- No --> C[Select row<br/>footer action becomes Load]
|
||||
B -- Yes --> D[Change title + open bottom Recovery section<br/>for this preset only]
|
||||
D --> E{User clicks…}
|
||||
E -- Named preset row again --> C
|
||||
E -- Scoped recovery row --> F[Restore newest autosave<br/>(periodic.json or pre_load_*.json)]
|
||||
E -- Elsewhere / Esc --> G[Collapse scoped recovery]
|
||||
C --> H[markStateSaved · green dot]
|
||||
F --> I[markStateAutosaved · blue dot]
|
||||
E -- NO - load saved --> F[Load named snapshot<br/>(pre-load auto-save written first)]
|
||||
E -- YES - load latest --> G[Restore newest autosave<br/>(periodic.json or pre_load_*.json)]
|
||||
E -- Another row / Esc --> H[Cancel scoped recovery selection]
|
||||
C --> I[User clicks Load]
|
||||
I --> F
|
||||
F --> J[markStateSaved · green dot]
|
||||
G --> K[markStateAutosaved · blue dot]
|
||||
```
|
||||
|
||||
Compared to the pre-#137 design (a YES/NO modal that interrupted the
|
||||
flow), the inline scoped row keeps both options visible alongside the
|
||||
preset list. The user picks by clicking the row they want — no extra
|
||||
modal layer, no Esc-to-cancel ambiguity, no double-confirm.
|
||||
flow), the final one-window design keeps both options visible in the
|
||||
Load dialog but makes the commit action explicit in the footer. Row
|
||||
clicks select; footer buttons load. This supersedes the intermediate
|
||||
"click the named row again / click the scoped row" design because that
|
||||
proved too easy to miss during review.
|
||||
|
||||
## Superseded notes from the mockup
|
||||
|
||||
- The old section-4 YES/NO modal remains useful as visual history, but
|
||||
it is no longer the target interaction.
|
||||
- The intermediate scoped recovery row under the clicked preset is also
|
||||
superseded. Recovery now appears in the bottom Recovery auto-saves
|
||||
section.
|
||||
- The section-5 four-state button cycle remains the pattern for true
|
||||
committing primary actions. `Save to…` and `Load from…` are utility
|
||||
navigation buttons that open the folder picker; they do not need the
|
||||
Save button's write-state cycle.
|
||||
|
||||
## Out of scope (deliberately, for this PR)
|
||||
|
||||
|
||||
@@ -967,7 +967,7 @@
|
||||
<button class="modal-btn subtle">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="anno fix">Recovery section is <strong>hidden by default</strong>. Clicking a preset row (e.g. <code>Koolook_v03</code> or <code>starter</code>) opens the recovery dropdown and reveals <strong>only that preset's <code><preset>_autosave/</code> folder</strong> — other presets' autosaves are not shown here. Inside the folder, exactly <strong>one row</strong> is rendered: whichever of <em>Pre-load</em> or <em>Periodic</em> is the newest (the system picks). Row shows kind badge + timestamp + small picks/workflows meta; the on-disk filename is implementation detail and stays hidden. The group header keeps its <strong>Open folder ↗</strong> link for manual inspection/backup.</div>
|
||||
<div class="anno fix">Recovery section is <strong>hidden by default</strong>. Clicking a preset row (e.g. <code>Koolook_v03</code> or <code>starter</code>) opens the recovery dropdown and reveals <strong>only that preset's <code><preset>_autosave/</code> folder</strong> — other presets' autosaves are not shown here. Inside the folder, exactly <strong>one row</strong> is rendered: whichever of <em>Pre-load</em> or <em>Periodic</em> is the newest (the system picks). Row shows kind badge + timestamp + small picks/workflows meta; the on-disk filename is implementation detail and stays hidden. The group header keeps its <strong>Open folder ↗</strong> link for manual inspection/backup. <strong>Final decision:</strong> row clicks select only. When this section opens because the selected snapshot has newer recovery, the Load dialog header changes to <em>Auto-save is newer than the saved version</em> and the footer presents <strong>NO - load saved</strong> / <strong>YES - load latest</strong>.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1017,9 +1017,9 @@
|
||||
</div>
|
||||
|
||||
<!-- =========================================================
|
||||
4. Autosave-newer choice modal — proposed only
|
||||
4. Autosave-newer choice modal — superseded reference
|
||||
========================================================= -->
|
||||
<h2>4. Autosave-newer YES / NO modal — strip to essentials</h2>
|
||||
<h2>4. Autosave-newer YES / NO modal — superseded reference</h2>
|
||||
<section class="card">
|
||||
<header class="card-head"><span class="card-tag proposed">Proposed</span><span class="card-title">Default view shows only what the user needs to decide; details hide behind ⓘ</span></header>
|
||||
<div class="card-body">
|
||||
@@ -1053,7 +1053,7 @@
|
||||
<button class="modal-btn primary">YES — load latest</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="anno fix">Each option is rendered as a normal <code>preset-row</code> — same shape the user already reads in the Load dialog (bold name + <em>picks · workflows · saved-date</em> meta line), so there's nothing new to parse. NO loads <code>Koolook_v03.json</code>, YES loads the newest file inside <code>Koolook_v03_autosave/</code>. The two rows make the diff obvious without spelling it out: the autosave row carries a later timestamp and (typically) higher picks/workflow counts because it captured WIP added after the morning save. Buttons collapse to <strong>NO</strong> / <strong>YES — load latest</strong> so they sit on one row and read as a binary.</div>
|
||||
<div class="anno fix"><strong>Superseded final decision:</strong> this separate modal is no longer the target UX. Keep the decision inside the Load dialog instead: selecting a snapshot with newer recovery opens the bottom Recovery auto-saves section, changes the header to <em>Auto-save is newer than the saved version</em>, and exposes footer buttons <strong>NO - load saved</strong> / <strong>YES - load latest</strong>. The row shapes above remain useful visual references for the named-vs-latest comparison, but the extra modal layer is intentionally dropped.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1087,7 +1087,7 @@
|
||||
<div class="state-note">After a successful write, briefly. Same pattern as the Settings dialog's idle "Saved" state.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="anno">Same four-state cycle applies to "Load from…", "Save to…", and any future primary action. Doesn't require new infrastructure — just discipline: every place a button is wired, write the four label/state branches up front.</div>
|
||||
<div class="anno"><strong>Final decision:</strong> the four-state cycle applies to true committing primary actions, such as the Save button that writes snapshot data. <strong>Save to…</strong> and <strong>Load from…</strong> are utility navigation buttons that open the folder picker; they stay stable rather than cycling through write-state labels.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1168,10 +1168,10 @@
|
||||
<li>Library row labels: <strong>"Saved to"</strong> (Save dialog) / <strong>"Loaded from"</strong> (Load dialog).</li>
|
||||
<li>"Open folder ↗" text link sits in the top-right of the path box, above the path itself.</li>
|
||||
<li>Generic <strong>"Open folder"</strong> wording (no platform-specific Finder/Explorer split).</li>
|
||||
<li><strong>Autosave-newer YES/NO modal — dropped.</strong> When a preset has a newer autosave (server row-augment reports <code>latestAutosaveMtime > mtime</code>), clicking the preset in the Load dialog expands a single <em>scoped recovery row</em> directly beneath the clicked preset, showing the freshest entry (newest of <code>periodic.json</code> or any <code>pre_load_*.json</code> in the corresponding <code><preset>_autosave/</code> subdir). The user chooses by clicking: re-click the named row to load named, click the scoped recovery row to restore the autosave. No extra modal layer; both options are visible alongside the preset list.</li>
|
||||
<li><strong>Autosave-newer YES/NO modal — dropped.</strong> The intermediate scoped-row click design is also superseded. When a preset has newer recovery (server row-augment reports <code>latestAutosaveMtime > mtime</code>), selecting the preset keeps the user in the Load dialog, changes the header to <em>"Auto-save is newer than the saved version"</em>, opens the bottom Recovery auto-saves section for that preset only, and shows explicit footer buttons: <strong>NO - load saved</strong> and <strong>YES - load latest</strong>. Row clicks select; footer buttons commit.</li>
|
||||
<li>Reset-to-default — <strong>dropped</strong>. Empty path inside the Save to… / Load from… picker clears the override if needed.</li>
|
||||
<li>Recovery row chrome: kind badge (Pre-load / Periodic) + timestamp + meta. No filenames exposed in rows.</li>
|
||||
<li>Delete confirmation is <strong>inline in the command bar</strong>, not a separate modal. Target row gets a red outline; Close transforms to <em>"Yes — delete '\<name\>'"</em>. Escape cancels.</li>
|
||||
<li>Delete confirmation is <strong>inline in the command bar</strong>, not a separate modal. Target row gets a red outline; Close transforms to <em>"Yes"</em>. Escape or clicking another row cancels. Delete buttons stay visually neutral until hover.</li>
|
||||
<li>Folder picker: <strong>title only, no descriptive subtitle.</strong> Any explanatory gloss goes on the title's hover tooltip — same convention as every other dialog (<a class="text-link" href="../maintainers/conventions.md">conventions.md § Modal headers</a>).</li>
|
||||
<li><strong>Visual harness</strong> in <code>docs/designs/_harness/</code> mounts each redesigned dialog against the live <code>web/sidebar/</code> modules so agents and maintainers can diff implementations against this mockup without dev-syncing. Workflow: <a class="text-link" href="../maintainers/visual-harness.md">visual-harness.md</a>.</li>
|
||||
</ul>
|
||||
|
||||
+50
-50
@@ -175,24 +175,24 @@ export function ensureStyle() {
|
||||
.koolook-toast-btn { padding: 4px 10px; background: rgba(0,0,0,0.25); border: 1px solid rgba(255,255,255,0.25); border-radius: 3px; color: #fff; cursor: pointer; font-size: 11px; font-family: inherit; }
|
||||
.koolook-toast-btn:hover { background: rgba(0,0,0,0.4); }
|
||||
.koolook-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.55); z-index: 9998; display: flex; align-items: center; justify-content: center; }
|
||||
.koolook-modal { background: var(--comfy-menu-bg, #2a2a2a); border: 1px solid var(--border-color, rgba(255,255,255,0.15)); border-radius: 6px; padding: 16px 18px; min-width: 320px; max-width: 520px; box-shadow: 0 6px 24px rgba(0,0,0,0.55); color: var(--input-text, inherit); }
|
||||
.koolook-modal-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; }
|
||||
.koolook-modal { background: var(--comfy-menu-bg, #151515); border: 1px solid var(--border-color, #4f4f54); border-radius: 8px; padding: 16px 18px; min-width: 320px; max-width: 520px; box-shadow: 0 10px 40px rgba(0,0,0,0.6); color: var(--input-text, #f9fafb); overflow: hidden; }
|
||||
.koolook-modal-title { font-size: 15px; font-weight: 700; margin: -16px -18px 14px; padding: 14px 18px; border-bottom: 1px solid var(--border-color, #302f2f); background: var(--comfy-menu-bg, #181818); }
|
||||
.koolook-modal-pathline { font-size: 11px; opacity: 0.55; margin: -6px 0 12px; white-space: normal; overflow-wrap: anywhere; cursor: default; }
|
||||
.koolook-modal-message { font-size: 12px; opacity: 0.85; margin-bottom: 14px; line-height: 1.45; }
|
||||
.koolook-modal-label { font-size: 11px; opacity: 0.7; margin: 6px 0 4px; display: block; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
.koolook-modal-input, .koolook-modal-select { width: 100%; padding: 6px 8px; background: var(--comfy-input-bg, rgba(0,0,0,0.3)); border: 1px solid var(--border-color, rgba(255,255,255,0.1)); border-radius: 4px; color: inherit; font-size: 13px; box-sizing: border-box; outline: none; }
|
||||
.koolook-modal-input, .koolook-modal-select { width: 100%; padding: 6px 8px; background: var(--comfy-input-bg, #111111); border: 1px solid var(--border-color, #302f2f); border-radius: 4px; color: var(--input-text, #e6e8ec); font-size: 13px; box-sizing: border-box; outline: none; }
|
||||
.koolook-modal-input:focus, .koolook-modal-select:focus { border-color: var(--p-primary-color, rgba(100,150,255,0.5)); }
|
||||
/* Library-folder name + path pair used by the redesigned Save and Load
|
||||
dialogs to render the inline 'Saved to' / 'Loaded from' info row.
|
||||
Naming kept from the (deleted) Settings dialog for git-blame continuity;
|
||||
the two classes have always rendered the same shape (leaf folder name
|
||||
on the first line, full absolute path on the second). */
|
||||
.koolook-settings-folder-name { opacity: 0.95; font-size: 12px; font-weight: 600; color: var(--input-text, inherit); }
|
||||
.koolook-settings-folder-path { opacity: 0.68; font-size: 11px; margin-top: 2px; overflow-wrap: anywhere; line-height: 1.35; }
|
||||
.koolook-modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
||||
.koolook-settings-folder-name { opacity: 1; font-size: 13px; font-weight: 700; color: var(--input-text, #e6e8ec); margin-top: 2px; }
|
||||
.koolook-settings-folder-path { color: var(--input-text, #8f959c); opacity: 0.68; font: 11px/1.35 ui-monospace, "Cascadia Mono", Menlo, monospace; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.koolook-modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin: 16px -18px -16px; padding: 12px 18px; border-top: 1px solid var(--border-color, #302f2f); background: var(--comfy-menu-bg, #131313); }
|
||||
.koolook-modal-checkbox-row { display: flex; align-items: center; gap: 8px; margin-top: 10px; font-size: 12px; opacity: 0.85; cursor: pointer; user-select: none; }
|
||||
.koolook-modal-checkbox-row input[type="checkbox"] { cursor: pointer; }
|
||||
.koolook-modal-btn { padding: 6px 14px; background: var(--comfy-input-bg, rgba(0,0,0,0.3)); border: 1px solid var(--border-color, rgba(255,255,255,0.1)); border-radius: 4px; color: inherit; cursor: pointer; font-size: 12px; }
|
||||
.koolook-modal-btn { padding: 6px 14px; background: var(--comfy-input-bg, #1a1a1f); border: 1px solid var(--border-color, #4f4f54); border-radius: 4px; color: var(--input-text, #e6e8ec); cursor: pointer; font: inherit; font-size: 12px; font-weight: 600; }
|
||||
.koolook-modal-btn:hover { background: rgba(255,255,255,0.1); }
|
||||
.koolook-modal-btn-primary { background: rgba(80,140,235,0.35); border-color: rgba(80,140,235,0.55); }
|
||||
.koolook-modal-btn-primary:hover { background: rgba(80,140,235,0.5); }
|
||||
@@ -222,13 +222,13 @@ export function ensureStyle() {
|
||||
.koolook-install-unresolved-summary { cursor: pointer; font-size: 11px; opacity: 0.75; margin-top: 4px; }
|
||||
.koolook-install-progress { width: 100%; height: 6px; background: rgba(255,255,255,0.08); border-radius: 3px; margin: 10px 0 4px; overflow: hidden; }
|
||||
.koolook-install-progress-bar { height: 100%; background: rgba(80,140,235,0.6); width: 0%; transition: width 200ms ease; }
|
||||
.koolook-snapshot-list { max-height: 280px; overflow-y: auto; border: 1px solid rgba(255,255,255,0.08); border-radius: 4px; }
|
||||
/* Recovery auto-saves section in the Load dialog. Collapsible details
|
||||
element; lazy-loads the list on first expand. Each subdir gets its own
|
||||
group header so users can scan visually for the preset they want to
|
||||
recover from. */
|
||||
.koolook-recovery-section { margin-top: 14px; border-top: 1px solid rgba(255,255,255,0.06); padding-top: 8px; }
|
||||
.koolook-recovery-summary { cursor: pointer; font-size: 12px; opacity: 0.78; padding: 4px 2px; user-select: none; outline: none; }
|
||||
.koolook-snapshot-list { max-height: 280px; overflow-y: auto; border: 1px solid var(--border-color, #302f2f); border-radius: 4px; background: var(--comfy-input-bg, #111111); overflow: hidden auto; }
|
||||
/* Recovery auto-saves section in the Load dialog. The collapsed summary is
|
||||
always visible at the bottom of the body; clicking a preset with a newer
|
||||
autosave opens one scoped group inside this container. */
|
||||
.koolook-recovery-section { margin-top: 14px; border: 1px solid var(--border-color, #302f2f); border-radius: 4px; background: rgba(255,255,255,0.02); background: color-mix(in srgb, var(--comfy-menu-bg, #151515) 92%, var(--comfy-input-bg, #111111)); overflow: hidden; }
|
||||
.koolook-recovery-summary { cursor: pointer; font-size: 12px; color: var(--input-text, #d8dadd); padding: 10px 12px; user-select: none; outline: none; }
|
||||
.koolook-recovery-summary-passive { cursor: default; }
|
||||
.koolook-recovery-summary:hover { opacity: 1; }
|
||||
/* No outer border on the recovery list — each group carries its own
|
||||
bordered rows-list, mirroring the library section's structure. We
|
||||
@@ -239,27 +239,31 @@ export function ensureStyle() {
|
||||
here — they would prematurely close the template literal in ES
|
||||
module mode and break the entire constants.js module load (and with
|
||||
it the whole sidebar). Use plain quotes or no quotes at all. */
|
||||
.koolook-recovery-list { margin-top: 6px; max-height: 240px; overflow-y: auto; }
|
||||
.koolook-recovery-group { border-bottom: 1px solid rgba(255,255,255,0.04); }
|
||||
.koolook-recovery-group:last-child { border-bottom: none; }
|
||||
/* Group header is a flex wrapper hosting the same folder-name plus
|
||||
folder-path pair the library section uses, so it carries no
|
||||
typography of its own — the inner CSS drives appearance. */
|
||||
.koolook-recovery-group-header { padding: 4px 0 6px; }
|
||||
.koolook-recovery-list { margin-top: 0; max-height: 240px; overflow-y: auto; border-top: 1px solid rgba(255,255,255,0.04); }
|
||||
.koolook-recovery-group { padding: 10px 12px; }
|
||||
.koolook-recovery-group-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
|
||||
.koolook-recovery-group-title { color: var(--input-text, #e6e8ec); font-size: 12px; font-weight: 700; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.koolook-recovery-group-path { color: var(--input-text, #8f959c); opacity: 0.68; font: 11px/1.35 ui-monospace, "Cascadia Mono", Menlo, monospace; margin-bottom: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.koolook-recovery-group + .koolook-recovery-group { margin-top: 10px; }
|
||||
.koolook-recovery-kind { font-size: 10px; opacity: 0.65; margin-right: 5px; padding: 1px 6px; border-radius: 8px; background: rgba(255,255,255,0.06); text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
.koolook-recovery-kind-pre_load { color: #ffb74d; }
|
||||
.koolook-recovery-kind-periodic { color: #6db4ff; }
|
||||
.koolook-snapshot-row { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-bottom: 1px solid rgba(255,255,255,0.05); }
|
||||
.koolook-recovery-kind { display: inline-flex; align-items: center; width: fit-content; font-size: 10px; line-height: 1.2; text-transform: uppercase; letter-spacing: 0.06em; padding: 2px 6px; border-radius: 3px; font-weight: 700; margin-bottom: 6px; }
|
||||
.koolook-recovery-kind-pre_load { background: rgba(255,184,77,0.16); color: #f5d3a0; border: 1px solid rgba(255,184,77,0.35); }
|
||||
.koolook-recovery-kind-periodic { background: rgba(109,180,255,0.16); color: #b3d4f5; border: 1px solid rgba(109,180,255,0.35); }
|
||||
.koolook-recovery-row { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border: 1px solid var(--border-color, #302f2f); border-radius: 3px; background: var(--comfy-input-bg, #111111); }
|
||||
.koolook-recovery-row-selected { background: rgba(80,140,235,0.16); background: color-mix(in srgb, var(--comfy-input-bg, #111111) 88%, var(--p-primary-color, #6db4ff)); }
|
||||
.koolook-recovery-row-info { flex: 1; min-width: 0; cursor: pointer; font-size: 12px; }
|
||||
.koolook-recovery-row-info:hover .koolook-recovery-row-meta { color: var(--p-primary-color, rgba(120,170,255,1)); }
|
||||
.koolook-recovery-row-meta { color: var(--input-text, #8f959c); opacity: 0.68; font-size: 11px; line-height: 1.35; }
|
||||
.koolook-snapshot-row { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--border-color, #302f2f); background: var(--comfy-input-bg, #111111); }
|
||||
.koolook-snapshot-row-selected { background: rgba(80,140,235,0.16); background: color-mix(in srgb, var(--comfy-input-bg, #111111) 88%, var(--p-primary-color, #6db4ff)); }
|
||||
.koolook-snapshot-row:last-child { border-bottom: none; }
|
||||
.koolook-snapshot-row-info { flex: 1; min-width: 0; cursor: pointer; }
|
||||
.koolook-snapshot-row-info:hover .koolook-snapshot-row-name { color: var(--p-primary-color, rgba(120,170,255,1)); }
|
||||
.koolook-snapshot-row-name { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.koolook-snapshot-row-meta { font-size: 11px; opacity: 0.55; margin-top: 2px; }
|
||||
.koolook-snapshot-row-name { color: var(--input-text, #e6e8ec); font-size: 13px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.koolook-snapshot-row-meta { color: var(--input-text, #8f959c); opacity: 0.68; font-size: 11px; margin-top: 2px; }
|
||||
.koolook-snapshot-row-actions { display: flex; gap: 4px; flex-shrink: 0; }
|
||||
.koolook-snapshot-row-btn { padding: 3px 8px; background: var(--comfy-input-bg, rgba(0,0,0,0.3)); border: 1px solid rgba(255,255,255,0.08); border-radius: 3px; cursor: pointer; color: inherit; font-size: 11px; }
|
||||
.koolook-snapshot-row-btn { min-width: 26px; height: 24px; padding: 0 8px; background: var(--comfy-input-bg, #1a1a1f); border: 1px solid var(--border-color, #4f4f54); border-radius: 4px; cursor: pointer; color: var(--input-text, #e6e8ec); font-size: 16px; font-weight: 800; line-height: 1; }
|
||||
.koolook-snapshot-row-btn:hover { background: rgba(255,255,255,0.08); }
|
||||
.koolook-snapshot-row-btn-danger:hover { background: rgba(220,80,80,0.25); border-color: rgba(220,80,80,0.4); }
|
||||
.koolook-snapshot-row-btn-danger:hover { background: rgba(74,5,5,0.45); border-color: rgba(255,138,138,0.45); color: #ffd9d9; }
|
||||
.koolook-snapshot-empty { padding: 20px; opacity: 0.5; font-size: 12px; text-align: center; }
|
||||
.koolook-mode-toggle { display: inline-flex; gap: 0; margin-right: 3px; border: 1px solid var(--border-color, rgba(255,255,255,0.1)); border-radius: 3px; overflow: hidden; }
|
||||
.koolook-mode-toggle-btn { padding: 0; width: 28px; height: 17px; display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; background: transparent; border: none; cursor: pointer; color: rgba(218,221,226,0.78); font-size: 11px; opacity: 1; line-height: 1; }
|
||||
@@ -303,37 +307,38 @@ export function ensureStyle() {
|
||||
backticks in comments here — they prematurely close the template
|
||||
literal and break the entire sidebar at load. */
|
||||
.koolook-folder-picker { min-width: 480px; max-width: 600px; }
|
||||
.koolook-folder-picker-toolbar { display: flex; gap: 8px; align-items: center; margin: 4px 0 8px; }
|
||||
.koolook-folder-picker-toolbar { display: flex; gap: 8px; align-items: center; margin: 0 0 10px; }
|
||||
.koolook-folder-picker-up { flex-shrink: 0; }
|
||||
/* End-visible path overflow: 'direction: rtl' makes the input's
|
||||
overflow-axis right-to-left, 'unicode-bidi: plaintext' keeps the
|
||||
Latin path characters in natural left-to-right order. Net effect:
|
||||
long paths clip the LEFT (leading /Users/...) and always show the
|
||||
trailing folder name on the right. Per mockup section 6. */
|
||||
.koolook-folder-picker-path { flex: 1; min-width: 0; direction: rtl; text-align: left; unicode-bidi: plaintext; font-family: var(--font-family-mono, ui-monospace, monospace); font-size: 12px; }
|
||||
.koolook-folder-picker-list { max-height: 320px; min-height: 120px; overflow-y: auto; border: 1px solid rgba(255,255,255,0.08); border-radius: 4px; background: rgba(0,0,0,0.18); }
|
||||
.koolook-folder-picker-row { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; padding: 7px 12px; border: 0; border-bottom: 1px solid rgba(255,255,255,0.05); background: transparent; color: inherit; cursor: pointer; font: inherit; font-size: 12px; }
|
||||
.koolook-folder-picker-path { flex: 1; min-width: 0; direction: rtl; text-align: left; unicode-bidi: plaintext; font-family: ui-monospace, "Cascadia Mono", Menlo, monospace; font-size: 12px; }
|
||||
.koolook-folder-picker-list { max-height: 320px; min-height: 120px; overflow-y: auto; border: 1px solid var(--border-color, #302f2f); border-radius: 4px; background: var(--comfy-input-bg, #111111); overflow: hidden auto; }
|
||||
.koolook-folder-picker-row { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; padding: 10px 12px; border: 0; border-bottom: 1px solid var(--border-color, #302f2f); background: var(--comfy-input-bg, #111111); color: var(--input-text, #e6e8ec); cursor: pointer; font: inherit; font-size: 12px; }
|
||||
.koolook-folder-picker-row:last-child { border-bottom: none; }
|
||||
.koolook-folder-picker-row:hover { background: rgba(255,255,255,0.08); }
|
||||
.koolook-folder-picker-row-file { cursor: default; opacity: 0.45; }
|
||||
.koolook-folder-picker-row-file:hover { background: transparent; }
|
||||
.koolook-folder-picker-row:hover { background: rgba(80,140,235,0.16); background: color-mix(in srgb, var(--comfy-input-bg, #111111) 88%, var(--p-primary-color, #6db4ff)); }
|
||||
.koolook-folder-picker-row-file { cursor: default; color: var(--input-text, #8f959c); opacity: 0.68; }
|
||||
.koolook-folder-picker-row-file:hover { background: var(--comfy-input-bg, #111111); }
|
||||
.koolook-folder-picker-icon { font-size: 13px; line-height: 1; flex-shrink: 0; }
|
||||
.koolook-folder-picker-row:not(.koolook-folder-picker-row-file) .koolook-folder-picker-icon { filter: hue-rotate(-30deg) saturate(1.4); }
|
||||
.koolook-folder-picker-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.koolook-folder-picker-empty { padding: 18px 14px; opacity: 0.6; font-size: 12px; font-style: italic; text-align: center; }
|
||||
.koolook-folder-picker-empty { padding: 20px; color: var(--input-text, #8f959c); opacity: 0.68; font-size: 12px; font-style: italic; text-align: center; }
|
||||
.koolook-folder-picker-error { color: #ffae9a; font-style: normal; opacity: 0.9; }
|
||||
.koolook-folder-picker-spacer { flex: 1; }
|
||||
.koolook-folder-picker-newfolder-label { font-size: 11px; opacity: 0.6; flex-shrink: 0; }
|
||||
.koolook-folder-picker-newfolder-label { color: var(--input-text, #8f959c); opacity: 0.68; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; flex-shrink: 0; }
|
||||
.koolook-folder-picker-newfolder-input { flex: 1; min-width: 0; }
|
||||
/* Save dialog redesign (issue #137, mockup section 2). The library row
|
||||
at the top is purely informational — label + Open folder link sit on
|
||||
the top edge so a long absolute path can't overlap the link. Every
|
||||
action lives in the bottom command bar (Save to... | Cancel | Save
|
||||
as new... | Save). */
|
||||
.koolook-snap-lib-row { padding: 8px 10px; border: 1px solid rgba(255,255,255,0.06); border-radius: 4px; background: rgba(0,0,0,0.18); margin-bottom: 12px; }
|
||||
.koolook-snap-lib-row-top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 4px; }
|
||||
.koolook-snap-lib-label { font-size: 10px; opacity: 0.55; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.koolook-snap-open-folder-link { font-size: 11px; opacity: 0.7; color: var(--p-primary-color, rgba(120,170,255,1)); text-decoration: none; }
|
||||
.koolook-snap-lib-row { padding: 10px 12px; border: 1px solid var(--border-color, #302f2f); border-radius: 4px; background: var(--comfy-input-bg, #181818); margin-bottom: 14px; }
|
||||
.koolook-snap-lib-row-info { min-width: 0; }
|
||||
.koolook-snap-lib-row-top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 2px; }
|
||||
.koolook-snap-lib-label { color: var(--input-text, #8f959c); opacity: 0.68; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
.koolook-snap-open-folder-link { font-size: 11px; opacity: 1; color: #6db4ff; text-decoration: none; white-space: nowrap; }
|
||||
.koolook-snap-open-folder-link:hover { opacity: 1; text-decoration: underline; }
|
||||
.koolook-snap-save-message { margin: 6px 0 0; }
|
||||
/* Four-state primary button (mockup section 5). The in-progress and
|
||||
@@ -345,14 +350,9 @@ export function ensureStyle() {
|
||||
short-circuits naturally. */
|
||||
.koolook-snap-save-in-progress { opacity: 0.7; cursor: progress; }
|
||||
.koolook-snap-save-done { background: rgba(120, 200, 120, 0.18); border-color: rgba(120, 200, 120, 0.5); color: #b3e3b3; opacity: 0.95; cursor: default; }
|
||||
/* Load dialog redesign (issue #137, mockup section 3). The scoped
|
||||
recovery row sits as a sibling immediately after the preset row that
|
||||
triggered it, so the kind badge plus timestamp meta line up below
|
||||
the named row with no nested chrome. The inline delete state outlines
|
||||
the row in red and the bottom Close button transforms to Yes (delete).
|
||||
Reminder: no backticks in comments inside this CSS template literal. */
|
||||
.koolook-snapshot-scoped-recovery { margin: -1px 0 4px; padding-left: 14px; border-left: 2px solid rgba(120, 170, 255, 0.35); }
|
||||
.koolook-snapshot-scoped-recovery-row { background: rgba(80, 140, 235, 0.06); }
|
||||
.koolook-delete-confirm-text { align-self: center; color: #ffb1b1; font-size: 12px; font-weight: 600; }
|
||||
/* Inline delete state outlines the target row in red. Reminder: no
|
||||
backticks in comments inside this CSS template literal. */
|
||||
.koolook-snapshot-row-pending-delete { outline: 1.5px solid rgba(220, 80, 80, 0.75); outline-offset: -1px; background: rgba(220, 80, 80, 0.06); }
|
||||
`;
|
||||
document.head.appendChild(s);
|
||||
|
||||
+280
-75
@@ -6,7 +6,7 @@
|
||||
// click / Cancel / OK dismissal paths and ties the document-level keydown
|
||||
// listener to the overlay's lifetime so opening many modals doesn't leak.
|
||||
// =============================================================================
|
||||
import { compareNames, criticalToast, toast } from "./constants.js";
|
||||
import { compareNames, criticalToast, formatLibraryPathBreadcrumb, toast } from "./constants.js";
|
||||
import { formatLocalStamp } from "./format_time.js";
|
||||
import { listDirectoryNames, dirOf } from "./workflows_store.js";
|
||||
import {
|
||||
@@ -86,7 +86,7 @@ function makeModalShell({ title, titleTooltip, body, actions }) {
|
||||
escHandler = (e) => { if (e.key === "Escape") close(); };
|
||||
document.addEventListener("keydown", escHandler);
|
||||
document.body.appendChild(overlay);
|
||||
return { overlay, modal, close };
|
||||
return { overlay, modal, titleEl, close };
|
||||
}
|
||||
|
||||
function makeModalButton({ label, primary, danger, onClick }) {
|
||||
@@ -990,14 +990,14 @@ function todayStamp() {
|
||||
// for legacy rows whose listing endpoint didn't carry mtime.
|
||||
function formatPreviewMeta(p) {
|
||||
const parts = [];
|
||||
parts.push(`${p.workflowCount} workflow${p.workflowCount === 1 ? "" : "s"}`);
|
||||
parts.push(`${p.pickCount} pick${p.pickCount === 1 ? "" : "s"}`);
|
||||
parts.push(`${p.workflowCount} workflow${p.workflowCount === 1 ? "" : "s"}`);
|
||||
if (typeof p.mtime === "number" && isFinite(p.mtime)) {
|
||||
const stamp = formatLocalStamp(new Date(p.mtime * 1000));
|
||||
if (stamp) parts.push(`saved ${stamp}`);
|
||||
if (stamp) parts.push(p.kind ? stamp : `saved ${stamp}`);
|
||||
} else if (p.exportedAt) {
|
||||
const stamp = formatLocalStamp(new Date(p.exportedAt));
|
||||
if (stamp) parts.push(`exported ${stamp}`);
|
||||
if (stamp) parts.push(p.kind ? stamp : `exported ${stamp}`);
|
||||
}
|
||||
return parts.join(" · ");
|
||||
}
|
||||
@@ -1055,6 +1055,7 @@ export function showSaveSnapshotDialog({
|
||||
// → Dirty state → primary label becomes "Save to new folder".
|
||||
let originalLibraryPath = "";
|
||||
let currentLibraryPath = "";
|
||||
let libraryRevealInFlight = false;
|
||||
|
||||
const body = document.createElement("div");
|
||||
|
||||
@@ -1067,6 +1068,9 @@ export function showSaveSnapshotDialog({
|
||||
const libRow = document.createElement("div");
|
||||
libRow.className = "koolook-snap-lib-row";
|
||||
|
||||
const libRowInfo = document.createElement("div");
|
||||
libRowInfo.className = "koolook-snap-lib-row-info";
|
||||
|
||||
const libRowTop = document.createElement("div");
|
||||
libRowTop.className = "koolook-snap-lib-row-top";
|
||||
const libLabel = document.createElement("span");
|
||||
@@ -1082,25 +1086,30 @@ export function showSaveSnapshotDialog({
|
||||
openFolderLink.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
if (typeof revealPresetFolder !== "function") return;
|
||||
if (libraryRevealInFlight) return;
|
||||
libraryRevealInFlight = true;
|
||||
try {
|
||||
const r = await revealPresetFolder();
|
||||
toast(`Opened: ${r.path}`);
|
||||
} catch (err) {
|
||||
console.error("[Koolook] reveal failed:", err);
|
||||
toast(`Could not open library folder: ${err.message}`);
|
||||
} finally {
|
||||
libraryRevealInFlight = false;
|
||||
}
|
||||
});
|
||||
libRowTop.appendChild(openFolderLink);
|
||||
libRow.appendChild(libRowTop);
|
||||
libRowInfo.appendChild(libRowTop);
|
||||
|
||||
const libName = document.createElement("div");
|
||||
libName.className = "koolook-settings-folder-name";
|
||||
libName.textContent = "Library folder: (loading…)";
|
||||
libRow.appendChild(libName);
|
||||
libRowInfo.appendChild(libName);
|
||||
|
||||
const libPath = document.createElement("div");
|
||||
libPath.className = "koolook-settings-folder-path";
|
||||
libRow.appendChild(libPath);
|
||||
libRowInfo.appendChild(libPath);
|
||||
libRow.appendChild(libRowInfo);
|
||||
|
||||
body.appendChild(libRow);
|
||||
|
||||
@@ -1262,6 +1271,9 @@ export function showSaveSnapshotDialog({
|
||||
toast("Folder picker unavailable in this session.");
|
||||
return;
|
||||
}
|
||||
if (saveToBtn.disabled) return;
|
||||
saveToBtn.disabled = true;
|
||||
setTimeout(() => { saveToBtn.disabled = false; }, 0);
|
||||
showFolderPicker({
|
||||
title: "Save snapshots to",
|
||||
titleTooltip: "Pick the library folder this Save will write into.",
|
||||
@@ -1448,6 +1460,9 @@ export function showLoadSnapshotDialog({
|
||||
const libRow = document.createElement("div");
|
||||
libRow.className = "koolook-snap-lib-row";
|
||||
|
||||
const libRowInfo = document.createElement("div");
|
||||
libRowInfo.className = "koolook-snap-lib-row-info";
|
||||
|
||||
const libRowTop = document.createElement("div");
|
||||
libRowTop.className = "koolook-snap-lib-row-top";
|
||||
const libLabel = document.createElement("span");
|
||||
@@ -1463,42 +1478,71 @@ export function showLoadSnapshotDialog({
|
||||
openFolderLink.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
if (typeof revealPresetFolder !== "function") return;
|
||||
if (libraryRevealInFlight) return;
|
||||
libraryRevealInFlight = true;
|
||||
try {
|
||||
const r = await revealPresetFolder();
|
||||
toast(`Opened: ${r.path}`);
|
||||
} catch (err) {
|
||||
console.error("[Koolook] reveal failed:", err);
|
||||
toast(`Could not open library folder: ${err.message}`);
|
||||
} finally {
|
||||
libraryRevealInFlight = false;
|
||||
}
|
||||
});
|
||||
libRowTop.appendChild(openFolderLink);
|
||||
libRow.appendChild(libRowTop);
|
||||
libRowInfo.appendChild(libRowTop);
|
||||
|
||||
const libName = document.createElement("div");
|
||||
libName.className = "koolook-settings-folder-name";
|
||||
libName.textContent = "Library folder: (loading…)";
|
||||
libRow.appendChild(libName);
|
||||
libRowInfo.appendChild(libName);
|
||||
|
||||
const libPath = document.createElement("div");
|
||||
libPath.className = "koolook-settings-folder-path";
|
||||
libRow.appendChild(libPath);
|
||||
libRowInfo.appendChild(libPath);
|
||||
libRow.appendChild(libRowInfo);
|
||||
|
||||
body.appendChild(libRow);
|
||||
|
||||
const listWrap = document.createElement("div");
|
||||
body.appendChild(listWrap);
|
||||
|
||||
const recoverySection = document.createElement("div");
|
||||
recoverySection.className = "koolook-recovery-section";
|
||||
const recoverySummary = document.createElement("div");
|
||||
recoverySummary.className = "koolook-recovery-summary";
|
||||
recoverySummary.textContent = "▸ Recovery auto-saves";
|
||||
recoverySummary.classList.add("koolook-recovery-summary-passive");
|
||||
recoverySection.appendChild(recoverySummary);
|
||||
const recoveryContent = document.createElement("div");
|
||||
recoveryContent.className = "koolook-recovery-list";
|
||||
recoveryContent.hidden = true;
|
||||
recoverySection.appendChild(recoveryContent);
|
||||
recoverySummary.addEventListener("click", () => {
|
||||
if (scopedRecovery) {
|
||||
clearScopedRecovery();
|
||||
setDialogTitle("Load snapshot");
|
||||
applyCloseButtonState();
|
||||
}
|
||||
});
|
||||
body.appendChild(recoverySection);
|
||||
|
||||
let overlay;
|
||||
let titleEl;
|
||||
const close = () => overlay.remove();
|
||||
|
||||
// Per-dialog state. ``pendingDelete`` holds the preview whose × was
|
||||
// clicked and the row element currently outlined red; the Close
|
||||
// button's label flips to "Yes (delete)" until the user confirms or
|
||||
// Escape cancels. ``scopedRecoveryRow`` holds the inline single-row
|
||||
// recovery widget that opens beneath a preset row that has a newer
|
||||
// autosave — exactly one is shown at a time (mockup section 3).
|
||||
// Per-dialog state. ``pendingDelete`` holds the target whose × was
|
||||
// clicked and the row element currently outlined red; the command
|
||||
// bar swaps to a "Confirm delete?" prompt until the user confirms or
|
||||
// Escape cancels. ``scopedRecovery`` holds the bottom recovery
|
||||
// section state for one preset at a time (mockup section 3).
|
||||
let pendingDelete = null;
|
||||
let scopedRecoveryRow = null;
|
||||
let scopedRecovery = null;
|
||||
let selectedNamed = null;
|
||||
let selectedRecovery = null;
|
||||
let recoveryRequestId = 0;
|
||||
let libraryRevealInFlight = false;
|
||||
|
||||
function renderEmpty(text) {
|
||||
const el = document.createElement("div");
|
||||
@@ -1508,6 +1552,7 @@ export function showLoadSnapshotDialog({
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
cancelDelete();
|
||||
listWrap.innerHTML = "";
|
||||
listWrap.appendChild(renderEmpty("Loading…"));
|
||||
let previews;
|
||||
@@ -1515,6 +1560,7 @@ export function showLoadSnapshotDialog({
|
||||
catch (e) { previews = []; }
|
||||
listWrap.innerHTML = "";
|
||||
clearScopedRecovery();
|
||||
clearLoadSelection();
|
||||
if (previews.length === 0) {
|
||||
listWrap.appendChild(renderEmpty("No presets in this library yet. Use Save to create one."));
|
||||
return;
|
||||
@@ -1680,51 +1726,54 @@ export function showLoadSnapshotDialog({
|
||||
}
|
||||
|
||||
// ---- Click routing ----
|
||||
// Mockup section 3: clicking a preset that has a newer autosave does
|
||||
// NOT load it; it expands a single scoped recovery row beneath the
|
||||
// clicked preset. The user then re-clicks the named row (to load
|
||||
// named) or clicks the recovery row (to load the autosave). Presets
|
||||
// with no newer autosave skip the choice entirely — direct load.
|
||||
// Row clicks select; footer buttons commit. A newer recovery opens
|
||||
// the bottom section and changes the dialog title/button pair, but
|
||||
// there is no double-click-to-load convention.
|
||||
function onPresetClick(preview, rowEl) {
|
||||
// Cancel any pending delete-confirm on a different row.
|
||||
if (pendingDelete && pendingDelete.preview.fileName !== preview.fileName) {
|
||||
cancelDelete();
|
||||
}
|
||||
// A second click on the SAME preset that already has its scoped
|
||||
// recovery row open commits the named load — the user has seen
|
||||
// both options and is choosing the named version.
|
||||
if (scopedRecoveryRow && scopedRecoveryRow.parentPreview.fileName === preview.fileName) {
|
||||
doNamedLoad(preview);
|
||||
return;
|
||||
}
|
||||
if (selectedNamed?.preview.fileName === preview.fileName && !pendingDelete) return;
|
||||
clearLoadSelection();
|
||||
selectedNamed = { preview, rowEl };
|
||||
rowEl.classList.add("koolook-snapshot-row-selected");
|
||||
|
||||
clearScopedRecovery();
|
||||
if (typeof preview.latestAutosaveMtime === "number" &&
|
||||
typeof preview.mtime === "number" &&
|
||||
preview.latestAutosaveMtime > preview.mtime) {
|
||||
insertScopedRecovery(preview, rowEl);
|
||||
openScopedRecovery(preview);
|
||||
setDialogTitle("Auto-save is newer than the saved version");
|
||||
applyCloseButtonState();
|
||||
return;
|
||||
}
|
||||
// No newer autosave — direct load. The dialog already says
|
||||
// "Load snapshot" at the title; a second "Replace current state?"
|
||||
// confirm modal is the kind of extra friction the redesign drops.
|
||||
doNamedLoad(preview);
|
||||
setDialogTitle("Load snapshot");
|
||||
applyCloseButtonState();
|
||||
}
|
||||
|
||||
// Build a single inline row representing the newest recovery file
|
||||
// for the clicked preset, slotted in the DOM directly after the
|
||||
// preset row. Loading state shows briefly while listAutosaves()
|
||||
// resolves; error/empty states render in place so the user knows
|
||||
// the affordance opened but yielded nothing.
|
||||
async function insertScopedRecovery(preview, anchorRow) {
|
||||
const slot = document.createElement("div");
|
||||
slot.className = "koolook-snapshot-scoped-recovery";
|
||||
slot.appendChild(renderEmpty("Loading recovery…"));
|
||||
anchorRow.insertAdjacentElement("afterend", slot);
|
||||
scopedRecoveryRow = { el: slot, parentPreview: preview };
|
||||
async function openScopedRecovery(preview) {
|
||||
const requestId = ++recoveryRequestId;
|
||||
const expectedFileName = preview.fileName;
|
||||
scopedRecovery = { parentPreview: preview, item: null, rowEl: null, loading: true };
|
||||
recoverySummary.textContent = "▾ Recovery auto-saves";
|
||||
recoverySummary.classList.remove("koolook-recovery-summary-passive");
|
||||
recoveryContent.hidden = false;
|
||||
recoveryContent.innerHTML = "";
|
||||
recoveryContent.appendChild(renderEmpty("Loading recovery…"));
|
||||
|
||||
let items = [];
|
||||
if (typeof listAutosaves === "function") {
|
||||
try { items = await listAutosaves(); } catch (e) { items = []; }
|
||||
try {
|
||||
items = await listAutosaves();
|
||||
} catch (e) {
|
||||
items = [];
|
||||
toast("Could not load recovery list; saved snapshot is still available.");
|
||||
}
|
||||
}
|
||||
if (requestId !== recoveryRequestId ||
|
||||
selectedNamed?.preview.fileName !== expectedFileName) {
|
||||
return;
|
||||
}
|
||||
// Filter to this preset's autosave subdir AND pick the single
|
||||
// freshest entry by mtime. ``periodic.json`` and the rotated
|
||||
@@ -1735,59 +1784,142 @@ export function showLoadSnapshotDialog({
|
||||
const scoped = items
|
||||
.filter((item) => item.dir === subdir)
|
||||
.sort((a, b) => (b.mtime || 0) - (a.mtime || 0));
|
||||
slot.innerHTML = "";
|
||||
recoveryContent.innerHTML = "";
|
||||
if (scoped.length === 0) {
|
||||
slot.appendChild(renderEmpty("No recovery auto-save for this preset."));
|
||||
clearScopedRecovery();
|
||||
setDialogTitle("Load snapshot");
|
||||
applyCloseButtonState();
|
||||
return;
|
||||
}
|
||||
const newest = scoped[0];
|
||||
const list = document.createElement("div");
|
||||
list.className = "koolook-snapshot-list";
|
||||
|
||||
const group = document.createElement("div");
|
||||
group.className = "koolook-recovery-group";
|
||||
|
||||
const groupHead = document.createElement("div");
|
||||
groupHead.className = "koolook-recovery-group-head";
|
||||
const groupTitle = document.createElement("div");
|
||||
groupTitle.className = "koolook-recovery-group-title";
|
||||
groupTitle.textContent = subdir;
|
||||
groupHead.appendChild(groupTitle);
|
||||
|
||||
const groupOpen = document.createElement("a");
|
||||
groupOpen.className = "koolook-snap-open-folder-link";
|
||||
groupOpen.href = "#";
|
||||
groupOpen.textContent = "Open folder ↗";
|
||||
groupOpen.title = "Open this recovery folder in your file manager";
|
||||
let groupRevealInFlight = false;
|
||||
groupOpen.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
if (typeof revealPresetFolder !== "function") return;
|
||||
if (groupRevealInFlight) return;
|
||||
groupRevealInFlight = true;
|
||||
try {
|
||||
const r = await revealPresetFolder({ dir: subdir });
|
||||
toast(`Opened: ${r.path}`);
|
||||
} catch (err) {
|
||||
console.error("[Koolook] reveal recovery folder failed:", err);
|
||||
toast(`Could not open recovery folder: ${err.message}`);
|
||||
} finally {
|
||||
groupRevealInFlight = false;
|
||||
}
|
||||
});
|
||||
groupHead.appendChild(groupOpen);
|
||||
group.appendChild(groupHead);
|
||||
|
||||
const groupPath = document.createElement("div");
|
||||
groupPath.className = "koolook-recovery-group-path";
|
||||
const fullGroupPath = libraryPath ? `${libraryPath}/${subdir}/` : subdir;
|
||||
groupPath.textContent = formatLibraryPathBreadcrumb(fullGroupPath, 56);
|
||||
groupPath.title = fullGroupPath;
|
||||
group.appendChild(groupPath);
|
||||
|
||||
const row = document.createElement("div");
|
||||
row.className = "koolook-snapshot-row koolook-snapshot-scoped-recovery-row";
|
||||
row.className = "koolook-recovery-row";
|
||||
|
||||
const info = document.createElement("div");
|
||||
info.className = "koolook-snapshot-row-info";
|
||||
info.className = "koolook-recovery-row-info";
|
||||
info.title = "Click to load this auto-save instead of the named preset.";
|
||||
|
||||
const name = document.createElement("div");
|
||||
name.className = "koolook-snapshot-row-name";
|
||||
const kindBadge = document.createElement("span");
|
||||
kindBadge.className = "koolook-recovery-kind koolook-recovery-kind-" + newest.kind;
|
||||
kindBadge.textContent = newest.kind === "pre_load" ? "Pre-load" :
|
||||
newest.kind === "periodic" ? "Periodic" : "Other";
|
||||
name.appendChild(kindBadge);
|
||||
// Per locked-in decision: recovery rows show kind + timestamp +
|
||||
// meta, no filenames. Timestamp comes from formatPreviewMeta
|
||||
// (which already prefers mtime → "saved <local-stamp>").
|
||||
info.appendChild(name);
|
||||
info.appendChild(kindBadge);
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "koolook-snapshot-row-meta";
|
||||
meta.className = "koolook-recovery-row-meta";
|
||||
meta.textContent = formatPreviewMeta(newest);
|
||||
info.appendChild(meta);
|
||||
|
||||
info.addEventListener("click", () => doAutosaveRestore(newest));
|
||||
info.addEventListener("click", () => {
|
||||
if (pendingDelete) {
|
||||
cancelDelete();
|
||||
}
|
||||
if (selectedNamed) {
|
||||
selectedNamed.rowEl.classList.remove("koolook-snapshot-row-selected");
|
||||
selectedNamed = null;
|
||||
}
|
||||
if (selectedRecovery) selectedRecovery.rowEl.classList.remove("koolook-recovery-row-selected");
|
||||
selectedRecovery = { item: newest, rowEl: row };
|
||||
row.classList.add("koolook-recovery-row-selected");
|
||||
applyCloseButtonState();
|
||||
});
|
||||
row.appendChild(info);
|
||||
list.appendChild(row);
|
||||
slot.appendChild(list);
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "koolook-snapshot-row-actions";
|
||||
const delBtn = document.createElement("button");
|
||||
delBtn.className = "koolook-snapshot-row-btn koolook-snapshot-row-btn-danger";
|
||||
delBtn.textContent = "×";
|
||||
delBtn.title = "Delete this recovery auto-save";
|
||||
delBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
armDelete(newest, row, { dir: newest.dir });
|
||||
});
|
||||
actions.appendChild(delBtn);
|
||||
row.appendChild(actions);
|
||||
|
||||
group.appendChild(row);
|
||||
recoveryContent.appendChild(group);
|
||||
scopedRecovery = { parentPreview: preview, item: newest, rowEl: row };
|
||||
applyCloseButtonState();
|
||||
}
|
||||
|
||||
function clearScopedRecovery() {
|
||||
if (scopedRecoveryRow && scopedRecoveryRow.el && scopedRecoveryRow.el.parentNode) {
|
||||
scopedRecoveryRow.el.remove();
|
||||
recoveryRequestId += 1;
|
||||
recoverySummary.textContent = "▸ Recovery auto-saves";
|
||||
recoverySummary.classList.add("koolook-recovery-summary-passive");
|
||||
recoveryContent.hidden = true;
|
||||
recoveryContent.innerHTML = "";
|
||||
scopedRecovery = null;
|
||||
if (selectedRecovery) {
|
||||
selectedRecovery.rowEl.classList.remove("koolook-recovery-row-selected");
|
||||
selectedRecovery = null;
|
||||
}
|
||||
scopedRecoveryRow = null;
|
||||
}
|
||||
|
||||
function clearLoadSelection() {
|
||||
if (selectedNamed) selectedNamed.rowEl.classList.remove("koolook-snapshot-row-selected");
|
||||
if (selectedRecovery) selectedRecovery.rowEl.classList.remove("koolook-recovery-row-selected");
|
||||
selectedNamed = null;
|
||||
selectedRecovery = null;
|
||||
}
|
||||
|
||||
function setDialogTitle(text) {
|
||||
if (titleEl) titleEl.textContent = text;
|
||||
}
|
||||
|
||||
// ---- Inline delete confirm ----
|
||||
// Mockup-locked: × outlines the row red, Close transforms to
|
||||
// "Yes (delete)". A second confirm click commits; Escape cancels.
|
||||
function armDelete(preview, rowEl) {
|
||||
function armDelete(preview, rowEl, { dir = "" } = {}) {
|
||||
if (pendingDelete) cancelDelete();
|
||||
clearScopedRecovery();
|
||||
clearLoadSelection();
|
||||
if (!dir) clearScopedRecovery();
|
||||
rowEl.classList.add("koolook-snapshot-row-pending-delete");
|
||||
pendingDelete = { preview, rowEl };
|
||||
pendingDelete = { preview, rowEl, dir };
|
||||
setDialogTitle("Load snapshot");
|
||||
applyCloseButtonState();
|
||||
}
|
||||
|
||||
@@ -1802,8 +1934,9 @@ export function showLoadSnapshotDialog({
|
||||
const target = pendingDelete;
|
||||
if (!target) return;
|
||||
try {
|
||||
await deletePreset(target.preview.fileName);
|
||||
await deletePreset(target.preview.fileName, { dir: target.dir });
|
||||
if (typeof getCurrentPresetName === "function" &&
|
||||
!target.dir &&
|
||||
getCurrentPresetName() === target.preview.fileName) {
|
||||
setCurrentPresetName(null);
|
||||
}
|
||||
@@ -1824,6 +1957,29 @@ export function showLoadSnapshotDialog({
|
||||
onClick: () => openLoadFrom(),
|
||||
});
|
||||
|
||||
const confirmText = document.createElement("span");
|
||||
confirmText.className = "koolook-delete-confirm-text";
|
||||
confirmText.hidden = true;
|
||||
|
||||
const loadSavedBtn = makeModalButton({
|
||||
label: "NO - load saved",
|
||||
onClick: () => {
|
||||
if (!scopedRecovery) return;
|
||||
doNamedLoad(scopedRecovery.parentPreview);
|
||||
},
|
||||
});
|
||||
loadSavedBtn.hidden = true;
|
||||
|
||||
const loadLatestBtn = makeModalButton({
|
||||
label: "YES - load latest",
|
||||
primary: true,
|
||||
onClick: () => {
|
||||
if (!scopedRecovery || !scopedRecovery.item) return;
|
||||
doAutosaveRestore(selectedRecovery?.item || scopedRecovery.item);
|
||||
},
|
||||
});
|
||||
loadLatestBtn.hidden = true;
|
||||
|
||||
const closeBtn = makeModalButton({
|
||||
label: "Close",
|
||||
onClick: () => {
|
||||
@@ -1831,18 +1987,64 @@ export function showLoadSnapshotDialog({
|
||||
commitDelete();
|
||||
return;
|
||||
}
|
||||
if (selectedNamed) {
|
||||
doNamedLoad(selectedNamed.preview);
|
||||
return;
|
||||
}
|
||||
close();
|
||||
},
|
||||
});
|
||||
|
||||
function applyCloseButtonState() {
|
||||
if (pendingDelete) {
|
||||
closeBtn.textContent = `Yes — delete "${pendingDelete.preview.displayName}"`;
|
||||
loadFromBtn.hidden = true;
|
||||
loadSavedBtn.hidden = true;
|
||||
loadLatestBtn.hidden = true;
|
||||
confirmText.hidden = false;
|
||||
confirmText.textContent = `Confirm delete "${pendingDelete.preview.displayName}"?`;
|
||||
closeBtn.hidden = false;
|
||||
closeBtn.textContent = "Yes";
|
||||
closeBtn.classList.add("koolook-modal-btn-danger");
|
||||
closeBtn.classList.remove("koolook-modal-btn-primary");
|
||||
closeBtn.title = "Confirm deletion. Press Esc to cancel.";
|
||||
} else {
|
||||
} else if (scopedRecovery && scopedRecovery.item) {
|
||||
loadFromBtn.hidden = true;
|
||||
confirmText.hidden = true;
|
||||
confirmText.textContent = "";
|
||||
loadSavedBtn.hidden = false;
|
||||
loadLatestBtn.hidden = false;
|
||||
closeBtn.hidden = true;
|
||||
closeBtn.classList.remove("koolook-modal-btn-danger", "koolook-modal-btn-primary");
|
||||
} else if (scopedRecovery && scopedRecovery.loading) {
|
||||
loadFromBtn.hidden = true;
|
||||
confirmText.hidden = true;
|
||||
confirmText.textContent = "";
|
||||
loadSavedBtn.hidden = true;
|
||||
loadLatestBtn.hidden = true;
|
||||
closeBtn.hidden = false;
|
||||
closeBtn.textContent = "Close";
|
||||
closeBtn.classList.remove("koolook-modal-btn-danger", "koolook-modal-btn-primary");
|
||||
closeBtn.title = "";
|
||||
} else if (selectedNamed) {
|
||||
loadFromBtn.hidden = false;
|
||||
confirmText.hidden = true;
|
||||
confirmText.textContent = "";
|
||||
loadSavedBtn.hidden = true;
|
||||
loadLatestBtn.hidden = true;
|
||||
closeBtn.hidden = false;
|
||||
closeBtn.textContent = "Load";
|
||||
closeBtn.classList.add("koolook-modal-btn-primary");
|
||||
closeBtn.classList.remove("koolook-modal-btn-danger");
|
||||
closeBtn.title = `Load "${selectedNamed.preview.displayName}".`;
|
||||
} else {
|
||||
loadFromBtn.hidden = false;
|
||||
confirmText.hidden = true;
|
||||
confirmText.textContent = "";
|
||||
loadSavedBtn.hidden = true;
|
||||
loadLatestBtn.hidden = true;
|
||||
closeBtn.hidden = false;
|
||||
closeBtn.textContent = "Close";
|
||||
closeBtn.classList.remove("koolook-modal-btn-danger", "koolook-modal-btn-primary");
|
||||
closeBtn.title = "";
|
||||
}
|
||||
}
|
||||
@@ -1852,6 +2054,9 @@ export function showLoadSnapshotDialog({
|
||||
toast("Folder picker unavailable in this session.");
|
||||
return;
|
||||
}
|
||||
if (loadFromBtn.disabled) return;
|
||||
loadFromBtn.disabled = true;
|
||||
setTimeout(() => { loadFromBtn.disabled = false; }, 0);
|
||||
showFolderPicker({
|
||||
title: "Load snapshots from",
|
||||
titleTooltip: "Switch the snapshot library this Load is reading from.",
|
||||
@@ -1886,11 +2091,11 @@ export function showLoadSnapshotDialog({
|
||||
const spacer = document.createElement("span");
|
||||
spacer.className = "koolook-folder-picker-spacer";
|
||||
|
||||
({ overlay } = makeModalShell({
|
||||
({ overlay, titleEl } = makeModalShell({
|
||||
title: "Load snapshot",
|
||||
titleTooltip: "Restore the sidebar state from a preset file.",
|
||||
body,
|
||||
actions: [loadFromBtn, spacer, closeBtn],
|
||||
actions: [loadFromBtn, confirmText, spacer, loadSavedBtn, loadLatestBtn, closeBtn],
|
||||
}));
|
||||
|
||||
// Escape-to-cancel for inline delete. The modal shell already wires
|
||||
|
||||
Reference in New Issue
Block a user