@oharness/server 0.10.0 → 0.11.0
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/dist/analytics.d.ts +32 -29
- package/dist/analytics.js +225 -278
- package/dist/analytics.js.map +1 -1
- package/dist/i18n.js +152 -8
- package/dist/i18n.js.map +1 -1
- package/dist/icons.js +1 -0
- package/dist/icons.js.map +1 -1
- package/dist/model-order.js +1 -0
- package/dist/model-order.js.map +1 -1
- package/dist/page/client/account.js +140 -4
- package/dist/page/client/account.js.map +1 -1
- package/dist/page/client/base.js +40 -16
- package/dist/page/client/base.js.map +1 -1
- package/dist/page/client/browser.js +1 -1
- package/dist/page/client/composer.js +111 -7
- package/dist/page/client/composer.js.map +1 -1
- package/dist/page/client/dialogs.js +75 -0
- package/dist/page/client/dialogs.js.map +1 -1
- package/dist/page/client/events.js +25 -2
- package/dist/page/client/events.js.map +1 -1
- package/dist/page/client/index.js +4 -0
- package/dist/page/client/index.js.map +1 -1
- package/dist/page/client/panels.js +4 -4
- package/dist/page/client/ripple.d.ts +13 -0
- package/dist/page/client/ripple.js +247 -0
- package/dist/page/client/ripple.js.map +1 -0
- package/dist/page/client/routines.js +173 -41
- package/dist/page/client/routines.js.map +1 -1
- package/dist/page/client/shell.js +200 -54
- package/dist/page/client/shell.js.map +1 -1
- package/dist/page/client/state.js +7 -2
- package/dist/page/client/state.js.map +1 -1
- package/dist/page/client/terminal.js +34 -7
- package/dist/page/client/terminal.js.map +1 -1
- package/dist/page/client/theme.d.ts +17 -0
- package/dist/page/client/theme.js +365 -0
- package/dist/page/client/theme.js.map +1 -0
- package/dist/page/client/transcript.js +63 -7
- package/dist/page/client/transcript.js.map +1 -1
- package/dist/page/style.js +183 -22
- package/dist/page/style.js.map +1 -1
- package/dist/page.js +4 -5
- package/dist/page.js.map +1 -1
- package/dist/prefs.d.ts +17 -0
- package/dist/prefs.js +5 -0
- package/dist/prefs.js.map +1 -1
- package/dist/server.js +465 -42
- package/dist/server.js.map +1 -1
- package/dist/suggest.d.ts +42 -0
- package/dist/suggest.js +215 -0
- package/dist/suggest.js.map +1 -0
- package/dist/terminal.d.ts +28 -2
- package/dist/terminal.js +45 -3
- package/dist/terminal.js.map +1 -1
- package/dist/theme-assets.d.ts +39 -0
- package/dist/theme-assets.js +109 -0
- package/dist/theme-assets.js.map +1 -0
- package/dist/theme-store.d.ts +50 -0
- package/dist/theme-store.js +124 -0
- package/dist/theme-store.js.map +1 -0
- package/dist/theme-tools.d.ts +25 -0
- package/dist/theme-tools.js +243 -0
- package/dist/theme-tools.js.map +1 -0
- package/dist/turn-shots.d.ts +18 -2
- package/dist/turn-shots.js +10 -1
- package/dist/turn-shots.js.map +1 -1
- package/dist/user-theme.d.ts +143 -0
- package/dist/user-theme.js +510 -0
- package/dist/user-theme.js.map +1 -0
- package/package.json +2 -2
|
@@ -807,5 +807,80 @@ function closePairing(id) {
|
|
|
807
807
|
dialog.remove();
|
|
808
808
|
}
|
|
809
809
|
|
|
810
|
+
/**
|
|
811
|
+
* The app's own confirm and prompt, in place of the browser's. Those carry the
|
|
812
|
+
* page's address as a title ("127.0.0.1:4314 says"), take the OS's look, and
|
|
813
|
+
* block the whole page; these match every other dialog here and resolve a
|
|
814
|
+
* promise. Escape and Cancel are the same answer: false, or null for text.
|
|
815
|
+
*/
|
|
816
|
+
function askDialog(opts, withField) {
|
|
817
|
+
return new Promise((resolve) => {
|
|
818
|
+
const dialog = el("dialog", "ask-dialog");
|
|
819
|
+
if (opts.title) dialog.appendChild(el("div", "ask-dialog-title", opts.title));
|
|
820
|
+
if (opts.body) dialog.appendChild(el("div", "ask-dialog-body", opts.body));
|
|
821
|
+
let field = null;
|
|
822
|
+
if (withField) {
|
|
823
|
+
field = el("input", "ask-dialog-field");
|
|
824
|
+
field.type = "text";
|
|
825
|
+
field.value = opts.value || "";
|
|
826
|
+
if (opts.placeholder) field.placeholder = opts.placeholder;
|
|
827
|
+
field.setAttribute("aria-label", opts.title || opts.body || "");
|
|
828
|
+
dialog.appendChild(field);
|
|
829
|
+
}
|
|
830
|
+
const actions = el("div", "dlg-actions");
|
|
831
|
+
const cancel = el("button", "quiet", opts.cancel || t("dialog.cancel"));
|
|
832
|
+
const ok = el("button", opts.danger ? "approve danger" : "approve", opts.ok || t("dialog.ok"));
|
|
833
|
+
cancel.type = ok.type = "button";
|
|
834
|
+
actions.append(cancel, ok);
|
|
835
|
+
dialog.appendChild(actions);
|
|
836
|
+
|
|
837
|
+
let settled = false;
|
|
838
|
+
const finish = (value) => {
|
|
839
|
+
if (settled) return;
|
|
840
|
+
settled = true;
|
|
841
|
+
if (dialog.open) dialog.close();
|
|
842
|
+
dialog.remove();
|
|
843
|
+
resolve(value);
|
|
844
|
+
};
|
|
845
|
+
const accept = () => {
|
|
846
|
+
if (!field) return finish(true);
|
|
847
|
+
const text = field.value.trim();
|
|
848
|
+
if (text) finish(text);
|
|
849
|
+
};
|
|
850
|
+
cancel.onclick = () => finish(field ? null : false);
|
|
851
|
+
ok.onclick = accept;
|
|
852
|
+
if (field) {
|
|
853
|
+
const sync = () => { ok.disabled = !field.value.trim(); };
|
|
854
|
+
field.oninput = sync;
|
|
855
|
+
sync();
|
|
856
|
+
field.onkeydown = (e) => {
|
|
857
|
+
if (e.key !== "Enter" || e.isComposing) return;
|
|
858
|
+
e.preventDefault();
|
|
859
|
+
accept();
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
dialog.addEventListener("cancel", (e) => {
|
|
863
|
+
e.preventDefault();
|
|
864
|
+
finish(field ? null : false);
|
|
865
|
+
});
|
|
866
|
+
document.body.appendChild(dialog);
|
|
867
|
+
dialog.showModal();
|
|
868
|
+
if (field) {
|
|
869
|
+
field.focus();
|
|
870
|
+
field.select();
|
|
871
|
+
} else (opts.danger ? cancel : ok).focus();
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/** Yes or no. Resolves true only for the confirming button. */
|
|
876
|
+
function askConfirm(opts) {
|
|
877
|
+
return askDialog(typeof opts === "string" ? { body: opts } : opts, false);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/** One line of text, trimmed. Resolves null when cancelled. */
|
|
881
|
+
function askText(opts) {
|
|
882
|
+
return askDialog(opts, true);
|
|
883
|
+
}
|
|
884
|
+
|
|
810
885
|
`;
|
|
811
886
|
//# sourceMappingURL=dialogs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dialogs.js","sourceRoot":"","sources":["../../../src/page/client/dialogs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"dialogs.js","sourceRoot":"","sources":["../../../src/page/client/dialogs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA82BhC,CAAC"}
|
|
@@ -74,6 +74,8 @@ events.onmessage = (message) => {
|
|
|
74
74
|
// been written yet, so there is nothing at /attachment to ask for.
|
|
75
75
|
const sent = sentPreviews.get(event.session) || [];
|
|
76
76
|
sentPreviews.delete(event.session);
|
|
77
|
+
P.suggestion = null;
|
|
78
|
+
if (event.session === activeId) paintSuggestion();
|
|
77
79
|
addUser(event.text || "", sent);
|
|
78
80
|
startWorking(t("work.thinking"));
|
|
79
81
|
const unshown = (event.attachments || []).filter(
|
|
@@ -144,7 +146,15 @@ events.onmessage = (message) => {
|
|
|
144
146
|
renderQueued();
|
|
145
147
|
break;
|
|
146
148
|
|
|
147
|
-
case "assistant_message":
|
|
149
|
+
case "assistant_message": {
|
|
150
|
+
P.streaming = null;
|
|
151
|
+
const meta = event.message && event.message.meta;
|
|
152
|
+
if (meta && typeof meta.agent === "string") {
|
|
153
|
+
const session = meta.acpSession || {};
|
|
154
|
+
agentReply(P.turn, { name: meta.agent, session: session.id, cwd: session.cwd });
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
148
158
|
// After the turn ended: under its last reply, not a new turn of its own.
|
|
149
159
|
case "turn_shots": {
|
|
150
160
|
const replies = P.node.querySelectorAll(":scope > .turn.assistant");
|
|
@@ -228,6 +238,11 @@ events.onmessage = (message) => {
|
|
|
228
238
|
case "remote_pair_request": showPairing(event); break;
|
|
229
239
|
case "remote_pair_closed": closePairing(event.id); break;
|
|
230
240
|
case "question_request": questionCard(event); break;
|
|
241
|
+
// A theme proposed by set_theme, and what became of it; then the theme in
|
|
242
|
+
// use, from this window or another.
|
|
243
|
+
case "theme_preview": themePreviewCard(event.preview); break;
|
|
244
|
+
case "theme_preview_closed": closeThemePreview(event.id, event.outcome); break;
|
|
245
|
+
case "theme": setThemeState(event.state); break;
|
|
231
246
|
case "question_result": settleQuestion(event); break;
|
|
232
247
|
// Sent once per (re)connect, after the replay: a card this page still
|
|
233
248
|
// shows as open but the server no longer holds closed while we were away.
|
|
@@ -329,13 +344,21 @@ events.onmessage = (message) => {
|
|
|
329
344
|
void loadSessions();
|
|
330
345
|
break;
|
|
331
346
|
case "state": applyState(event.state); break;
|
|
347
|
+
case "suggestion":
|
|
348
|
+
P.suggestion = event.text;
|
|
349
|
+
if (event.session === activeId) paintSuggestion();
|
|
350
|
+
break;
|
|
351
|
+
case "suggestions":
|
|
352
|
+
if (!event.enabled) for (const entry of panes.values()) entry.suggestion = null;
|
|
353
|
+
paintSuggestion();
|
|
354
|
+
break;
|
|
332
355
|
// What a conversation is doing on the computer, for the live view.
|
|
333
356
|
case "computer": computerEvent(event.session, event.event); break;
|
|
334
357
|
// The project's starters, written in the background after the fixed three.
|
|
335
358
|
case "starters": startersArrived(event); break;
|
|
336
359
|
case "connectors_changed":
|
|
337
360
|
if (authorizing === event.connector) authorizing = null;
|
|
338
|
-
offer = { connectors: event.connectors, plugins: event.plugins || [], skills: event.skills || [] };
|
|
361
|
+
offer = { connectors: event.connectors, plugins: event.plugins || [], skills: event.skills || [], agents: event.agents || offer.agents || [] };
|
|
339
362
|
// The failure already said what went wrong; this only announces the
|
|
340
363
|
// half that is worth celebrating.
|
|
341
364
|
if (event.authorized) addNotice(t("market.authorized", { name: event.connector }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../../src/page/client/events.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../../src/page/client/events.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsY/B,CAAC"}
|
|
@@ -16,6 +16,8 @@ import { COMPUTER } from "./computer.js";
|
|
|
16
16
|
import { FILES } from "./files.js";
|
|
17
17
|
import { TERMINAL } from "./terminal.js";
|
|
18
18
|
import { SHELL } from "./shell.js";
|
|
19
|
+
import { RIPPLE } from "./ripple.js";
|
|
20
|
+
import { THEME } from "./theme.js";
|
|
19
21
|
/**
|
|
20
22
|
* The client script, assembled.
|
|
21
23
|
*
|
|
@@ -50,5 +52,7 @@ export const APP_SCRIPT = [
|
|
|
50
52
|
BROWSER,
|
|
51
53
|
COMPUTER,
|
|
52
54
|
SHELL,
|
|
55
|
+
RIPPLE,
|
|
56
|
+
THEME,
|
|
53
57
|
].join("") + "\n";
|
|
54
58
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/page/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,UAAU,GACrB;IACA,IAAI;IACJ,OAAO;IACP,UAAU;IACV,KAAK;IACL,MAAM;IACN,OAAO;IACP,QAAQ;IACR,KAAK;IACL,SAAS;IACT,OAAO;IACP,QAAQ;IACR,KAAK;IACL,MAAM;IACN,KAAK;IACL,QAAQ;IACR,OAAO;IACP,QAAQ;IACR,KAAK;CACJ,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/page/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,UAAU,GACrB;IACA,IAAI;IACJ,OAAO;IACP,UAAU;IACV,KAAK;IACL,MAAM;IACN,OAAO;IACP,QAAQ;IACR,KAAK;IACL,SAAS;IACT,OAAO;IACP,QAAQ;IACR,KAAK;IACL,MAAM;IACN,KAAK;IACL,QAAQ;IACR,OAAO;IACP,QAAQ;IACR,KAAK;IACL,MAAM;IACN,KAAK;CACJ,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC"}
|
|
@@ -384,7 +384,7 @@ async function showMemoryPage(id) {
|
|
|
384
384
|
form.appendChild(mergeActions);
|
|
385
385
|
form.onsubmit = async (event) => {
|
|
386
386
|
event.preventDefault();
|
|
387
|
-
if (!
|
|
387
|
+
if (!(await askConfirm({ body: t("memory.mergeConfirm"), ok: t("memory.mergeApply") }))) return;
|
|
388
388
|
apply.disabled = true;
|
|
389
389
|
await act({ action: "merge", note: { summary: summary.value.trim(), body: body.value.trim(), tags: tags.value.split(/[,,\s]+/).filter(Boolean), type: proposal.draft.type || "" } });
|
|
390
390
|
apply.disabled = false;
|
|
@@ -563,9 +563,9 @@ async function showMemoryPage(id) {
|
|
|
563
563
|
row.appendChild(line);
|
|
564
564
|
}
|
|
565
565
|
const remove = el("button", "ghost", t("memory.delete")); remove.type = "button";
|
|
566
|
-
remove.onclick = () => {
|
|
566
|
+
remove.onclick = async () => {
|
|
567
567
|
const linked = links.in.length ? t("memory.confirmDeleteLinked", { count: links.in.length }) : "";
|
|
568
|
-
if (
|
|
568
|
+
if (await askConfirm({ body: linked + t("memory.confirmDelete"), ok: t("memory.delete"), danger: true })) void act({ action: "delete", scope: note.scope, id: note.id, expectedHash: note.hash });
|
|
569
569
|
};
|
|
570
570
|
row.appendChild(editor(note, remove));
|
|
571
571
|
list.appendChild(row);
|
|
@@ -588,7 +588,7 @@ async function showMemoryPage(id) {
|
|
|
588
588
|
text.appendChild(el("div", "muted small", [new Date(backup.backedUpAt).toLocaleString(LOCALE), backup.reason ? t("memory.backupReason." + backup.reason) : "", scopeLabel, backup.id].filter(Boolean).join(" · ")));
|
|
589
589
|
row.appendChild(text);
|
|
590
590
|
const restore = el("button", "ghost", t("memory.restore")); restore.type = "button";
|
|
591
|
-
restore.onclick = () => { if (
|
|
591
|
+
restore.onclick = async () => { if (await askConfirm({ body: t("memory.confirmRestore"), ok: t("memory.restore") })) void act({ action: "restore", file: backup.file }); };
|
|
592
592
|
row.appendChild(restore);
|
|
593
593
|
backups.appendChild(row);
|
|
594
594
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The field behind a new session: a grid of dots a click sends a shockwave
|
|
3
|
+
* through.
|
|
4
|
+
*
|
|
5
|
+
* Only while the empty state is on screen. The canvas sits under everything
|
|
6
|
+
* in #main and takes no pointer, so it can never eat a click meant for the
|
|
7
|
+
* composer or a starter; it only hears them go by. Nothing animates at rest —
|
|
8
|
+
* a frame is drawn when a wave is travelling or the pointer moves, and the
|
|
9
|
+
* loop stops once the last wave has died out, so an idle new tab costs nothing.
|
|
10
|
+
*
|
|
11
|
+
* One part of the client script; see ./index.ts for how the parts join.
|
|
12
|
+
*/
|
|
13
|
+
export declare const RIPPLE: string;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The field behind a new session: a grid of dots a click sends a shockwave
|
|
3
|
+
* through.
|
|
4
|
+
*
|
|
5
|
+
* Only while the empty state is on screen. The canvas sits under everything
|
|
6
|
+
* in #main and takes no pointer, so it can never eat a click meant for the
|
|
7
|
+
* composer or a starter; it only hears them go by. Nothing animates at rest —
|
|
8
|
+
* a frame is drawn when a wave is travelling or the pointer moves, and the
|
|
9
|
+
* loop stops once the last wave has died out, so an idle new tab costs nothing.
|
|
10
|
+
*
|
|
11
|
+
* One part of the client script; see ./index.ts for how the parts join.
|
|
12
|
+
*/
|
|
13
|
+
export const RIPPLE = String.raw `
|
|
14
|
+
// --- ripple field ------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
(function rippleField() {
|
|
17
|
+
const host = document.getElementById("main");
|
|
18
|
+
if (!host || !emptyEl) return;
|
|
19
|
+
const still = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
20
|
+
|
|
21
|
+
const canvas = document.createElement("canvas");
|
|
22
|
+
canvas.id = "ripple";
|
|
23
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
24
|
+
host.prepend(canvas);
|
|
25
|
+
const ctx = canvas.getContext("2d");
|
|
26
|
+
|
|
27
|
+
const GAP = 24; // grid pitch, css px
|
|
28
|
+
const SPEED = 0.38; // wavefront, px per ms
|
|
29
|
+
const LIFE = 3200; // ms a wave travels before it is gone
|
|
30
|
+
const BAND = 90; // width of the wavefront's swell
|
|
31
|
+
const LENS = 120; // radius the pointer magnifies
|
|
32
|
+
const TINT = "fg"; // what a wave lights up in: "fg" plain, "brand" lime
|
|
33
|
+
|
|
34
|
+
let w = 0, h = 0, dpr = 1;
|
|
35
|
+
let waves = [];
|
|
36
|
+
let pointer = null; // {x, y} inside the canvas, or null
|
|
37
|
+
let raf = 0;
|
|
38
|
+
let shown = false;
|
|
39
|
+
let ink = { fg: "#111", brand: "#5e8c00" };
|
|
40
|
+
|
|
41
|
+
function readInk() {
|
|
42
|
+
const css = getComputedStyle(document.documentElement);
|
|
43
|
+
ink = {
|
|
44
|
+
fg: css.getPropertyValue("--fg").trim() || ink.fg,
|
|
45
|
+
brand: css.getPropertyValue("--brand").trim() || ink.brand,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function fit() {
|
|
50
|
+
const box = host.getBoundingClientRect();
|
|
51
|
+
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
52
|
+
w = Math.max(1, Math.round(box.width));
|
|
53
|
+
h = Math.max(1, Math.round(box.height));
|
|
54
|
+
canvas.width = w * dpr;
|
|
55
|
+
canvas.height = h * dpr;
|
|
56
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Is the invitation actually in front of someone? */
|
|
60
|
+
const visible = () => emptyEl.isConnected && emptyEl.offsetParent !== null;
|
|
61
|
+
|
|
62
|
+
function kick() {
|
|
63
|
+
if (!raf) raf = requestAnimationFrame(frame);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function frame(now) {
|
|
67
|
+
raf = 0;
|
|
68
|
+
waves = waves.filter((wave) => now - wave.t < LIFE);
|
|
69
|
+
draw(now);
|
|
70
|
+
if (waves.length) kick();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function draw(at) {
|
|
74
|
+
ctx.clearRect(0, 0, w, h);
|
|
75
|
+
if (!shown) return;
|
|
76
|
+
// rAF's stamp is when the frame began, which can trail a wave pushed by a
|
|
77
|
+
// timer since; time is read here so no wave is ever younger than zero.
|
|
78
|
+
const now = Math.max(at, performance.now());
|
|
79
|
+
|
|
80
|
+
// The dot field: each dot pushed outward by every wave passing it, and lit
|
|
81
|
+
// by how hard it is being pushed.
|
|
82
|
+
const ox = (w % GAP) / 2 + GAP / 2;
|
|
83
|
+
const oy = (h % GAP) / 2 + GAP / 2;
|
|
84
|
+
ctx.fillStyle = ink.fg;
|
|
85
|
+
const lit = [];
|
|
86
|
+
for (let gy = oy; gy < h; gy += GAP) {
|
|
87
|
+
for (let gx = ox; gx < w; gx += GAP) {
|
|
88
|
+
let dx = 0, dy = 0, energy = 0;
|
|
89
|
+
for (const wave of waves) {
|
|
90
|
+
const age = now - wave.t;
|
|
91
|
+
const front = age * SPEED;
|
|
92
|
+
const vx = gx - wave.x, vy = gy - wave.y;
|
|
93
|
+
const dist = Math.hypot(vx, vy) || 1;
|
|
94
|
+
const off = dist - front;
|
|
95
|
+
if (off > BAND || off < -BAND * 3) continue;
|
|
96
|
+
const fade = 1 - age / LIFE;
|
|
97
|
+
// One long, low swell at the front — a breath through the grid, not a shake.
|
|
98
|
+
const env = Math.exp(-(off * off) / (BAND * BAND * 0.35)) * fade * fade;
|
|
99
|
+
const push = Math.sin(off / 26) * 4.5 * env * wave.power;
|
|
100
|
+
dx += (vx / dist) * push;
|
|
101
|
+
dy += (vy / dist) * push;
|
|
102
|
+
energy += env * wave.power;
|
|
103
|
+
}
|
|
104
|
+
let r = 1.05, alpha = 0.13;
|
|
105
|
+
if (pointer) {
|
|
106
|
+
const near = Math.hypot(gx - pointer.x, gy - pointer.y);
|
|
107
|
+
if (near < LENS) {
|
|
108
|
+
const k = 1 - near / LENS;
|
|
109
|
+
r += k * k * 1.3;
|
|
110
|
+
alpha += k * k * 0.22;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (energy > 0.06) {
|
|
114
|
+
lit.push(gx + dx, gy + dy, Math.min(1, energy), r);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
ctx.globalAlpha = alpha;
|
|
118
|
+
ctx.beginPath();
|
|
119
|
+
ctx.arc(gx + dx, gy + dy, r, 0, 6.2832);
|
|
120
|
+
ctx.fill();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Dots on a wavefront take the brand's lime and swell with it.
|
|
124
|
+
ctx.fillStyle = ink[TINT];
|
|
125
|
+
for (let i = 0; i < lit.length; i += 4) {
|
|
126
|
+
const e = lit[i + 2];
|
|
127
|
+
ctx.globalAlpha = 0.18 + e * 0.5;
|
|
128
|
+
ctx.beginPath();
|
|
129
|
+
ctx.arc(lit[i], lit[i + 1], lit[i + 3] + e * 0.9, 0, 6.2832);
|
|
130
|
+
ctx.fill();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The fronts themselves: a hairline lime ring with a faint dotted ghost of
|
|
134
|
+
// the ink riding just outside it, and a soft bloom where the click landed.
|
|
135
|
+
ctx.lineCap = "round";
|
|
136
|
+
for (const wave of waves) {
|
|
137
|
+
const age = now - wave.t;
|
|
138
|
+
const fade = 1 - age / LIFE;
|
|
139
|
+
const front = age * SPEED;
|
|
140
|
+
ctx.globalAlpha = 0.32 * fade * fade * fade * wave.power;
|
|
141
|
+
ctx.strokeStyle = ink[TINT];
|
|
142
|
+
ctx.lineWidth = 1;
|
|
143
|
+
ctx.beginPath();
|
|
144
|
+
ctx.arc(wave.x, wave.y, front, 0, 6.2832);
|
|
145
|
+
ctx.stroke();
|
|
146
|
+
ctx.globalAlpha = 0.08 * fade * fade * fade * wave.power;
|
|
147
|
+
ctx.strokeStyle = ink.fg;
|
|
148
|
+
ctx.lineWidth = 1;
|
|
149
|
+
ctx.setLineDash([2, 9]);
|
|
150
|
+
ctx.lineDashOffset = -age / 40;
|
|
151
|
+
ctx.beginPath();
|
|
152
|
+
ctx.arc(wave.x, wave.y, front * 1.03 + 4, 0, 6.2832);
|
|
153
|
+
ctx.stroke();
|
|
154
|
+
ctx.setLineDash([]);
|
|
155
|
+
if (age < 420) {
|
|
156
|
+
const k = 1 - age / 420;
|
|
157
|
+
const glow = ctx.createRadialGradient(wave.x, wave.y, 0, wave.x, wave.y, 46);
|
|
158
|
+
glow.addColorStop(0, ink[TINT]);
|
|
159
|
+
glow.addColorStop(1, "transparent");
|
|
160
|
+
ctx.globalAlpha = 0.22 * k * k * wave.power;
|
|
161
|
+
ctx.fillStyle = glow;
|
|
162
|
+
ctx.beginPath();
|
|
163
|
+
ctx.arc(wave.x, wave.y, 46, 0, 6.2832);
|
|
164
|
+
ctx.fill();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
ctx.globalAlpha = 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function wave(x, y, power) {
|
|
171
|
+
if (still.matches) return;
|
|
172
|
+
waves.push({ x, y, t: performance.now(), power });
|
|
173
|
+
if (waves.length > 28) waves.shift();
|
|
174
|
+
kick();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function local(event) {
|
|
178
|
+
const box = canvas.getBoundingClientRect();
|
|
179
|
+
return { x: event.clientX - box.left, y: event.clientY - box.top };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Held down and dragged, the pointer leaves a wake: a small wave every
|
|
183
|
+
// TRAIL px it travels, so a stroke draws a line of rings behind it.
|
|
184
|
+
const TRAIL = 40;
|
|
185
|
+
let drag = null; // where the last wake wave went out, while held
|
|
186
|
+
|
|
187
|
+
host.addEventListener("pointerdown", (event) => {
|
|
188
|
+
if (!shown || event.button > 0) return;
|
|
189
|
+
const at = local(event);
|
|
190
|
+
drag = at;
|
|
191
|
+
// A stroke begun on bare background is drawing, not selecting: without
|
|
192
|
+
// this it would sweep the headline and starters into a text selection.
|
|
193
|
+
const bare = event.target === host || event.target.id === "log" || event.target === emptyEl;
|
|
194
|
+
if (bare) event.preventDefault();
|
|
195
|
+
wave(at.x, at.y, 1);
|
|
196
|
+
});
|
|
197
|
+
host.addEventListener("pointermove", (event) => {
|
|
198
|
+
if (!shown) return;
|
|
199
|
+
const at = local(event);
|
|
200
|
+
if (event.pointerType === "mouse") pointer = at;
|
|
201
|
+
if (drag && event.buttons & 1) {
|
|
202
|
+
const gone = Math.hypot(at.x - drag.x, at.y - drag.y);
|
|
203
|
+
if (gone >= TRAIL) {
|
|
204
|
+
drag = at;
|
|
205
|
+
wave(at.x, at.y, Math.min(0.45, 0.2 + gone / 240));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
kick();
|
|
209
|
+
});
|
|
210
|
+
const release = () => { drag = null; };
|
|
211
|
+
window.addEventListener("pointerup", release);
|
|
212
|
+
window.addEventListener("pointercancel", release);
|
|
213
|
+
host.addEventListener("pointerleave", () => {
|
|
214
|
+
pointer = null;
|
|
215
|
+
if (shown) kick();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// The empty state comes and goes with tab switches and first messages, and
|
|
219
|
+
// nothing announces it, so look twice a second; on arrival, the brand mark
|
|
220
|
+
// sends out one wave of its own.
|
|
221
|
+
function check() {
|
|
222
|
+
const now = visible();
|
|
223
|
+
if (now === shown) return;
|
|
224
|
+
shown = now;
|
|
225
|
+
canvas.classList.toggle("on", shown);
|
|
226
|
+
if (!shown) { waves = []; pointer = null; kick(); return; }
|
|
227
|
+
readInk();
|
|
228
|
+
fit();
|
|
229
|
+
const mark = emptyEl.querySelector(".brand-mark");
|
|
230
|
+
if (mark) {
|
|
231
|
+
const at = mark.getBoundingClientRect();
|
|
232
|
+
const box = canvas.getBoundingClientRect();
|
|
233
|
+
wave(at.left + at.width / 2 - box.left, at.top + at.height / 2 - box.top, 0.7);
|
|
234
|
+
}
|
|
235
|
+
kick();
|
|
236
|
+
}
|
|
237
|
+
setInterval(check, 500);
|
|
238
|
+
check();
|
|
239
|
+
|
|
240
|
+
new ResizeObserver(() => { if (shown) { fit(); kick(); } }).observe(host);
|
|
241
|
+
// Theme flips (system or the in-app toggle) change the ink.
|
|
242
|
+
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { readInk(); kick(); });
|
|
243
|
+
new MutationObserver(() => { readInk(); kick(); })
|
|
244
|
+
.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
|
|
245
|
+
})();
|
|
246
|
+
`;
|
|
247
|
+
//# sourceMappingURL=ripple.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ripple.js","sourceRoot":"","sources":["../../../src/page/client/ripple.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyO/B,CAAC"}
|