@heycys/gallery 0.2.3 → 0.2.4
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/README.md +1 -1
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/assets/preview/runtime.js +156 -0
- package/assets/preview/shell.html +107 -0
- package/assets/writing-preferences.md +1 -1
- package/browser/editor/components/divider/index.mjs +1 -1
- package/browser/editor/page/annotate/card.mjs +6 -0
- package/browser/editor/page/annotate/index.mjs +22 -2
- package/browser/editor/page/columns/index.mjs +1 -1
- package/browser/editor/page/conflicts.mjs +1 -1
- package/browser/editor/page/drag/index.mjs +1 -1
- package/browser/editor/page/editor.mjs +39 -3
- package/browser/editor/page/focus.mjs +104 -0
- package/browser/editor/page/handle/index.mjs +14 -2
- package/browser/editor/page/links/index.mjs +2 -2
- package/browser/editor/page/quiet.mjs +3 -3
- package/browser/editor/page/resize/index.mjs +1 -1
- package/browser/editor/page/session.mjs +4 -1
- package/browser/editor/page/ui.mjs +1 -1
- package/browser/shared/annotations.mjs +22 -3
- package/browser/shared/block-packs.mjs +13 -4
- package/browser/shared/document-look.mjs +14 -0
- package/browser/shared/drafts.mjs +1 -1
- package/browser/shared/extension-manifest.mjs +40 -12
- package/browser/shared/favorite-merge.mjs +17 -0
- package/browser/shared/favorites.mjs +77 -0
- package/browser/shared/i18n/en.mjs +123 -24
- package/browser/shared/outline-ops.mjs +109 -0
- package/browser/shared/outline.mjs +6 -0
- package/browser/shared/pages.mjs +20 -8
- package/browser/shared/paths.mjs +5 -2
- package/browser/shared/preference-documents.mjs +137 -0
- package/browser/shared/presence.mjs +94 -0
- package/browser/shared/preview-server.mjs +8 -5
- package/browser/shared/qa-levels.mjs +9 -1
- package/browser/shared/session.mjs +4 -0
- package/browser/shared/shipped-blocks.mjs +22 -4
- package/browser/shared/startup.mjs +101 -0
- package/browser/shared/workspace-columns.mjs +1 -1
- package/browser/shared/workspace-look.mjs +21 -8
- package/browser/shared/workspace-registry.mjs +6 -3
- package/browser/shared/workspace.mjs +37 -10
- package/browser/shared/writing-preferences.mjs +2 -42
- package/dist/chunk-25LYSMAE.mjs +2 -0
- package/dist/chunk-2I6ZVC5O.mjs +2 -0
- package/dist/{chunk-M4NRDYI5.mjs → chunk-2MYTOOP6.mjs} +1 -1
- package/dist/chunk-2VNENLER.mjs +2 -0
- package/dist/chunk-3FERXZF5.mjs +4 -0
- package/dist/chunk-3M6SBBGU.mjs +2 -0
- package/dist/chunk-3VCAAF34.mjs +2 -0
- package/dist/{chunk-WD4U6XXT.mjs → chunk-3ZTRLGSA.mjs} +1 -1
- package/dist/chunk-4FRA5AUE.mjs +2 -0
- package/dist/chunk-4LR2QEC2.mjs +4 -0
- package/dist/chunk-56SVJENP.mjs +966 -0
- package/dist/chunk-5CQISL2G.mjs +4 -0
- package/dist/{chunk-RRKJMHNX.mjs → chunk-5I5ZDDWC.mjs} +5 -5
- package/dist/chunk-5IHDLEB4.mjs +10 -0
- package/dist/chunk-5LRDMV7O.mjs +2 -0
- package/dist/chunk-5MVUYRKG.mjs +5 -0
- package/dist/chunk-5TLLIZT7.mjs +2 -0
- package/dist/chunk-6ILJWFYW.mjs +4 -0
- package/dist/chunk-6N25CJ6Y.mjs +3 -0
- package/dist/{chunk-SQ6Y2ZMA.mjs → chunk-7DTINW6U.mjs} +2 -2
- package/dist/chunk-7KWAGKIK.mjs +2 -0
- package/dist/chunk-7WNUOAFK.mjs +2 -0
- package/dist/chunk-7XCRW5RC.mjs +2 -0
- package/dist/{chunk-BYNPEUDS.mjs → chunk-7XSDTILF.mjs} +1 -1
- package/dist/chunk-ABKYFOVR.mjs +2 -0
- package/dist/chunk-AMUF7PUN.mjs +2 -0
- package/dist/{chunk-FJLM7YE7.mjs → chunk-B2EIMERT.mjs} +2 -2
- package/dist/{chunk-QNECWMGR.mjs → chunk-B45IAAU6.mjs} +1 -1
- package/dist/chunk-B6RAGF4R.mjs +2 -0
- package/dist/chunk-BSJKFE7I.mjs +4 -0
- package/dist/chunk-C72EEVLK.mjs +2 -0
- package/dist/chunk-CRAUKZ23.mjs +4 -0
- package/dist/chunk-F4CT35KL.mjs +2 -0
- package/dist/chunk-F63BNJLG.mjs +2 -0
- package/dist/chunk-FBKUPS44.mjs +2 -0
- package/dist/chunk-FL52ZL5Z.mjs +15 -0
- package/dist/chunk-FOZCU4VW.mjs +2 -0
- package/dist/chunk-G5HV7BC3.mjs +2 -0
- package/dist/{chunk-ERLAIBOA.mjs → chunk-GB5SOYQJ.mjs} +2 -2
- package/dist/chunk-GN3SG3IF.mjs +2 -0
- package/dist/{chunk-6QWQXHAV.mjs → chunk-HFTBVEZK.mjs} +1 -1
- package/dist/chunk-IOW5HLRK.mjs +2 -0
- package/dist/chunk-IQ3GJNTI.mjs +6 -0
- package/dist/chunk-K6SPFBUJ.mjs +2 -0
- package/dist/chunk-K7Y7SPTS.mjs +2 -0
- package/dist/chunk-L2IJCEUH.mjs +2 -0
- package/dist/{chunk-GPIWQ4X4.mjs → chunk-L3VT6LM4.mjs} +1 -1
- package/dist/chunk-L4WLQYFC.mjs +5 -0
- package/dist/{chunk-NLDF35P5.mjs → chunk-LAMCAGK6.mjs} +1 -1
- package/dist/chunk-LHGKPMTI.mjs +2 -0
- package/dist/chunk-LJZIYVNA.mjs +3 -0
- package/dist/chunk-LSVCTLS2.mjs +2 -0
- package/dist/chunk-LZY74XTP.mjs +2 -0
- package/dist/chunk-MC4LRSVH.mjs +2 -0
- package/dist/chunk-MCEXN4G5.mjs +2 -0
- package/dist/chunk-MTLAOCFF.mjs +181 -0
- package/dist/chunk-MZ66LN3B.mjs +2 -0
- package/dist/chunk-N5QWJRRI.mjs +2 -0
- package/dist/chunk-NF4MMOUB.mjs +2 -0
- package/dist/chunk-NHJYAJ73.mjs +3 -0
- package/dist/chunk-NRSD7SVY.mjs +2 -0
- package/dist/chunk-NT23I4XW.mjs +2 -0
- package/dist/chunk-NUALABIS.mjs +3 -0
- package/dist/chunk-NW6LAU22.mjs +2 -0
- package/dist/chunk-NWWHDGKF.mjs +5 -0
- package/dist/chunk-OFRCVMSJ.mjs +2 -0
- package/dist/chunk-OKVQQ3CX.mjs +4 -0
- package/dist/chunk-OM24O4KR.mjs +8 -0
- package/dist/{chunk-4B2TB5K2.mjs → chunk-PBMOC7KA.mjs} +2 -2
- package/dist/chunk-PVTCP43W.mjs +2 -0
- package/dist/chunk-Q62HZAJS.mjs +2 -0
- package/dist/chunk-QBJWIR3Q.mjs +2 -0
- package/dist/chunk-QDSEZX7Z.mjs +6 -0
- package/dist/chunk-QR4V4A2Z.mjs +15 -0
- package/dist/{chunk-QVGOPHOF.mjs → chunk-QRLVVZ3R.mjs} +1 -1
- package/dist/chunk-QW3QG67J.mjs +2 -0
- package/dist/chunk-R2T2EVWY.mjs +8 -0
- package/dist/{chunk-JJNHKGTD.mjs → chunk-R5RERE6G.mjs} +1 -1
- package/dist/chunk-RFHEWT3Y.mjs +5 -0
- package/dist/chunk-RS2Z2JQP.mjs +2 -0
- package/dist/chunk-S6YEC4GE.mjs +16 -0
- package/dist/chunk-SHZLMGGW.mjs +2 -0
- package/dist/chunk-TALJQM2O.mjs +2 -0
- package/dist/chunk-TLNJWEFY.mjs +22 -0
- package/dist/{chunk-ZLS2FRS6.mjs → chunk-TQZU7D7C.mjs} +1 -1
- package/dist/chunk-UTKNWYBA.mjs +2 -0
- package/dist/chunk-V3TM3TEN.mjs +2 -0
- package/dist/chunk-V3XROSP5.mjs +7 -0
- package/dist/{chunk-6QBHU4DF.mjs → chunk-V5JTW2LH.mjs} +1 -1
- package/dist/chunk-VAXLSISW.mjs +4 -0
- package/dist/chunk-VBHI3RG6.mjs +2 -0
- package/dist/{chunk-CRDG6EF2.mjs → chunk-VKZJRGDR.mjs} +1 -1
- package/dist/chunk-VZXZM7NS.mjs +2 -0
- package/dist/chunk-WDS6HEPE.mjs +2 -0
- package/dist/chunk-WHGANNEY.mjs +13 -0
- package/dist/chunk-WMJWC4QY.mjs +56 -0
- package/dist/{chunk-UTMAY3WK.mjs → chunk-WWONJJQS.mjs} +1 -1
- package/dist/chunk-X4WNLFIK.mjs +2 -0
- package/dist/chunk-XNNCQROX.mjs +11 -0
- package/dist/chunk-XRYECF23.mjs +170 -0
- package/dist/{chunk-225ALKYI.mjs → chunk-Z4NEXXFK.mjs} +1 -1
- package/dist/{chunk-PKVK6WFC.mjs → chunk-ZFVF2YPV.mjs} +2 -2
- package/dist/chunk-ZM2IX3R2.mjs +2 -0
- package/dist/chunk-ZNGGDUZI.mjs +2 -0
- package/dist/chunk-ZOEJQOKT.mjs +2 -0
- package/dist/cli.mjs +1 -1
- package/dist/server.mjs +1 -1
- package/dist/update-fetch.mjs +1 -1
- package/dist/worker.mjs +1 -1
- package/package.json +1 -1
- package/release.json +3 -3
- package/skill/SKILL.md +54 -148
- package/skill/references/building-blocks.md +4 -4
- package/skill/references/charts-and-figures.md +73 -0
- package/skill/references/extensions.md +47 -4
- package/skill/references/pages-and-data.md +22 -3
- package/skill/references/preference-files.md +7 -0
- package/skill/references/previews.md +17 -0
- package/skill/references/writing-html.md +21 -82
- package/web/assets/index-B4uUluLM.js +15 -0
- package/web/assets/index-C3vHT8sH.css +1 -0
- package/web/index.html +2 -2
- package/dist/chunk-2HS7ZBPU.mjs +0 -2
- package/dist/chunk-3F57LEN5.mjs +0 -2
- package/dist/chunk-4OZWXZE5.mjs +0 -5
- package/dist/chunk-5GK7HS62.mjs +0 -2
- package/dist/chunk-6GV3CBB2.mjs +0 -2
- package/dist/chunk-6NLXUKAY.mjs +0 -4
- package/dist/chunk-6XD6U3QA.mjs +0 -5
- package/dist/chunk-6Y3HYOBM.mjs +0 -2
- package/dist/chunk-7EFHNQ5B.mjs +0 -3
- package/dist/chunk-7M2BQQGZ.mjs +0 -2
- package/dist/chunk-7UZUQQ7Q.mjs +0 -554
- package/dist/chunk-ALZ6ZENZ.mjs +0 -3
- package/dist/chunk-BX5AKINW.mjs +0 -15
- package/dist/chunk-DR73Y2UJ.mjs +0 -3
- package/dist/chunk-EURLTOTZ.mjs +0 -4
- package/dist/chunk-F4ZA6JRZ.mjs +0 -3
- package/dist/chunk-FNCCVGKX.mjs +0 -2
- package/dist/chunk-FOJ3K5Z3.mjs +0 -6
- package/dist/chunk-FQ72LZHB.mjs +0 -2
- package/dist/chunk-G7N7YFVD.mjs +0 -2
- package/dist/chunk-GIRXJBGC.mjs +0 -2
- package/dist/chunk-GKOBB6DH.mjs +0 -2
- package/dist/chunk-GMJD7ID3.mjs +0 -2
- package/dist/chunk-GQJMVSSN.mjs +0 -2
- package/dist/chunk-HX4DLE3J.mjs +0 -10
- package/dist/chunk-IS7N3N6Q.mjs +0 -168
- package/dist/chunk-JEUK3ZO7.mjs +0 -2
- package/dist/chunk-JPG2LBQM.mjs +0 -11
- package/dist/chunk-JYK5WJPM.mjs +0 -2
- package/dist/chunk-KLQP46Y3.mjs +0 -4
- package/dist/chunk-L55NOXBE.mjs +0 -4
- package/dist/chunk-LCB6MARS.mjs +0 -5
- package/dist/chunk-LJLVCU6W.mjs +0 -16
- package/dist/chunk-LMUZQK5O.mjs +0 -2
- package/dist/chunk-NMF3IPYA.mjs +0 -13
- package/dist/chunk-ONKC4BCC.mjs +0 -2
- package/dist/chunk-OU6ZZVBX.mjs +0 -5
- package/dist/chunk-PUOQ65NC.mjs +0 -2
- package/dist/chunk-QJAVCTY7.mjs +0 -2
- package/dist/chunk-QP7BOHE4.mjs +0 -2
- package/dist/chunk-S3SX3COT.mjs +0 -20
- package/dist/chunk-SQ3FQCCN.mjs +0 -8
- package/dist/chunk-T4Y2ZSVX.mjs +0 -4
- package/dist/chunk-TANZRINB.mjs +0 -2
- package/dist/chunk-UGWMMEJ5.mjs +0 -15
- package/dist/chunk-UTF7GBQU.mjs +0 -4
- package/dist/chunk-V4ZHRALW.mjs +0 -170
- package/dist/chunk-XBNFXWAW.mjs +0 -6
- package/dist/chunk-XZV72SQ4.mjs +0 -2
- package/dist/chunk-YFJB534W.mjs +0 -2
- package/dist/chunk-Z6OKLRHC.mjs +0 -2
- package/dist/chunk-ZMHUDYLV.mjs +0 -2
- package/web/assets/index-D8P8uhDW.js +0 -15
- package/web/assets/index-FLb9jCfl.css +0 -1
- /package/dist/{chunk-PAREWQGB.mjs → chunk-NOGQX56R.mjs} +0 -0
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ Explorer shows, OneDrive's when OneDrive backs it up) unless you name another (`
|
|
|
41
41
|
`GALLERY_WORKSPACE` variable, or `gallery init <dir>`, which marks a folder as one). Each document is a folder
|
|
42
42
|
(`<slug>/doc.html`, `doc.json`), the deliverables are in `exports/`, and Gallery keeps its own state in `.gallery/`.
|
|
43
43
|
|
|
44
|
-
The server listens on 127.0.0.1 only
|
|
44
|
+
The server listens on 127.0.0.1 only, reusing its last port or trying 8080–8099, one per Gallery data directory; `gallery stop` stops it. After
|
|
45
45
|
upgrading, run `gallery open` once (the server of the version before stops by itself and the new one starts) and
|
|
46
46
|
`gallery skill install` again (the skill of the new version). From the older Gallery skill (the whole tools in the
|
|
47
47
|
agent's skills folder), `gallery skill install` copies its documents into the workspace and moves it out of the
|
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Third-party notices
|
|
2
2
|
|
|
3
|
-
@heycys/gallery 0.2.
|
|
3
|
+
@heycys/gallery 0.2.4 carries the following code and data of others, bundled into dist/ and web/, built into kit/, or as files in vendor/ and browser/.
|
|
4
4
|
|
|
5
5
|
## @asamuzakjp/css-color 5.1.11
|
|
6
6
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Preview runtime: injected at the top of every preview page by cli/preview.mjs.
|
|
2
|
+
// Gives the page the toolbar state (theme, width, its own options) and Gallery's tooltip, toast, confirm and menu,
|
|
3
|
+
// drawn with Gallery's own classes, so a page only writes what is new.
|
|
4
|
+
(() => {
|
|
5
|
+
const listeners = new Set();
|
|
6
|
+
let state = (() => { try { return { ...parent.__pvState }; } catch { return {}; } })();
|
|
7
|
+
|
|
8
|
+
const ICON = {
|
|
9
|
+
close: '<svg viewBox="0 0 20 20"><path d="m5.5 5.5 9 9M14.5 5.5l-9 9"/></svg>',
|
|
10
|
+
plus: '<svg viewBox="0 0 16 16"><path d="M8 3.25v9.5M3.25 8h9.5"/></svg>',
|
|
11
|
+
more: '<svg viewBox="0 0 16 16" class="fill"><circle cx="3.25" cy="8" r="1.25"/><circle cx="8" cy="8" r="1.25"/><circle cx="12.75" cy="8" r="1.25"/></svg>',
|
|
12
|
+
rename: '<svg viewBox="0 0 20 20"><path d="M12.75 4.25 15.75 7.25 7.5 15.5l-3.75.75.75-3.75z"/></svg>',
|
|
13
|
+
duplicate: '<svg viewBox="0 0 20 20"><rect x="6.75" y="6.75" width="9.5" height="9.5" rx="1.75"/><path d="M13.25 6.75v-1.5a1.5 1.5 0 0 0-1.5-1.5h-6.5a1.5 1.5 0 0 0-1.5 1.5v6.5a1.5 1.5 0 0 0 1.5 1.5h1.5"/></svg>',
|
|
14
|
+
trash: '<svg viewBox="0 0 20 20"><path d="M3.75 5.75h12.5M8 5.75V4.5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.25M5.25 5.75l.7 9.6a1.5 1.5 0 0 0 1.5 1.4h5.1a1.5 1.5 0 0 0 1.5-1.4l.7-9.6"/></svg>',
|
|
15
|
+
folder: '<svg viewBox="0 0 24 24" class="lu"><path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/></svg>',
|
|
16
|
+
copy: '<svg viewBox="0 0 24 24" class="lu"><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>',
|
|
17
|
+
file: '<svg viewBox="0 0 24 24" class="lu"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M16 13H8M16 17H8M10 9H8"/></svg>',
|
|
18
|
+
puzzle: '<svg viewBox="0 0 24 24" class="lu"><path d="M15.39 4.39a1 1 0 0 0 1.68-.474 2.5 2.5 0 1 1 3.014 3.015 1 1 0 0 0-.474 1.68l1.683 1.682a2.414 2.414 0 0 1 0 3.414L19.61 15.39a1 1 0 0 1-1.68-.474 2.5 2.5 0 1 0-3.014 3.015 1 1 0 0 1 .474 1.68l-1.683 1.682a2.414 2.414 0 0 1-3.414 0L8.61 19.61a1 1 0 0 0-1.68.474 2.5 2.5 0 1 1-3.014-3.015 1 1 0 0 0 .474-1.68l-1.683-1.682a2.414 2.414 0 0 1 0-3.414L4.39 8.61a1 1 0 0 1 1.68.474 2.5 2.5 0 1 0 3.014-3.015 1 1 0 0 1-.474-1.68l1.683-1.682a2.414 2.414 0 0 1 3.414 0z"/></svg>',
|
|
19
|
+
column: '<svg viewBox="0 0 24 24" class="lu"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>',
|
|
20
|
+
window: '<svg viewBox="0 0 24 24" class="lu"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/></svg>',
|
|
21
|
+
server: '<svg viewBox="0 0 24 24" class="lu"><rect width="20" height="8" x="2" y="2" rx="2"/><rect width="20" height="8" x="2" y="14" rx="2"/><path d="M6 6h.01M6 18h.01"/></svg>',
|
|
22
|
+
bot: '<svg viewBox="0 0 24 24" class="lu"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2M20 14h2M15 13v2M9 13v2"/></svg>',
|
|
23
|
+
};
|
|
24
|
+
const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
|
|
25
|
+
|
|
26
|
+
function apply(next, key) {
|
|
27
|
+
state = { ...state, ...next };
|
|
28
|
+
document.documentElement.dataset.theme = state.theme === 'dark' ? 'dark' : 'light';
|
|
29
|
+
document.documentElement.style.colorScheme = state.theme === 'dark' ? 'dark' : 'light';
|
|
30
|
+
for (const fn of listeners) fn(state, key);
|
|
31
|
+
}
|
|
32
|
+
addEventListener('message', e => { if (e.data?.pv === 'state') apply(e.data.state, e.data.key); });
|
|
33
|
+
if (!state.theme) try { parent.postMessage({ pv: 'hello' }, '*'); } catch {}
|
|
34
|
+
|
|
35
|
+
// Tooltip: Gallery shows one for any [data-tip] (data-tip-delay in ms, default 450).
|
|
36
|
+
let tipEl = null, tipTimer = 0;
|
|
37
|
+
const hideTip = () => { clearTimeout(tipTimer); tipEl?.classList.remove('show'); };
|
|
38
|
+
document.addEventListener('mouseover', e => {
|
|
39
|
+
const t = e.target.closest?.('[data-tip]');
|
|
40
|
+
hideTip();
|
|
41
|
+
if (!t || !t.dataset.tip || document.body.classList.contains('pv-dragging')) return;
|
|
42
|
+
tipTimer = setTimeout(() => {
|
|
43
|
+
tipEl ??= document.body.appendChild(Object.assign(document.createElement('div'), { className: 'tip' }));
|
|
44
|
+
tipEl.textContent = t.dataset.tip;
|
|
45
|
+
const r = t.getBoundingClientRect(), w = tipEl.offsetWidth, h = tipEl.offsetHeight;
|
|
46
|
+
const x = Math.max(8, Math.min(innerWidth - w - 8, r.left + r.width / 2 - w / 2));
|
|
47
|
+
const y = r.bottom + 6 + h > innerHeight - 8 ? r.top - h - 6 : r.bottom + 6;
|
|
48
|
+
tipEl.style.transform = 'translate(' + x + 'px, ' + y + 'px)';
|
|
49
|
+
tipEl.classList.add('show');
|
|
50
|
+
}, Number(t.dataset.tipDelay) || 450);
|
|
51
|
+
});
|
|
52
|
+
document.addEventListener('pointerdown', hideTip, true);
|
|
53
|
+
|
|
54
|
+
let toastTimer = 0;
|
|
55
|
+
function toast(text, { action, onAction, tone } = {}) {
|
|
56
|
+
document.querySelector('.toast')?.remove();
|
|
57
|
+
const t = document.createElement('div');
|
|
58
|
+
t.className = 'toast' + (tone === 'error' ? ' error' : '');
|
|
59
|
+
t.setAttribute('role', 'status');
|
|
60
|
+
t.innerHTML = '<span>' + esc(text) + '</span>' + (action ? '<button type="button">' + esc(action) + '</button>' : '');
|
|
61
|
+
t.querySelector('button')?.addEventListener('click', () => { t.remove(); onAction?.(); });
|
|
62
|
+
document.body.append(t);
|
|
63
|
+
requestAnimationFrame(() => t.classList.add('show'));
|
|
64
|
+
clearTimeout(toastTimer);
|
|
65
|
+
toastTimer = setTimeout(() => { t.classList.remove('show'); setTimeout(() => t.remove(), 200); }, 4000);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let modal = null;
|
|
69
|
+
function confirm({ title, message, actions = [{ id: 'cancel', label: '取消' }, { id: 'ok', label: '确定', primary: true }] }) {
|
|
70
|
+
return new Promise(done => {
|
|
71
|
+
modal?.end('cancel');
|
|
72
|
+
const m = document.createElement('div');
|
|
73
|
+
m.className = 'modal dialog-overlay';
|
|
74
|
+
m.innerHTML = '<div class="dialog" role="alertdialog" aria-modal="true"><div class="dialog-title">' + esc(title) + '</div>' +
|
|
75
|
+
(message ? '<div class="dialog-message">' + esc(message) + '</div>' : '') + '<div class="dialog-actions">' +
|
|
76
|
+
actions.map(a => '<button type="button" class="dialog-btn' + (a.primary ? ' primary' : '') + (a.danger ? ' danger' : '') + '" data-a="' + esc(a.id) + '">' + esc(a.label) + '</button>').join('') + '</div></div>';
|
|
77
|
+
const back = document.activeElement;
|
|
78
|
+
const end = id => { m.remove(); modal = null; back?.focus?.({ preventScroll: true }); done(id); };
|
|
79
|
+
m.addEventListener('pointerdown', e => { if (e.target === m) end('cancel'); });
|
|
80
|
+
m.addEventListener('click', e => { const b = e.target.closest('[data-a]'); if (b) end(b.dataset.a); });
|
|
81
|
+
document.body.append(m);
|
|
82
|
+
modal = { end };
|
|
83
|
+
(m.querySelector('.primary') || m.querySelector('button')).focus();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// items: { icon, label, danger, disabled, tip, run } or '-' for a line. Returns a function that closes it.
|
|
88
|
+
let menuEl = null, menuAnchor = null, menuDone = null;
|
|
89
|
+
function closeMenu() {
|
|
90
|
+
if (!menuEl) return;
|
|
91
|
+
menuEl.remove(); menuEl = null;
|
|
92
|
+
menuAnchor?.classList.remove('pv-menu-open'); menuAnchor?.setAttribute?.('aria-expanded', 'false');
|
|
93
|
+
const d = menuDone; menuAnchor = menuDone = null; d?.();
|
|
94
|
+
}
|
|
95
|
+
function menu(anchor, items, { align = 'start', onClose } = {}) {
|
|
96
|
+
closeMenu(); hideTip();
|
|
97
|
+
menuEl = document.createElement('div');
|
|
98
|
+
menuEl.className = 'menu';
|
|
99
|
+
menuEl.setAttribute('role', 'menu');
|
|
100
|
+
menuEl.style.zIndex = 1600;
|
|
101
|
+
menuEl.innerHTML = items.filter(Boolean).map((it, i) => it === '-' ? '<div class="menu-sep"></div>' :
|
|
102
|
+
'<div class="menu-item' + (it.danger ? ' danger' : '') + (it.disabled ? ' off' : '') + '" role="menuitem" data-i="' + i + '"' +
|
|
103
|
+
(it.disabled ? ' aria-disabled="true"' : '') + (it.tip ? ' data-tip="' + esc(it.tip) + '"' : '') + '>' +
|
|
104
|
+
(it.icon ? '<span class="mi-icon">' + (ICON[it.icon] || it.icon) + '</span>' : '') + '<span class="mi-label">' + esc(it.label) + '</span></div>').join('');
|
|
105
|
+
document.body.append(menuEl);
|
|
106
|
+
const r = anchor.getBoundingClientRect(), w = menuEl.offsetWidth;
|
|
107
|
+
menuEl.style.left = Math.max(8, Math.min(align === 'end' ? r.right - w : r.left, innerWidth - w - 8)) + 'px';
|
|
108
|
+
menuEl.style.top = (r.bottom + 4 + menuEl.offsetHeight > innerHeight - 8 ? Math.max(8, r.top - 4 - menuEl.offsetHeight) : r.bottom + 4) + 'px';
|
|
109
|
+
menuAnchor = anchor; menuDone = onClose;
|
|
110
|
+
anchor.classList.add('pv-menu-open'); anchor.setAttribute?.('aria-expanded', 'true');
|
|
111
|
+
const list = items.filter(Boolean);
|
|
112
|
+
menuEl.addEventListener('click', e => {
|
|
113
|
+
const el = e.target.closest('.menu-item');
|
|
114
|
+
if (!el || el.classList.contains('off')) return;
|
|
115
|
+
const it = list[el.dataset.i];
|
|
116
|
+
closeMenu();
|
|
117
|
+
it.run?.();
|
|
118
|
+
});
|
|
119
|
+
return closeMenu;
|
|
120
|
+
}
|
|
121
|
+
document.addEventListener('pointerdown', e => {
|
|
122
|
+
if (menuEl && !menuEl.contains(e.target) && !menuAnchor?.contains(e.target)) closeMenu();
|
|
123
|
+
}, true);
|
|
124
|
+
// Escape closes what is on top first: the confirm, then the menu; only then does the page see it.
|
|
125
|
+
document.addEventListener('keydown', e => {
|
|
126
|
+
if (e.key !== 'Escape') return;
|
|
127
|
+
if (modal) { e.stopImmediatePropagation(); e.preventDefault(); modal.end('cancel'); }
|
|
128
|
+
else if (menuEl) { e.stopImmediatePropagation(); e.preventDefault(); const a = menuAnchor; closeMenu(); a?.focus?.({ preventScroll: true }); }
|
|
129
|
+
}, true);
|
|
130
|
+
|
|
131
|
+
// A Gallery window behind the page's dialog: <body data-backdrop="gallery">.
|
|
132
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
133
|
+
if (document.body.dataset.backdrop !== 'gallery') return;
|
|
134
|
+
const b = document.createElement('div');
|
|
135
|
+
b.className = 'pv-backdrop';
|
|
136
|
+
b.setAttribute('aria-hidden', 'true');
|
|
137
|
+
b.innerHTML = '<div class="pv-backdrop-side"><i style="width:40%"></i><i style="width:70%"></i><i style="width:55%"></i><i style="width:80%"></i><i style="width:62%"></i></div>' +
|
|
138
|
+
'<div class="pv-backdrop-main"><i style="width:36%;height:22px"></i><i style="width:90%"></i><i style="width:84%"></i><i style="width:72%"></i></div>';
|
|
139
|
+
document.body.prepend(b);
|
|
140
|
+
});
|
|
141
|
+
const style = document.createElement('style');
|
|
142
|
+
style.textContent = 'body{margin:0;font-family:var(--font);font-size:14px;background:var(--bacPri);color:var(--texPri)}' +
|
|
143
|
+
'.pv-backdrop{position:fixed;inset:0;display:flex}.pv-backdrop-side{width:270px;flex:none;box-sizing:border-box;background:var(--sidBac);padding:56px 16px;display:flex;flex-direction:column;gap:16px}' +
|
|
144
|
+
'.pv-backdrop-main{flex:1;min-width:0;padding:72px 64px;display:flex;flex-direction:column;gap:18px}.pv-backdrop i{display:block;height:10px;border-radius:5px;background:var(--bacTerTra)}' +
|
|
145
|
+
'@media (max-width:700px){.pv-backdrop-side{display:none}}';
|
|
146
|
+
document.head.prepend(style);
|
|
147
|
+
|
|
148
|
+
window.preview = {
|
|
149
|
+
get config() { try { return JSON.parse(document.getElementById('preview-config')?.textContent || '{}'); } catch { return {}; } },
|
|
150
|
+
get state() { return state; },
|
|
151
|
+
on(fn) { listeners.add(fn); fn(state, null); return () => listeners.delete(fn); },
|
|
152
|
+
set(next) { try { parent.postMessage({ pv: 'set', state: next }, '*'); } catch {} },
|
|
153
|
+
toast, confirm, menu, closeMenu, hideTip, icon: ICON, esc,
|
|
154
|
+
};
|
|
155
|
+
apply({});
|
|
156
|
+
})();
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>预览 · {{TITLE}}</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { --pv-bg: #f1f0ee; --pv-bar: #fbfbfa; --pv-line: #e3e2e0; --pv-text: #37352f; --pv-sub: #787774; --pv-hover: #37352f0f; --pv-on: #37352f17; --pv-frame: #ffffff; }
|
|
9
|
+
:root[data-theme="dark"] { --pv-bg: #151515; --pv-bar: #202020; --pv-line: #2f2f2f; --pv-text: #e6e6e5; --pv-sub: #9b9b9b; --pv-hover: #ffffff0e; --pv-on: #ffffff1a; --pv-frame: #191919; }
|
|
10
|
+
html, body { height: 100%; margin: 0; }
|
|
11
|
+
body { display: flex; flex-direction: column; background: var(--pv-bg); color: var(--pv-text); font: 13px/1.4 -apple-system, BlinkMacSystemFont, "PingFang SC", "Segoe UI", sans-serif; }
|
|
12
|
+
.pvs-bar { flex: none; display: flex; align-items: center; gap: 12px; height: 44px; padding: 0 12px; box-sizing: border-box; background: var(--pv-bar); border-bottom: 1px solid var(--pv-line); }
|
|
13
|
+
.pvs-title, .pvs-hint { flex: 1 1 0; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
14
|
+
.pvs-title { font-weight: 600; }
|
|
15
|
+
.pvs-hint { color: var(--pv-sub); font-size: 12px; text-align: right; }
|
|
16
|
+
.pvs-tools { flex: none; display: flex; align-items: center; max-width: 100%; overflow-x: auto; scrollbar-width: none; }
|
|
17
|
+
.pvs-group { display: flex; align-items: center; gap: 2px; white-space: nowrap; }
|
|
18
|
+
.pvs-group + .pvs-group { margin-left: 6px; padding-left: 8px; border-left: 1px solid var(--pv-line); }
|
|
19
|
+
.pvs-label { padding: 0 4px; color: var(--pv-sub); font-size: 12px; }
|
|
20
|
+
.pvs-tools button { height: 26px; padding: 0 9px; border: 0; border-radius: 6px; background: none; color: var(--pv-sub); font: inherit; cursor: pointer; }
|
|
21
|
+
.pvs-tools button:hover { background: var(--pv-hover); }
|
|
22
|
+
.pvs-tools button[aria-pressed="true"] { background: var(--pv-on); color: var(--pv-text); font-weight: 500; }
|
|
23
|
+
.pvs-tools button:focus-visible { outline: 2px solid #2383e2; outline-offset: -2px; }
|
|
24
|
+
.pvs-stage { flex: 1; min-height: 0; display: flex; justify-content: center; }
|
|
25
|
+
.pvs-stage iframe { display: block; width: 100%; max-width: 100%; height: 100%; border: 0; background: var(--pv-frame); }
|
|
26
|
+
.pvs-stage[data-fixed] iframe { box-shadow: 0 0 0 1px var(--pv-line); }
|
|
27
|
+
@media (max-width: 900px) { .pvs-title, .pvs-hint { display: none; } .pvs-bar { justify-content: center; } }
|
|
28
|
+
</style>
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<header class="pvs-bar">
|
|
32
|
+
<div class="pvs-title"></div>
|
|
33
|
+
<div class="pvs-tools" role="toolbar" aria-label="预览设置"></div>
|
|
34
|
+
<div class="pvs-hint"></div>
|
|
35
|
+
</header>
|
|
36
|
+
<main class="pvs-stage"><iframe title="预览"></iframe></main>
|
|
37
|
+
<script>
|
|
38
|
+
(() => {
|
|
39
|
+
const CONFIG = /*CONFIG*/;
|
|
40
|
+
const PAGE = /*PAGE*/;
|
|
41
|
+
const WIDTH_NAMES = { normal: '正常', wide: '宽', narrow: '窄', phone: '手机' };
|
|
42
|
+
const groups = [
|
|
43
|
+
...(CONFIG.options || []).map(o => ({ key: o.key, label: o.label, values: Object.entries(o.values) })),
|
|
44
|
+
{ key: 'theme', label: '主题', values: [['light', '浅色'], ['dark', '深色']] },
|
|
45
|
+
{ key: 'width', label: '宽度', values: Object.keys(CONFIG.widths).map(k => [k, WIDTH_NAMES[k] || k]) },
|
|
46
|
+
];
|
|
47
|
+
const fromHash = Object.fromEntries(new URLSearchParams(location.hash.slice(1)));
|
|
48
|
+
const state = {};
|
|
49
|
+
for (const g of groups) {
|
|
50
|
+
const fallback = g.key === 'theme' ? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : g.values[0][0];
|
|
51
|
+
state[g.key] = g.values.some(([v]) => v === fromHash[g.key]) ? fromHash[g.key] : fallback;
|
|
52
|
+
}
|
|
53
|
+
window.__pvState = state;
|
|
54
|
+
|
|
55
|
+
const $ = s => document.querySelector(s);
|
|
56
|
+
const esc = value => String(value).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
|
57
|
+
const frame = $('iframe'), stage = $('.pvs-stage');
|
|
58
|
+
$('.pvs-title').textContent = CONFIG.title || '';
|
|
59
|
+
$('.pvs-hint').textContent = CONFIG.hint || '';
|
|
60
|
+
$('.pvs-tools').innerHTML = groups.map(g => '<div class="pvs-group"><span class="pvs-label">' + esc(g.label) + '</span>' +
|
|
61
|
+
g.values.map(([v, label]) => '<button type="button" data-k="' + esc(g.key) + '" data-v="' + esc(v) + '">' + esc(label) + '</button>').join('') + '</div>').join('');
|
|
62
|
+
|
|
63
|
+
function show() {
|
|
64
|
+
document.documentElement.dataset.theme = state.theme;
|
|
65
|
+
document.querySelectorAll('.pvs-tools button').forEach(b => b.setAttribute('aria-pressed', String(state[b.dataset.k] === b.dataset.v)));
|
|
66
|
+
const px = CONFIG.widths[state.width];
|
|
67
|
+
frame.style.width = px ? px + 'px' : '100%';
|
|
68
|
+
stage.toggleAttribute('data-fixed', Boolean(px));
|
|
69
|
+
history.replaceState(null, '', '#' + new URLSearchParams(state));
|
|
70
|
+
}
|
|
71
|
+
function set(next, key) {
|
|
72
|
+
Object.assign(state, next);
|
|
73
|
+
show();
|
|
74
|
+
frame.contentWindow?.postMessage({ pv: 'state', state: { ...state }, key }, '*');
|
|
75
|
+
}
|
|
76
|
+
$('.pvs-tools').addEventListener('click', e => {
|
|
77
|
+
const b = e.target.closest('button');
|
|
78
|
+
if (b && state[b.dataset.k] !== b.dataset.v) set({ [b.dataset.k]: b.dataset.v }, b.dataset.k);
|
|
79
|
+
});
|
|
80
|
+
addEventListener('message', e => {
|
|
81
|
+
if (e.source !== frame.contentWindow) return;
|
|
82
|
+
if (e.data?.pv === 'set') set(e.data.state, Object.keys(e.data.state)[0]);
|
|
83
|
+
if (e.data?.pv === 'hello') frame.contentWindow.postMessage({ pv: 'state', state: { ...state }, key: 'init' }, '*');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
let loaded;
|
|
87
|
+
const ready = new Promise(r => { loaded = r; });
|
|
88
|
+
frame.addEventListener('load', () => loaded(), { once: true });
|
|
89
|
+
show();
|
|
90
|
+
frame.srcdoc = PAGE;
|
|
91
|
+
|
|
92
|
+
// For scripts that drive the preview (screenshots, checks): run code inside the page, find where an element is.
|
|
93
|
+
window.pv = {
|
|
94
|
+
ready, state, set,
|
|
95
|
+
run: code => frame.contentWindow.eval('(async () => {\n' + code + '\n})()'),
|
|
96
|
+
rect(selector) {
|
|
97
|
+
const f = frame.getBoundingClientRect(), el = frame.contentDocument.querySelector(selector);
|
|
98
|
+
if (!el) return null;
|
|
99
|
+
const r = el.getBoundingClientRect();
|
|
100
|
+
return { x: f.left + r.left + r.width / 2, y: f.top + r.top + r.height / 2, left: f.left + r.left, top: f.top + r.top, width: r.width, height: r.height };
|
|
101
|
+
},
|
|
102
|
+
frameRect() { const r = frame.getBoundingClientRect(); return { x: r.left, y: r.top, width: r.width, height: r.height }; },
|
|
103
|
+
};
|
|
104
|
+
})();
|
|
105
|
+
</script>
|
|
106
|
+
</body>
|
|
107
|
+
</html>
|
|
@@ -42,7 +42,7 @@ export default {
|
|
|
42
42
|
},
|
|
43
43
|
label: t("分割线"), icon: svg('<path d="M3 10h14"/><path d="M5.5 6h9M5.5 14h9" stroke-opacity=".35"/>'),
|
|
44
44
|
hint: "---", markdown: [{ match: /^---$/, alone: true }], words: "分割线 分隔线 横线 fengexian fgx hr divider line rule",
|
|
45
|
-
glance: { row: 5, name: "分割线", write: "`<hr>`
|
|
45
|
+
glance: { row: 5, name: "分割线", write: "`<hr>` where a visual break is useful", rule: "The page" },
|
|
46
46
|
},
|
|
47
47
|
},
|
|
48
48
|
};
|
|
@@ -61,6 +61,12 @@ export const STYLE = `
|
|
|
61
61
|
.ge-note-delete { position: absolute; top: 7px; right: 7px; width: 24px; height: 24px; box-sizing: border-box; margin: 0; padding: 0; border: 0; border-radius: 6px; background: transparent; display: grid; place-items: center; color: var(--ge-ico3); cursor: pointer; }
|
|
62
62
|
.ge-note-delete svg { width: 18px; height: 18px; pointer-events: none; }
|
|
63
63
|
.ge-note-bar { width: 2px; border-radius: 1px; background: var(--ge-note-line); }
|
|
64
|
+
/* Acted on (gallery notes done): grey, as a resolved comment is quieter. */
|
|
65
|
+
.ge-note-card[data-done] .ge-note-face { opacity: 0.55; }
|
|
66
|
+
.ge-note-card[data-done] textarea { color: var(--ge-tx3); }
|
|
67
|
+
.ge-note-marker[data-done] { color: var(--ge-ico3); opacity: 0.6; }
|
|
68
|
+
.ge-note-bar[data-done] { opacity: 0.4; }
|
|
69
|
+
.ge-note-card[data-done][data-front] .ge-note-face { opacity: 1; }
|
|
64
70
|
.ge-note-bar[data-hot] { background: var(--ge-note-hot-line); }
|
|
65
71
|
@media print {
|
|
66
72
|
.ge-note-layer { display: none; }
|
|
@@ -113,6 +113,7 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
|
|
|
113
113
|
let items = new Map(); // id -> { id, created, updated, text, anchor } in page order, as the server last said, with the page's own changes
|
|
114
114
|
let filed = new Map(); // id -> its text as the file last held it (the server's last answer): what 撤销 brings back of one emptied in its card
|
|
115
115
|
let version = null; // the file's, as last read or written
|
|
116
|
+
let toldAt = null; // when the removal by a gallery command the workbench was last told of happened (gallery notes remove)
|
|
116
117
|
const lost = new Map(); // id -> an annotation a check deleted this visit (its words gone): back when an undo brings them back
|
|
117
118
|
const timers = new Map(); // id -> the timer of its save (typed, not saved yet)
|
|
118
119
|
const sending = new Set(); // ids whose save is on its way
|
|
@@ -218,9 +219,16 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
|
|
|
218
219
|
resolved = false;
|
|
219
220
|
everything = true;
|
|
220
221
|
remeasure = true;
|
|
222
|
+
marksChanged = true; // done ones are drawn quietly
|
|
221
223
|
if (items.size > 0) followText();
|
|
222
224
|
wake();
|
|
223
225
|
changed();
|
|
226
|
+
// An Agent removed some at the reader's word (gallery notes remove): offered back as a card's delete button's is.
|
|
227
|
+
const gone = answer.commandRemoved;
|
|
228
|
+
if (gone?.items?.length > 0 && gone.at !== toldAt) {
|
|
229
|
+
toldAt = gone.at;
|
|
230
|
+
removed(gone.items);
|
|
231
|
+
}
|
|
224
232
|
return true;
|
|
225
233
|
}
|
|
226
234
|
|
|
@@ -506,6 +514,7 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
|
|
|
506
514
|
function cardOf(id) {
|
|
507
515
|
const card = cards.get(id) ?? makeCard(id);
|
|
508
516
|
const text = items.get(id)?.text ?? "";
|
|
517
|
+
card.node.toggleAttribute("data-done", items.get(id)?.done === true);
|
|
509
518
|
if (card.field.value !== text && document.activeElement !== card.field) {
|
|
510
519
|
card.field.value = text;
|
|
511
520
|
card.height = 0;
|
|
@@ -829,7 +838,7 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
|
|
|
829
838
|
.sort((a, b) => a.at.compareBoundaryPoints(Range.START_TO_START, b.at));
|
|
830
839
|
return listed.map(({ item, view }) => {
|
|
831
840
|
const words = view.range || view.unknown ? view.excerpt?.quote ?? item.anchor?.quote : blockSelection(view.units)?.excerpt.quote;
|
|
832
|
-
return { id: item.id, text: item.text, quote: tidy(words) };
|
|
841
|
+
return { id: item.id, text: item.text, quote: tidy(words), done: item.done === true };
|
|
833
842
|
});
|
|
834
843
|
}
|
|
835
844
|
|
|
@@ -1071,6 +1080,7 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
|
|
|
1071
1080
|
const strong = [];
|
|
1072
1081
|
for (const [id, view] of views) {
|
|
1073
1082
|
if (!view.range || view.hidden || view.folded) continue;
|
|
1083
|
+
if (id !== hotId && items.get(id)?.done) continue; // acted on: the words are left unmarked
|
|
1074
1084
|
(id === hotId ? strong : plain).push(view.range);
|
|
1075
1085
|
}
|
|
1076
1086
|
if (shown?.id && !views.has(shown.id) && made.get(shown.id)?.range) strong.push(made.get(shown.id).range);
|
|
@@ -1078,7 +1088,9 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
|
|
|
1078
1088
|
CSS.highlights.set(HOT, new Highlight(...strong));
|
|
1079
1089
|
for (const [id, view] of views) {
|
|
1080
1090
|
view.marker?.toggleAttribute("data-hot", id === hotId);
|
|
1091
|
+
view.marker?.toggleAttribute("data-done", items.get(id)?.done === true);
|
|
1081
1092
|
view.bar?.toggleAttribute("data-hot", id === shown?.id || id === hot);
|
|
1093
|
+
view.bar?.toggleAttribute("data-done", items.get(id)?.done === true);
|
|
1082
1094
|
}
|
|
1083
1095
|
}
|
|
1084
1096
|
|
|
@@ -1167,7 +1179,7 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
|
|
|
1167
1179
|
// BAR.reach), so that each keeps the pointer it is for, and within the column's right room (margin.mjs).
|
|
1168
1180
|
function markerAcross(right, units) {
|
|
1169
1181
|
const section = units.map((unit) => sectionOps()?.sectionOf(unit)).find(Boolean);
|
|
1170
|
-
return markerLeft(right, windowWidth(), section ? Math.min(section.getBoundingClientRect().right + BAR.
|
|
1182
|
+
return markerLeft(right, windowWidth(), section ? Math.min(section.getBoundingClientRect().right + BAR.gap + BAR.width + 2, right + rightRoom() - RAIL.marker) : 0);
|
|
1171
1183
|
}
|
|
1172
1184
|
|
|
1173
1185
|
// Where a marker of `unit` alone stands (window coordinates): the place of the button beside it
|
|
@@ -1572,6 +1584,14 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
|
|
|
1572
1584
|
const answer = await fetch(`${ROUTE}?slug=${encodeURIComponent(slug)}&as=copy`, { headers: { "X-Gallery-Token": token }, cache: "no-store" }).then((response) => response.json()).catch(() => ({ ok: false, reason: "offline" }));
|
|
1573
1585
|
return answer.ok ? { ok: true, data: { title: answer.title, path: answer.path, items: answer.items } } : { ok: false, reason: answer.reason ?? "error" };
|
|
1574
1586
|
},
|
|
1587
|
+
// 删除已处理: those marked done (gallery notes done), what was typed saved first; 撤销 brings them back (the workbench's).
|
|
1588
|
+
async clearDone() {
|
|
1589
|
+
await flush();
|
|
1590
|
+
const answer = await send({ removeDone: true });
|
|
1591
|
+
if (!answer.ok) return { ok: false, reason: answer.reason ?? "error" };
|
|
1592
|
+
take(answer);
|
|
1593
|
+
return { ok: true, slug, removed: answer.removed ?? [] };
|
|
1594
|
+
},
|
|
1575
1595
|
async clear() {
|
|
1576
1596
|
if (focused()) document.activeElement.blur();
|
|
1577
1597
|
close();
|
|
@@ -345,7 +345,7 @@ export function createColumnGaps({ active, struct, look, say, settle = () => {}
|
|
|
345
345
|
}, true);
|
|
346
346
|
document.addEventListener("pointerup", () => { if (drag) finish(true); }, true);
|
|
347
347
|
endsGesture({ active: () => Boolean(drag), cancel: () => finish(false) });
|
|
348
|
-
cancelOnEscape({ active: () => Boolean(drag), cancel: () => { finish(false); say(t("
|
|
348
|
+
cancelOnEscape({ active: () => Boolean(drag), cancel: () => { finish(false); say(t("已取消调整")); } });
|
|
349
349
|
addEventListener("scroll", () => { if (hover && !drag) place(hover); }, { passive: true, capture: true });
|
|
350
350
|
|
|
351
351
|
return {
|
|
@@ -151,7 +151,7 @@ export function createConflicts({ act, nodeOf, restorable, copyBlock, say }) {
|
|
|
151
151
|
const node = nodeOf(entry);
|
|
152
152
|
const parts = node?.parentElement ? [...node.parentElement.children].filter((other) => partOwner(other)?.owner === node) : [];
|
|
153
153
|
const text = [node, ...parts].filter((part) => part?.isConnected).map(blocksText).filter(Boolean).join("\n") || mine;
|
|
154
|
-
if (text) navigator.clipboard?.writeText(text).then(() => say(t("
|
|
154
|
+
if (text) navigator.clipboard?.writeText(text).then(() => say(t("已复制")));
|
|
155
155
|
else say(t("没有可复制的文字。"));
|
|
156
156
|
return;
|
|
157
157
|
}
|
|
@@ -301,7 +301,7 @@ export function createDrag({ info, selection, struct, say, closeMenu, schedule,
|
|
|
301
301
|
for (const details of opened) if (!(lands && details.contains(drop.container))) details.open = false;
|
|
302
302
|
if (!lands) {
|
|
303
303
|
selection.select(targets.filter(alive), { quiet: true });
|
|
304
|
-
say(!commit ? t("
|
|
304
|
+
say(!commit ? t("已取消移动") : t("位置没有变化"));
|
|
305
305
|
return;
|
|
306
306
|
}
|
|
307
307
|
if (drop.aside) {
|
|
@@ -51,6 +51,7 @@ import { createSelectAll, topStep } from "./select-all/index.mjs";
|
|
|
51
51
|
import { revealText } from "./find/index.mjs";
|
|
52
52
|
import { createOutline } from "./outline/index.mjs";
|
|
53
53
|
import { createAnnotate } from "./annotate/index.mjs";
|
|
54
|
+
import { watchFocus } from "./focus.mjs";
|
|
54
55
|
import { createChanges as createAgentChanges } from "./changes/index.mjs";
|
|
55
56
|
import { BLOCK_TYPES, HOSTS, PLAIN_TEXT, TYPE_INFO, accepts, blankSaysOf, boxOwner, texOnly, textFilters } from "../components/registry.mjs";
|
|
56
57
|
import { createPages } from "../components/pages.mjs";
|
|
@@ -519,7 +520,7 @@ function committed({ conflicts: held, merged, outdated, forced = [], rekeyed = [
|
|
|
519
520
|
}
|
|
520
521
|
// What the server says besides (service.mjs retitle, reicon: "标题已更新", what did not follow it),
|
|
521
522
|
// each note in the UI's language, as one sentence.
|
|
522
|
-
else if (receipt.notes?.length) say(t("{0}。", receipt.notes.map((note) => t(note)).join(t(";"))), "note", receipt.notes.length > 1 ? 6000 : undefined);
|
|
523
|
+
else if (receipt.notes?.length) say(receipt.notes.length === 1 ? t(receipt.notes[0]) : t("{0}。", receipt.notes.map((note) => t(note)).join(t(";"))), "note", receipt.notes.length > 1 ? 6000 : undefined);
|
|
523
524
|
}
|
|
524
525
|
|
|
525
526
|
// ---------- conflicts ----------
|
|
@@ -1148,7 +1149,7 @@ async function saveNow() {
|
|
|
1148
1149
|
const outcome = await autosave?.flush();
|
|
1149
1150
|
const { held, drafts } = view();
|
|
1150
1151
|
const unfitHeld = unfitCount();
|
|
1151
|
-
if (outcome === "saved" || outcome === "nothing") say(unfitHeld === 0 && held > 0 ? t("其余修改已保存;红框处的冲突还没有保存。") : held > unfitHeld ? t("其余修改已保存;红框处的内容还没有保存。") : held > 0 ? t("其余修改已保存;红框处的内容文档存不下,没有保存。") : drafts > 0 ? t("其余修改已保存;还没写完的块(红框或空标题)没有保存。") : t("
|
|
1152
|
+
if (outcome === "saved" || outcome === "nothing") say(unfitHeld === 0 && held > 0 ? t("其余修改已保存;红框处的冲突还没有保存。") : held > unfitHeld ? t("其余修改已保存;红框处的内容还没有保存。") : held > 0 ? t("其余修改已保存;红框处的内容文档存不下,没有保存。") : drafts > 0 ? t("其余修改已保存;还没写完的块(红框或空标题)没有保存。") : t("已保存"));
|
|
1152
1153
|
else if (outcome === "offline") say(t("{0}:修改暂存在浏览器里,连上后自动保存。", OFFLINE), "error");
|
|
1153
1154
|
return outcome;
|
|
1154
1155
|
}
|
|
@@ -1177,6 +1178,32 @@ function post(route, body) {
|
|
|
1177
1178
|
return postAnswer(route, MAP.token, body);
|
|
1178
1179
|
}
|
|
1179
1180
|
|
|
1181
|
+
// A write of the file the reader asked for elsewhere (历史版本's 恢复 and its 撤销: POST /__gallery/edit/history,
|
|
1182
|
+
// `body` its action), as 撤回 of the Agent's changes goes (changes/): what waits is saved first, and nothing may
|
|
1183
|
+
// wait for the reader (a text not saved, a conflict, offline: `pending`, nothing done); then the server writes it
|
|
1184
|
+
// and the page is shown again as after a write of its own (autosave showOwn: no question of what that write put
|
|
1185
|
+
// back of the reader's, who asked for it). Answers the server's answer (`undo` for a restore) as soon as it
|
|
1186
|
+
// comes, before the page goes (the workbench takes a page's reload for its end, and a reply after it for none).
|
|
1187
|
+
async function writeOwn(body) {
|
|
1188
|
+
if (!editing || !autosave) return { ok: false, reason: "editing" };
|
|
1189
|
+
checkAll();
|
|
1190
|
+
await annotate?.flush();
|
|
1191
|
+
await autosave.flush();
|
|
1192
|
+
const state = autosave.state;
|
|
1193
|
+
if (state.waiting > 0 || state.syncing || state.held > 0 || state.offline) return { ok: false, reason: "pending" };
|
|
1194
|
+
return new Promise((resolve) => {
|
|
1195
|
+
let answered = false;
|
|
1196
|
+
autosave.showOwn(async () => {
|
|
1197
|
+
const answer = await post("/__gallery/edit/history", { slug: MAP.slug, ...body });
|
|
1198
|
+
answered = true;
|
|
1199
|
+
const { id: _, ...said } = answer; // the reply's own id is the command's
|
|
1200
|
+
resolve(said);
|
|
1201
|
+
await new Promise((sent) => setTimeout(sent, 0)); // the reply posted first
|
|
1202
|
+
return answer.ok && !answer.same ? answer.version || true : false;
|
|
1203
|
+
}).then(() => { if (!answered) resolve({ ok: false, reason: "pending" }); }, () => { if (!answered) resolve({ ok: false, reason: "pending" }); });
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1180
1207
|
let workbench = null; // the session with a workbench embedding the page (session.mjs), if any
|
|
1181
1208
|
let released = false; // the workbench closes the page: what does not get saved in time stays in the crash buffer
|
|
1182
1209
|
|
|
@@ -1232,6 +1259,13 @@ const COMMANDS = {
|
|
|
1232
1259
|
}
|
|
1233
1260
|
return exportNow();
|
|
1234
1261
|
},
|
|
1262
|
+
// 历史版本's 恢复 of a version and the 撤销 of one (the workbench's: features/history/), the reader's own writes.
|
|
1263
|
+
async "restore-version"({ version }) {
|
|
1264
|
+
return writeOwn({ action: "restore", version });
|
|
1265
|
+
},
|
|
1266
|
+
async "undo-version"({ undo }) {
|
|
1267
|
+
return writeOwn({ action: "undo", undo });
|
|
1268
|
+
},
|
|
1235
1269
|
// 搜索文档 opened the report at a hit: its words scrolled into view and marked for a moment (find/).
|
|
1236
1270
|
async reveal(message) {
|
|
1237
1271
|
return revealText(message, (node) => {
|
|
@@ -1638,7 +1672,7 @@ function showLost() {
|
|
|
1638
1672
|
panelItem(
|
|
1639
1673
|
element("p", { text: LOST[item.why] ?? LOST.refused }),
|
|
1640
1674
|
element("div", { class: "ge-version", text }),
|
|
1641
|
-
element("div", { class: "ge-actions" }, textButton(t("复制文字"), () => navigator.clipboard?.writeText(text).then(() => say(t("
|
|
1675
|
+
element("div", { class: "ge-actions" }, textButton(t("复制文字"), () => navigator.clipboard?.writeText(text).then(() => say(t("已复制"))))),
|
|
1642
1676
|
);
|
|
1643
1677
|
}
|
|
1644
1678
|
panelItem(element("div", { class: "ge-actions" }, textButton(t("都处理好了,不再保留"), () => {
|
|
@@ -1703,6 +1737,8 @@ function boot() {
|
|
|
1703
1737
|
changed: () => workbench?.update(),
|
|
1704
1738
|
removed: (items) => workbench?.annotationsRemoved(items),
|
|
1705
1739
|
});
|
|
1740
|
+
// Where the reader is (the caret's block, the words and blocks selected), told to the workbench for gallery status.
|
|
1741
|
+
if (!SOLO) watchFocus({ picked: () => blocks?.picked ?? [], crossTexts: () => crossText.texts(), tell: (where) => workbench?.focus(where) });
|
|
1706
1742
|
// Read at once, drawn once the page is ready; commits and autosave are made as the page edits (enter).
|
|
1707
1743
|
if (!SOLO) agentChanges = createAgentChanges({
|
|
1708
1744
|
slug: MAP.slug, token: MAP.token, commits: () => commits, autosave: () => autosave, say, session,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { cutWords } from "../../shared/presence.mjs";
|
|
2
|
+
import { editable } from "./caret.mjs";
|
|
3
|
+
import { unitElement } from "./core.mjs";
|
|
4
|
+
import { readText, textSelection } from "./annotate/anchors.mjs";
|
|
5
|
+
|
|
6
|
+
// Where the reader is on the page, for gallery status (shared/presence.mjs): the block the caret is in, or the words and
|
|
7
|
+
// blocks selected, told to the workbench (session.mjs `focus`), which reports them to the server with its tab's other facts.
|
|
8
|
+
// Only what the reader has in front of them: the block's data-block-id (null for one the file has no id for yet: its commit is
|
|
9
|
+
// on its way) and its words, or the selected words, cut to 200 characters (shared/presence.mjs cutWords) before they leave the page.
|
|
10
|
+
//
|
|
11
|
+
// Given: picked() (the blocks selected whole: blocks.mjs), crossTexts() (text selected across blocks: text-select/, [{ host, range }]
|
|
12
|
+
// or null), tell({ caret, selection }) (the workbench's `focus`). Nothing is told while nothing changed: the selection and the caret
|
|
13
|
+
// are read 250ms after the last event that may have moved them (selectionchange, a press, a key, the focus), and the blocks selected
|
|
14
|
+
// whole, which no browser event announces, are looked at every 2s.
|
|
15
|
+
|
|
16
|
+
const AFTER = 250;
|
|
17
|
+
const WATCH = 2000;
|
|
18
|
+
|
|
19
|
+
/** A block's id as the file has it, or null. */
|
|
20
|
+
const idOf = (unit) => unit?.getAttribute?.("data-block-id") || null;
|
|
21
|
+
/** A block's words, cut. */
|
|
22
|
+
const wordsOf = (unit) => cutWords(readText([unit]).text).text;
|
|
23
|
+
|
|
24
|
+
/** The blocks the reader has selected whole: { kind: "blocks", blocks: [ids], text: the first one's words }, or null. */
|
|
25
|
+
function blockSelection(units) {
|
|
26
|
+
const list = units.filter((unit) => unit?.isConnected);
|
|
27
|
+
if (list.length === 0) return null;
|
|
28
|
+
return { kind: "blocks", blocks: list.map(idOf).filter(Boolean), text: wordsOf(list[0]) };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Words selected as one range: { kind: "text", blocks: [ids], text, length }, or null. */
|
|
32
|
+
function wordsSelection(range) {
|
|
33
|
+
const found = textSelection(range);
|
|
34
|
+
if (found) {
|
|
35
|
+
const words = cutWords(found.excerpt.quote);
|
|
36
|
+
return { kind: "text", blocks: found.units.map(idOf).filter(Boolean), text: words.text, length: found.excerpt.quote.length };
|
|
37
|
+
}
|
|
38
|
+
// Words the annotations leave out (the title, a code block's): the block holding them, the words as the browser gives them.
|
|
39
|
+
const unit = unitElement(range.startContainer.nodeType === 1 ? range.startContainer : range.startContainer.parentElement);
|
|
40
|
+
const text = range.toString();
|
|
41
|
+
const words = cutWords(text);
|
|
42
|
+
return words.text ? { kind: "text", blocks: [idOf(unit)].filter(Boolean), text: words.text, length: text.length } : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What the reader has selected now (blocks, text across blocks, text in one block), or null. */
|
|
46
|
+
function selectionNow({ picked, crossTexts }) {
|
|
47
|
+
const blocks = picked();
|
|
48
|
+
if (blocks.length > 0) return blockSelection(blocks);
|
|
49
|
+
const texts = crossTexts();
|
|
50
|
+
if (texts?.length) {
|
|
51
|
+
const range = document.createRange();
|
|
52
|
+
range.setStart(texts[0].range.startContainer, texts[0].range.startOffset);
|
|
53
|
+
range.setEnd(texts.at(-1).range.endContainer, texts.at(-1).range.endOffset);
|
|
54
|
+
return wordsSelection(range);
|
|
55
|
+
}
|
|
56
|
+
const selection = getSelection();
|
|
57
|
+
if (!selection?.rangeCount || selection.isCollapsed) return null;
|
|
58
|
+
const range = selection.getRangeAt(0);
|
|
59
|
+
if (!editable(range.commonAncestorContainer) && !editable(range.startContainer)) return null;
|
|
60
|
+
return wordsSelection(range.cloneRange());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The block the caret is in: { block, text }, or null (no caret in the page's text). */
|
|
64
|
+
function caretNow() {
|
|
65
|
+
const selection = getSelection();
|
|
66
|
+
const host = selection?.rangeCount ? editable(selection.anchorNode) : null;
|
|
67
|
+
const unit = host ? unitElement(host) : null;
|
|
68
|
+
return unit ? { block: idOf(unit), text: wordsOf(unit) } : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function watchFocus({ picked, crossTexts, tell }) {
|
|
72
|
+
let last = null;
|
|
73
|
+
let timer = 0;
|
|
74
|
+
const read = () => {
|
|
75
|
+
timer = 0;
|
|
76
|
+
const selection = selectionNow({ picked, crossTexts });
|
|
77
|
+
const caret = selection ? null : caretNow();
|
|
78
|
+
const key = JSON.stringify({ caret, selection });
|
|
79
|
+
if (key === last) return;
|
|
80
|
+
last = key;
|
|
81
|
+
tell({ caret, selection });
|
|
82
|
+
};
|
|
83
|
+
const soon = () => {
|
|
84
|
+
if (!timer) timer = setTimeout(read, AFTER);
|
|
85
|
+
};
|
|
86
|
+
for (const type of ["selectionchange", "pointerup", "keyup", "focusin", "focusout"]) document.addEventListener(type, soon, { capture: true, passive: true });
|
|
87
|
+
// The blocks selected whole: no event says they changed, so their list is compared now and then.
|
|
88
|
+
let held = "";
|
|
89
|
+
const watching = setInterval(() => {
|
|
90
|
+
const now = picked().map(idOf).join(",");
|
|
91
|
+
if (now !== held) {
|
|
92
|
+
held = now;
|
|
93
|
+
soon();
|
|
94
|
+
}
|
|
95
|
+
}, WATCH);
|
|
96
|
+
return {
|
|
97
|
+
/** Tell now (the page is ready, or the workbench asks again). */
|
|
98
|
+
now: () => { last = null; clearTimeout(timer); read(); },
|
|
99
|
+
stop() {
|
|
100
|
+
clearInterval(watching);
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|