afterimage-cli 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/agents/cursor/afterimage.mdc +2 -2
- package/agents/skill/SKILL.md +2 -2
- package/package.json +1 -1
- package/src/runtime/overlay.js +140 -57
- package/viewer-dist/assets/{index-29Tm-kiu.js → index-DywHG8vc.js} +1 -1
- package/viewer-dist/index.html +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
- **The pill is the viewer's own bar.** It sits at the bottom of the page and reads Version N of N, Highlight, Compare and Save. Each one opens the viewer straight in that state, on the page and scroll you are on, so Highlight looks like the outlines switching on in place. There is no History step and no close button: turn Highlight off, leave Compare or close the list (or press Esc) and you are back on the live page, on the route you left the viewer on.
|
|
6
|
+
- **No flash when it opens.** Pointing at the pill loads the viewer out of sight, and it only shows once it is on the right page. Before, a dark layer covered the page while the viewer loaded.
|
|
7
|
+
- **Hash-routed apps compare page with page.** In an app that changes page by its hash (`#/orders`), the older version now follows to the same page. Before, it stayed on the first page, and the two different pages counted as a thousand changes. Change counts are kept per page.
|
|
8
|
+
- **Moves caused by an edit are not changes of their own.** When a version has real edits, the elements that only moved or resized because of them (the cells beside a column that got wider, the rows under one that got taller) are no longer outlined or counted. A version whose only difference is a move still shows it. Counts the version list cached under the old rule are worked out again.
|
|
9
|
+
|
|
3
10
|
## 0.2.0
|
|
4
11
|
|
|
5
12
|
- **The pill.** Afterimage now sits on the prototype itself. Save keeps the current state as the next version, and History opens every version over the page, in the same tab; Esc or the close button returns to the prototype. On a deployed site the same pill shows everyone the versions, without Save. It draws in a shadow root, so the app's CSS can't reach it, and it stays out of saved versions.
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Run your prototype:
|
|
|
35
35
|
npm run dev
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
The pill sits at the bottom
|
|
38
|
+
The pill sits at the bottom of the page and is the same bar the viewer has. Highlight outlines what changed since the version before, on the page you are on; Compare shows the two side by side; Version N of N lists every version. Turn it off again, or press Esc, and you are back on the live page. Save keeps the current state as the next version. Once you deploy, the same pill on the live site shows everyone the versions, without Save.
|
|
39
39
|
|
|
40
40
|
You can also save from the command line, or have your agent do it:
|
|
41
41
|
|
|
@@ -5,7 +5,7 @@ alwaysApply: false
|
|
|
5
5
|
|
|
6
6
|
# Afterimage
|
|
7
7
|
|
|
8
|
-
Afterimage keeps a version history of this prototype. Each save builds the prototype and stores the built page as the next version (v1, v2, v3 and so on) in `afterimage/`. A pill sits on the running prototype: Save keeps the current state as the next version
|
|
8
|
+
Afterimage keeps a version history of this prototype. Each save builds the prototype and stores the built page as the next version (v1, v2, v3 and so on) in `afterimage/`. A pill sits on the running prototype: Save keeps the current state as the next version; Highlight outlines what changed since the version before, right on the page; Compare and Version N of N open the rest of the history in place. On the deployed site the same pill shows the history to everyone who opens it. A version is the built page, kept to compare against and to share. It is not a backup of the source: the source belongs in git. Run every command from the project root, where `afterimage.config.json` is.
|
|
9
9
|
|
|
10
10
|
The user works on the prototype, not in a terminal. Do the steps yourself and reply in a line or two, with a link when there is one. Never hand the user a command to run.
|
|
11
11
|
|
|
@@ -91,7 +91,7 @@ With dev on its default port, a comparison is `http://localhost:4321/afterimage/
|
|
|
91
91
|
- **"Save a version called Remember me with a note that it keeps people signed in for 30 days."** Run `npx afterimage save "Remember me" --note "Keeps people signed in for 30 days."` and tell the user the id it printed, in one line.
|
|
92
92
|
- **"What changed since v2?"** Read `afterimage/versions.json`. If v2 is the latest version, tell the user nothing has changed since v2, because it is the newest version, and don't open a comparison. If v2 isn't listed, say so. Otherwise make sure dev is running and give the user the history's URL with `?version=<latest>&mode=compare&base=v2` added. The viewer shows the changes, so don't write a text diff. It compares saved versions: if the prototype has changed since the last save, say that those changes aren't in the comparison.
|
|
93
93
|
- **"Get the version history ready to deploy."** Run `npx afterimage build`. If the project isn't a git repository, tell the user the history has to be committed to the repository the site deploys from, and stop. Otherwise run `git status --short -- afterimage afterimage.config.json package.json package-lock.json pnpm-lock.yaml yarn.lock bun.lock bun.lockb .gitattributes vercel.json netlify.toml` and tell the user which of those aren't committed yet, since the host only sees what is in git. The rest of the project is the user's business. Tell them the history will be at `/afterimage/` on the deployed site, behind the same pill. Don't commit, push or deploy unless the user asks.
|
|
94
|
-
- **"Show me the version history."** Make sure dev is running, then tell the user to press
|
|
94
|
+
- **"Show me the version history."** Make sure dev is running, then tell the user to press Version N of N on the pill, and give them the history's URL too.
|
|
95
95
|
|
|
96
96
|
## Rules
|
|
97
97
|
|
package/agents/skill/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Adds Afterimage (afterimage-cli) to this prototype and saves, names
|
|
|
5
5
|
|
|
6
6
|
# Afterimage
|
|
7
7
|
|
|
8
|
-
Afterimage keeps a version history of this prototype. Each save builds the prototype and stores the built page as the next version (v1, v2, v3 and so on) in `afterimage/`. A pill sits on the running prototype: Save keeps the current state as the next version
|
|
8
|
+
Afterimage keeps a version history of this prototype. Each save builds the prototype and stores the built page as the next version (v1, v2, v3 and so on) in `afterimage/`. A pill sits on the running prototype: Save keeps the current state as the next version; Highlight outlines what changed since the version before, right on the page; Compare and Version N of N open the rest of the history in place. On the deployed site the same pill shows the history to everyone who opens it. A version is the built page, kept to compare against and to share. It is not a backup of the source: the source belongs in git. Run every command from the project root, where `afterimage.config.json` is.
|
|
9
9
|
|
|
10
10
|
The user works on the prototype, not in a terminal. Do the steps yourself and reply in a line or two, with a link when there is one. Never hand the user a command to run.
|
|
11
11
|
|
|
@@ -91,7 +91,7 @@ With dev on its default port, a comparison is `http://localhost:4321/afterimage/
|
|
|
91
91
|
- **"Save a version called Remember me with a note that it keeps people signed in for 30 days."** Run `npx afterimage save "Remember me" --note "Keeps people signed in for 30 days."` and tell the user the id it printed, in one line.
|
|
92
92
|
- **"What changed since v2?"** Read `afterimage/versions.json`. If v2 is the latest version, tell the user nothing has changed since v2, because it is the newest version, and don't open a comparison. If v2 isn't listed, say so. Otherwise make sure dev is running and give the user the history's URL with `?version=<latest>&mode=compare&base=v2` added. The viewer shows the changes, so don't write a text diff. It compares saved versions: if the prototype has changed since the last save, say that those changes aren't in the comparison.
|
|
93
93
|
- **"Get the version history ready to deploy."** Run `npx afterimage build`. If the project isn't a git repository, tell the user the history has to be committed to the repository the site deploys from, and stop. Otherwise run `git status --short -- afterimage afterimage.config.json package.json package-lock.json pnpm-lock.yaml yarn.lock bun.lock bun.lockb .gitattributes vercel.json netlify.toml` and tell the user which of those aren't committed yet, since the host only sees what is in git. The rest of the project is the user's business. Tell them the history will be at `/afterimage/` on the deployed site, behind the same pill. Don't commit, push or deploy unless the user asks.
|
|
94
|
-
- **"Show me the version history."** Make sure dev is running, then tell the user to press
|
|
94
|
+
- **"Show me the version history."** Make sure dev is running, then tell the user to press Version N of N on the pill, and give them the history's URL too.
|
|
95
95
|
|
|
96
96
|
## Rules
|
|
97
97
|
|
package/package.json
CHANGED
package/src/runtime/overlay.js
CHANGED
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
// How this copy was loaded says which, so a built site never asks for a helper, even served from this machine:
|
|
9
9
|
// - a script tag dev served carries dev's stamp (window.__afterimageDev); any other script tag is a built site's copy.
|
|
10
10
|
// - an import has no currentScript. Bundlers replace process.env.NODE_ENV, so a production build says it is one.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
//
|
|
12
|
+
// The pill is the viewer's own bar, in the same place: Version N of N, Highlight, Compare, and Save. Pointing at it
|
|
13
|
+
// loads the viewer, hidden, over the page; a button asks the viewer for that state, on the page's route and scroll, and
|
|
14
|
+
// the viewer only shows once it is there, so the page seems to switch the outlines on in place. Turning everything off
|
|
15
|
+
// again (or Esc) hands the page back, where the visitor left the viewer. The messages are in viewer/hooks/usePageHost.js.
|
|
13
16
|
//
|
|
14
17
|
// It is plain JavaScript with no imports, so it runs as a classic script and as a module, and it does nothing where
|
|
15
18
|
// there is no document (a server render of a bundled app). It draws in a shadow root, so the app's CSS can't reach it.
|
|
@@ -35,6 +38,7 @@
|
|
|
35
38
|
} catch { /* a script tag */ }
|
|
36
39
|
const inDev = local && (scriptTag ? servedByDev : mode !== 'production');
|
|
37
40
|
const helperPorts = Array.from({ length: 10 }, (_, i) => 4321 + i);
|
|
41
|
+
const OPEN_TIMEOUT_MS = 12000;
|
|
38
42
|
|
|
39
43
|
const getJson = async (url, timeout = 1500) => {
|
|
40
44
|
try {
|
|
@@ -72,44 +76,51 @@
|
|
|
72
76
|
return null;
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
// The viewer's pill (viewer/pill/pill.css and styles/tokens.css), so the two read as one control.
|
|
75
80
|
const css = `
|
|
76
81
|
:host { all: initial; }
|
|
77
82
|
* { box-sizing: border-box; }
|
|
78
|
-
.dock { position: fixed; right:
|
|
83
|
+
.dock { position: fixed; left: 0; right: 0; bottom: 16px; z-index: 2147483646; display: flex; justify-content: center;
|
|
84
|
+
padding: 0 16px; pointer-events: none; font: 13px/1 system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
79
85
|
-webkit-font-smoothing: antialiased; color: #ededed; }
|
|
80
|
-
.pill { display: flex; align-items: center; gap: 2px; padding: 5px; border-radius: 999px; background: #1b1b1b;
|
|
81
|
-
box-shadow: 0 0 0 1px #ffffff14, 0 1px 2px #0000004d, 0 10px 28px #00000040;
|
|
82
|
-
|
|
83
|
-
.
|
|
84
|
-
.label b { color: #ededed; font-weight: 500; }
|
|
86
|
+
.pill { display: flex; align-items: center; gap: 2px; max-width: 100%; padding: 5px; border-radius: 999px; background: #1b1b1b;
|
|
87
|
+
white-space: nowrap; box-shadow: 0 0 0 1px #ffffff14, 0 1px 2px #0000004d, 0 10px 28px #00000040; pointer-events: auto;
|
|
88
|
+
user-select: none; -webkit-user-select: none; }
|
|
89
|
+
.num { font-family: ui-monospace, 'SF Mono', 'Cascadia Mono', Menlo, Consolas, monospace; font-variant-numeric: tabular-nums; font-size: 12px; }
|
|
85
90
|
.sep { flex: none; width: 1px; height: 18px; margin: 0 4px; background: #ffffff14; }
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
.label { padding: 0 12px; color: #8e8e8e; }
|
|
92
|
+
button { appearance: none; display: inline-flex; align-items: center; gap: 8px; height: 34px; margin: 0; padding: 0 12px;
|
|
93
|
+
border: 0; border-radius: 999px; background: transparent; color: inherit; font: inherit; line-height: 1; white-space: nowrap;
|
|
94
|
+
cursor: pointer; touch-action: manipulation; -webkit-tap-highlight-color: transparent;
|
|
95
|
+
transition: background-color 150ms ease, color 150ms ease, transform 120ms cubic-bezier(0.22, 1, 0.36, 1); }
|
|
96
|
+
button:active:not(:disabled) { transform: scale(0.97); }
|
|
89
97
|
button:disabled { color: #5c5c5c; cursor: default; }
|
|
90
|
-
|
|
98
|
+
@media (hover: hover) { button:hover:not(:disabled) { background: #2a2a2a; } }
|
|
99
|
+
button:focus-visible, input:focus-visible { outline: 2px solid #ededed8c; outline-offset: -2px; }
|
|
100
|
+
.dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: #6fbf73; }
|
|
101
|
+
.field { display: inline-grid; place-items: center; min-width: 24px; height: 24px; padding: 0 6px; margin-left: -2px; border-radius: 6px;
|
|
102
|
+
background: #ffffff0d; color: #ededed; }
|
|
103
|
+
.of { color: #8e8e8e; margin-left: -4px; }
|
|
104
|
+
.of .num { margin-left: 4px; }
|
|
91
105
|
.primary { background: #ededed; color: #1b1b1b; }
|
|
92
|
-
.primary:hover:not(:disabled) { background: #ffffff; }
|
|
106
|
+
@media (hover: hover) { .primary:hover:not(:disabled) { background: #ffffff; } }
|
|
107
|
+
button[aria-busy='true']::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: #e5a83b;
|
|
108
|
+
animation: pulse 0.8s ease-in-out infinite alternate; }
|
|
93
109
|
input { width: 180px; height: 34px; margin: 0 2px 0 0; padding: 0 12px; border: 0; border-radius: 999px; background: #2a2a2a;
|
|
94
110
|
color: #ededed; font: inherit; }
|
|
95
111
|
input::placeholder { color: #8e8e8e; }
|
|
96
|
-
.status { padding: 0 12px
|
|
97
|
-
.status.bad { color: #e0705f; white-space: normal; max-width: 320px; line-height: 1.35; padding: 6px 12px
|
|
112
|
+
.status { padding: 0 12px; }
|
|
113
|
+
.status.bad { color: #e0705f; white-space: normal; max-width: 320px; line-height: 1.35; padding: 6px 12px; }
|
|
98
114
|
.busy { display: inline-block; width: 6px; height: 6px; margin-right: 8px; border-radius: 50%; background: #e5a83b;
|
|
99
115
|
vertical-align: 1px; animation: pulse 1s ease-in-out infinite alternate; }
|
|
100
116
|
@keyframes pulse { from { opacity: 0.35; } to { opacity: 1; } }
|
|
101
|
-
|
|
102
|
-
.layer
|
|
103
|
-
.layer
|
|
104
|
-
.
|
|
105
|
-
box-shadow: 0 0 0 1px #ffffff14, 0 6px 16px #00000040; color: #ededed; }
|
|
106
|
-
.close:hover:not(:disabled) { background: #2a2a2a; }
|
|
117
|
+
/* The viewer waits here, laid out but unseen, until it shows what was asked for. Not display:none, so it loads. */
|
|
118
|
+
.layer { position: fixed; inset: 0; z-index: 2147483647; opacity: 0; pointer-events: none; }
|
|
119
|
+
.layer[data-shown] { opacity: 1; pointer-events: auto; }
|
|
120
|
+
.layer iframe { display: block; width: 100%; height: 100%; border: 0; background: transparent; }
|
|
107
121
|
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
108
|
-
@media (prefers-reduced-motion: reduce) {
|
|
122
|
+
@media (prefers-reduced-motion: reduce) { button { transition: none; } .busy, button[aria-busy='true']::before { animation: none; } }
|
|
109
123
|
`;
|
|
110
|
-
// The wipe mark from afterimage's own icon: the first version on the left, today's on the right.
|
|
111
|
-
const mark = '<svg width="18" height="18" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="7" fill="#ededed"/><path d="M7 0h9v32H7a7 7 0 0 1-7-7V7a7 7 0 0 1 7-7z" fill="#8e8e8e"/><rect x="15" y="5" width="2" height="22" rx="1" fill="#1b1b1b"/></svg>';
|
|
112
|
-
const cross = '<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><path d="M2 2l8 8M10 2l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>';
|
|
113
124
|
|
|
114
125
|
const host = document.createElement('afterimage-overlay');
|
|
115
126
|
const root = host.attachShadow({ mode: 'open' });
|
|
@@ -119,7 +130,10 @@
|
|
|
119
130
|
let view = 'idle';
|
|
120
131
|
let message = '';
|
|
121
132
|
let timer = 0;
|
|
122
|
-
|
|
133
|
+
// The viewer, once the pill was pointed at: { el, frame, origin, versions, listening, asked, shown }.
|
|
134
|
+
// asked is { view, button } from a click until the viewer is ready.
|
|
135
|
+
let viewer = null;
|
|
136
|
+
let openTimer = 0;
|
|
123
137
|
let returnFocus = null;
|
|
124
138
|
|
|
125
139
|
const dock = make('div', { className: 'dock' });
|
|
@@ -136,15 +150,15 @@
|
|
|
136
150
|
clearTimeout(timer);
|
|
137
151
|
timer = setTimeout(next, ms);
|
|
138
152
|
};
|
|
139
|
-
const button = (
|
|
140
|
-
const element = make('button', { type: 'button',
|
|
153
|
+
const button = (content, onClick, extra = {}) => {
|
|
154
|
+
const element = make('button', { type: 'button', ...extra });
|
|
155
|
+
element.append(...[content].flat());
|
|
141
156
|
element.addEventListener('click', onClick);
|
|
142
157
|
return element;
|
|
143
158
|
};
|
|
144
159
|
|
|
145
160
|
function render() {
|
|
146
161
|
pill.replaceChildren();
|
|
147
|
-
pill.insertAdjacentHTML('beforeend', `<span class="mark">${mark}</span>`);
|
|
148
162
|
const versions = state.versions;
|
|
149
163
|
|
|
150
164
|
if (view === 'naming') {
|
|
@@ -166,12 +180,26 @@
|
|
|
166
180
|
return;
|
|
167
181
|
}
|
|
168
182
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
183
|
+
if (!versions) {
|
|
184
|
+
pill.append(make('span', { className: 'label', textContent: 'No versions yet' }));
|
|
185
|
+
if (state.canSave) pill.append(button('Save first version', () => { view = 'naming'; render(); }, { className: 'primary' }));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Version N of N opens the list of versions, as it does in the viewer. A first version has nothing to compare.
|
|
189
|
+
const number = Number(state.latest.id.slice(1)) || versions;
|
|
190
|
+
const single = versions < 2;
|
|
191
|
+
pill.append(
|
|
192
|
+
button([
|
|
193
|
+
make('span', { className: 'dot' }),
|
|
194
|
+
'Version',
|
|
195
|
+
make('span', { className: 'field num', textContent: String(number) }),
|
|
196
|
+
make('span', { className: 'of', innerHTML: `of<span class="num">${versions}</span>` }),
|
|
197
|
+
], event => open('versions', event), { ariaLabel: `Version ${number} of ${versions}, latest. Show versions` }),
|
|
198
|
+
make('span', { className: 'sep' }),
|
|
199
|
+
button('Highlight', event => open('highlight', event), { disabled: single, ariaLabel: single ? 'Highlight changes, not available on the first version' : 'Highlight changes' }),
|
|
200
|
+
button('Compare', event => open('compare', event), { disabled: single }),
|
|
201
|
+
);
|
|
202
|
+
if (state.canSave) pill.append(make('span', { className: 'sep' }), button('Save', () => { view = 'naming'; render(); }));
|
|
175
203
|
}
|
|
176
204
|
|
|
177
205
|
function back() {
|
|
@@ -206,42 +234,97 @@
|
|
|
206
234
|
later(back, 2400);
|
|
207
235
|
}
|
|
208
236
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
root.append(
|
|
237
|
+
// Loads the viewer behind the page, on the page's route, so a click finds it ready. A save since then made a version
|
|
238
|
+
// the loaded viewer doesn't know, so that one is replaced.
|
|
239
|
+
function prepare() {
|
|
240
|
+
if (!state?.versions || viewer?.shown || viewer?.asked) return;
|
|
241
|
+
if (viewer && viewer.versions === state.versions) return;
|
|
242
|
+
viewer?.el.remove();
|
|
243
|
+
const el = make('div', { className: 'layer' });
|
|
244
|
+
el.inert = true;
|
|
245
|
+
el.setAttribute('aria-hidden', 'true');
|
|
246
|
+
const frame = make('iframe', { src: `${state.base}/afterimage/${location.hash}`, title: 'Version history', tabIndex: -1 });
|
|
247
|
+
el.append(frame);
|
|
248
|
+
root.append(el);
|
|
249
|
+
viewer = { el, frame, origin: new URL(state.base).origin, versions: state.versions, listening: false, asked: null, shown: false };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// A click can come before the viewer has started; then it is asked as soon as it says it is listening.
|
|
253
|
+
function ask() {
|
|
254
|
+
if (!viewer?.listening || !viewer.asked) return;
|
|
255
|
+
viewer.frame.contentWindow.postMessage({ type: 'afterimage:open', view: viewer.asked.view, route: location.hash, scrollY: window.scrollY }, viewer.origin);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function open(name, event) {
|
|
259
|
+
if (viewer?.shown || viewer?.asked) return;
|
|
260
|
+
prepare();
|
|
261
|
+
const pressed = event?.currentTarget ?? null;
|
|
262
|
+
returnFocus = pressed;
|
|
263
|
+
pressed?.setAttribute('aria-busy', 'true');
|
|
264
|
+
viewer.asked = { view: name, button: pressed };
|
|
265
|
+
ask();
|
|
266
|
+
clearTimeout(openTimer);
|
|
267
|
+
openTimer = setTimeout(() => {
|
|
268
|
+
// The viewer never got there (the helper stopped, say): start from scratch next time.
|
|
269
|
+
viewer?.el.remove();
|
|
270
|
+
viewer = null;
|
|
271
|
+
view = 'failed';
|
|
272
|
+
message = "Couldn't open the history. Is afterimage dev still running?";
|
|
273
|
+
render();
|
|
274
|
+
say(message);
|
|
275
|
+
later(back, 6000);
|
|
276
|
+
}, OPEN_TIMEOUT_MS);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function show() {
|
|
280
|
+
if (!viewer?.asked) return;
|
|
281
|
+
clearTimeout(openTimer);
|
|
282
|
+
viewer.asked.button?.removeAttribute('aria-busy');
|
|
283
|
+
viewer.asked = null;
|
|
284
|
+
viewer.shown = true;
|
|
285
|
+
const { el } = viewer;
|
|
286
|
+
el.inert = false;
|
|
287
|
+
el.removeAttribute('aria-hidden');
|
|
288
|
+
el.setAttribute('role', 'dialog');
|
|
289
|
+
el.setAttribute('aria-modal', 'true');
|
|
290
|
+
el.setAttribute('aria-label', 'Version history');
|
|
291
|
+
el.setAttribute('data-shown', '');
|
|
221
292
|
dock.hidden = true;
|
|
222
293
|
document.documentElement.style.setProperty('overflow', 'hidden');
|
|
223
|
-
|
|
224
|
-
frame.addEventListener('load', () => frame.focus(), { once: true });
|
|
225
|
-
closer.focus();
|
|
294
|
+
viewer.frame.focus();
|
|
226
295
|
}
|
|
227
296
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
297
|
+
// The viewer stays loaded for next time. The page takes the route and scroll the visitor left the viewer on.
|
|
298
|
+
function hide({ route, scrollY } = {}) {
|
|
299
|
+
if (!viewer?.shown) return;
|
|
300
|
+
viewer.shown = false;
|
|
301
|
+
const { el } = viewer;
|
|
302
|
+
el.removeAttribute('data-shown');
|
|
303
|
+
el.removeAttribute('role');
|
|
304
|
+
el.removeAttribute('aria-modal');
|
|
305
|
+
el.setAttribute('aria-hidden', 'true');
|
|
306
|
+
el.inert = true;
|
|
232
307
|
dock.hidden = false;
|
|
233
308
|
document.documentElement.style.removeProperty('overflow');
|
|
234
|
-
|
|
309
|
+
if (typeof route === 'string' && route !== location.hash && (route || location.hash)) location.hash = route;
|
|
310
|
+
if (typeof scrollY === 'number') requestAnimationFrame(() => requestAnimationFrame(() => window.scrollTo(0, scrollY)));
|
|
311
|
+
returnFocus?.focus?.({ preventScroll: true });
|
|
235
312
|
// A save the agent made from the terminal meanwhile shows up in the count.
|
|
236
313
|
refresh();
|
|
237
314
|
}
|
|
238
315
|
|
|
239
316
|
window.addEventListener('keydown', event => {
|
|
240
|
-
if (event.key === 'Escape' &&
|
|
317
|
+
if (event.key === 'Escape' && viewer?.shown) hide();
|
|
241
318
|
});
|
|
242
319
|
window.addEventListener('message', event => {
|
|
243
|
-
if (
|
|
320
|
+
if (!viewer || event.source !== viewer.frame.contentWindow || event.origin !== viewer.origin) return;
|
|
321
|
+
const type = event.data?.type;
|
|
322
|
+
if (type === 'afterimage:listening') { viewer.listening = true; ask(); }
|
|
323
|
+
if (type === 'afterimage:ready') show();
|
|
324
|
+
if (type === 'afterimage:close') hide(event.data);
|
|
244
325
|
});
|
|
326
|
+
pill.addEventListener('pointerenter', prepare);
|
|
327
|
+
pill.addEventListener('focusin', prepare);
|
|
245
328
|
|
|
246
329
|
async function refresh() {
|
|
247
330
|
if (!state || view !== 'idle') return;
|