@heycys/gallery 0.1.0 → 0.1.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/THIRD-PARTY-NOTICES.md +51 -1
- package/browser/editor/components/callout/index.mjs +40 -15
- package/browser/editor/components/columns/page.mjs +41 -28
- package/browser/editor/components/details/index.mjs +2 -1
- package/browser/editor/components/dialog/index.mjs +2 -1
- package/browser/editor/components/dialog/page.mjs +0 -18
- package/browser/editor/components/divider/index.mjs +1 -1
- package/browser/editor/components/figure/page.mjs +10 -1
- package/browser/editor/components/file/page.mjs +2 -2
- package/browser/editor/components/flowchart/add.mjs +2 -2
- package/browser/editor/components/heading/index.mjs +2 -0
- package/browser/editor/components/heading/page.mjs +6 -0
- package/browser/editor/components/highlight/index.mjs +4 -20
- package/browser/editor/components/icons.mjs +5 -2
- package/browser/editor/components/image/page.mjs +2 -2
- package/browser/editor/components/installed/index.mjs +71 -25
- package/browser/editor/components/installed/page.mjs +122 -21
- package/browser/editor/components/kit.mjs +3 -0
- package/browser/editor/components/media/page.mjs +44 -14
- package/browser/editor/components/pages.mjs +2 -2
- package/browser/editor/components/registry.mjs +56 -30
- package/browser/editor/components/section/index.mjs +2 -1
- package/browser/editor/components/set-apart.mjs +24 -0
- package/browser/editor/components/table/index.mjs +5 -3
- package/browser/editor/components/table/page.mjs +29 -18
- package/browser/editor/page/annotate/anchors.mjs +4 -3
- package/browser/editor/page/annotate/index.mjs +61 -34
- package/browser/editor/page/autosave.mjs +50 -4
- package/browser/editor/page/beside.mjs +5 -5
- package/browser/editor/page/block-menu/colors.mjs +3 -1
- package/browser/editor/page/block-menu/index.mjs +8 -6
- package/browser/editor/page/blocks.mjs +16 -7
- package/browser/editor/page/caret.mjs +4 -1
- package/browser/editor/page/changes/bar.mjs +102 -0
- package/browser/editor/page/changes/ghost.mjs +206 -0
- package/browser/editor/page/changes/gone-words.mjs +94 -0
- package/browser/editor/page/changes/index.mjs +447 -0
- package/browser/editor/page/changes/words.mjs +27 -0
- package/browser/editor/page/clipboard/import/colors.mjs +3 -3
- package/browser/editor/page/clipboard/index.mjs +32 -13
- package/browser/editor/page/clipboard/own/blocks.mjs +14 -5
- package/browser/editor/page/clipboard/own/describe.mjs +16 -7
- package/browser/editor/page/clipboard/own/export.mjs +6 -6
- package/browser/editor/page/clipboard/own/selection.mjs +2 -2
- package/browser/editor/page/clipboard/own/writers.mjs +0 -11
- package/browser/editor/page/columns/index.mjs +1 -1
- package/browser/editor/page/commit.mjs +19 -4
- package/browser/editor/page/conflicts.mjs +6 -26
- package/browser/editor/page/convert.mjs +8 -7
- package/browser/editor/page/core.mjs +55 -17
- package/browser/editor/page/drag/index.mjs +40 -24
- package/browser/editor/page/drag/preview.mjs +9 -4
- package/browser/editor/page/editor.mjs +70 -15
- package/browser/editor/page/emoji-picker/emoji-tab.mjs +22 -13
- package/browser/editor/page/emoji-picker/grid.mjs +7 -5
- package/browser/editor/page/emoji-picker/icon-tab.mjs +4 -4
- package/browser/editor/page/emoji-picker/icons.mjs +0 -1
- package/browser/editor/page/emoji-picker/index.mjs +11 -9
- package/browser/editor/page/emoji-picker/styles.mjs +161 -2
- package/browser/editor/page/emoji-picker/support.mjs +6 -4
- package/browser/editor/page/emoji-picker/upload.mjs +12 -11
- package/browser/editor/page/fills.mjs +5 -3
- package/browser/editor/page/fold/index.mjs +348 -0
- package/browser/editor/page/fold/rule.mjs +125 -0
- package/browser/editor/page/folded.mjs +23 -0
- package/browser/editor/page/format-bar/bar.mjs +2 -1
- package/browser/editor/page/format-bar/index.mjs +1 -1
- package/browser/editor/page/geometry.mjs +8 -6
- package/browser/editor/page/handle/index.mjs +17 -6
- package/browser/editor/page/hints/index.mjs +1 -1
- package/browser/editor/page/insert/index.mjs +30 -17
- package/browser/editor/page/long-runs.mjs +75 -0
- package/browser/editor/page/look.mjs +15 -3
- package/browser/editor/page/margin.mjs +45 -24
- package/browser/editor/page/menu.mjs +69 -25
- package/browser/editor/page/merge.mjs +24 -0
- package/browser/editor/page/notices.mjs +1 -1
- package/browser/editor/page/offered.mjs +1 -1
- package/browser/editor/page/outline/folds.mjs +11 -10
- package/browser/editor/page/outline/index.mjs +131 -45
- package/browser/editor/page/outline/list.mjs +23 -11
- package/browser/editor/page/outline/read.mjs +1 -1
- package/browser/editor/page/outline/strip.mjs +7 -1
- package/browser/editor/page/panel.mjs +5 -4
- package/browser/editor/page/resize/index.mjs +1 -1
- package/browser/editor/page/selection/delete.mjs +8 -6
- package/browser/editor/page/selection/index.mjs +10 -3
- package/browser/editor/page/session.mjs +54 -3
- package/browser/editor/page/table/cell-bar.mjs +4 -1
- package/browser/editor/page/table/cells.mjs +1 -1
- package/browser/editor/page/table/icons.mjs +1 -1
- package/browser/editor/page/table/index.mjs +13 -8
- package/browser/editor/page/table/widths.mjs +9 -4
- package/browser/editor/page/text-select/index.mjs +18 -3
- package/browser/editor/page/tooltip.mjs +1 -1
- package/browser/editor/page/type-grid.mjs +4 -3
- package/browser/editor/page/typing/code-edges.mjs +63 -17
- package/browser/editor/page/typing/index.mjs +12 -6
- package/browser/editor/page/typing/leave.mjs +1 -1
- package/browser/shared/annotations.mjs +8 -10
- package/browser/shared/code-folds.mjs +5 -1
- package/browser/shared/colors.mjs +15 -13
- package/browser/shared/component-catalog.mjs +24 -10
- package/browser/shared/components/callout.mjs +4 -2
- package/browser/shared/components/index.mjs +61 -7
- package/browser/shared/components/installed.mjs +75 -5
- package/browser/shared/components/page.mjs +10 -1
- package/browser/shared/components/table.mjs +1 -1
- package/browser/shared/components/text.mjs +2 -2
- package/browser/shared/document-look.mjs +178 -0
- package/browser/shared/emoji/data/LICENSE-lucide.txt +43 -0
- package/browser/shared/emoji/data/README.md +4 -2
- package/browser/shared/emoji/data/categories.json +1 -1
- package/browser/shared/emoji/data/keywords-en.json +1 -1
- package/browser/shared/emoji/data/keywords-zh.json +1 -1
- package/browser/shared/emoji/data/names.json +1 -1
- package/browser/shared/emoji/data/positions.json +1 -1
- package/browser/shared/emoji/data/twemoji-index.json +1 -1
- package/browser/shared/emoji/data/twemoji-svg.bin +0 -0
- package/browser/shared/emoji/data.mjs +10 -12
- package/browser/shared/emoji/emoji.mjs +8 -7
- package/browser/shared/emoji/icon-value.mjs +4 -3
- package/browser/shared/html-tree.mjs +11 -5
- package/browser/shared/i18n/en.mjs +67 -8
- package/browser/shared/long-runs.mjs +39 -0
- package/browser/shared/new-names.mjs +5 -2
- package/browser/shared/outline.mjs +194 -0
- package/browser/shared/preferences.mjs +10 -4
- package/browser/shared/session.mjs +5 -4
- package/browser/shared/workspace-look.mjs +91 -0
- package/browser/shared/zoom.mjs +5 -5
- package/dist/chunk-3HIRPVLU.mjs +2 -0
- package/dist/chunk-4FS7Z7WR.mjs +2 -0
- package/dist/{chunk-LHTFLRB6.mjs → chunk-5MHMDLZD.mjs} +2 -2
- package/dist/{chunk-YXAUQPVK.mjs → chunk-5ZKJ3H4P.mjs} +3 -3
- package/dist/{chunk-ZBIM6ARY.mjs → chunk-64K7IPLO.mjs} +55 -16
- package/dist/{chunk-QYKCJ7FG.mjs → chunk-6QXGZM36.mjs} +1 -1
- package/dist/chunk-6RGJOMJP.mjs +16 -0
- package/dist/{chunk-EUX7IC5J.mjs → chunk-7DTINW6U.mjs} +2 -2
- package/dist/{chunk-6CYG3Z4V.mjs → chunk-AJ73XJF3.mjs} +1 -1
- package/dist/chunk-AUZGSZJH.mjs +13 -0
- package/dist/{chunk-7QQBR7PB.mjs → chunk-BFE45AKL.mjs} +1 -1
- package/dist/chunk-BGGDRNYQ.mjs +10 -0
- package/dist/{chunk-L4DVONOG.mjs → chunk-BXO2ZY5P.mjs} +1 -1
- package/dist/chunk-CEH4MRCC.mjs +2 -0
- package/dist/chunk-CY2FNNAB.mjs +16 -0
- package/dist/{chunk-LC7YNQFG.mjs → chunk-DFUDV4LX.mjs} +1 -1
- package/dist/chunk-DKXG4XDG.mjs +2 -0
- package/dist/chunk-E7LWZRXY.mjs +4 -0
- package/dist/chunk-EZQ34M3F.mjs +124 -0
- package/dist/chunk-FDLXYYG4.mjs +4 -0
- package/dist/chunk-FUKQNOQS.mjs +9 -0
- package/dist/{chunk-DB4WKGYF.mjs → chunk-GB5SOYQJ.mjs} +2 -2
- package/dist/{chunk-ACOPDCNB.mjs → chunk-IVDLMMBD.mjs} +1 -1
- package/dist/chunk-JPOSLGOC.mjs +6 -0
- package/dist/chunk-JVHHHGEA.mjs +3 -0
- package/dist/chunk-L3QKH4OK.mjs +26 -0
- package/dist/{chunk-B3VTYBYR.mjs → chunk-LCTHTZIL.mjs} +2 -2
- package/dist/chunk-LEF6DRCK.mjs +59 -0
- package/dist/chunk-LS4F7JUM.mjs +2 -0
- package/dist/chunk-NO5NJCHO.mjs +2 -0
- package/dist/{chunk-UNUEWH4S.mjs → chunk-NQPVAU63.mjs} +1 -1
- package/dist/chunk-O35FIXRB.mjs +2 -0
- package/dist/chunk-PL6GHLGD.mjs +15 -0
- package/dist/{chunk-G7TOXONE.mjs → chunk-QJIR2BLP.mjs} +1 -1
- package/dist/chunk-QUPLVPB2.mjs +7 -0
- package/dist/chunk-R7BNENLY.mjs +4 -0
- package/dist/chunk-S6O4UHLC.mjs +2 -0
- package/dist/{chunk-HMHK3EQ6.mjs → chunk-SGJWQKEI.mjs} +1 -1
- package/dist/chunk-SYGN7GLU.mjs +2 -0
- package/dist/chunk-TYK6VNAB.mjs +10 -0
- package/dist/{chunk-EVJ3BX3M.mjs → chunk-U6KNPJPR.mjs} +1 -1
- package/dist/{chunk-YZ5Z6H4W.mjs → chunk-UALRNVSE.mjs} +1 -1
- package/dist/chunk-UMHUSOPJ.mjs +2 -0
- package/dist/chunk-V3HXVQEM.mjs +2 -0
- package/dist/{chunk-UXUK4PMC.mjs → chunk-V5JTW2LH.mjs} +1 -1
- package/dist/chunk-V5VXUJKG.mjs +2 -0
- package/dist/chunk-VBXMADMP.mjs +2 -0
- package/dist/chunk-VNV55RUA.mjs +2 -0
- package/dist/chunk-VPD2PS35.mjs +2 -0
- package/dist/chunk-W3BGBJQW.mjs +2 -0
- package/dist/chunk-W4VKN3GF.mjs +7 -0
- package/dist/chunk-W5BXXPDJ.mjs +3 -0
- package/dist/{chunk-Z52HTUW7.mjs → chunk-XNROSH2T.mjs} +2 -2
- package/dist/chunk-XUNHRCOG.mjs +6 -0
- package/dist/{chunk-6U2SLSDN.mjs → chunk-Y6RLRPG3.mjs} +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/server.mjs +1 -1
- package/dist/worker.mjs +1 -1
- package/kit/math.js +18 -2
- package/kit/report.css +248 -137
- package/kit/runtime.js +126 -51
- package/kit/tables.js +51 -1
- package/kit/ui.js +97 -0
- package/package.json +1 -1
- package/release.json +8 -8
- package/sdk/doc-kit.d.ts +9 -3
- package/skill/SKILL.md +22 -10
- package/skill/references/building-components.md +46 -6
- package/skill/references/example-report.html +1 -1
- package/skill/references/writing-html.md +4 -4
- package/skill/sdk/doc-kit.d.ts +9 -3
- package/web/assets/index-Bh74DfsV.css +1 -0
- package/web/assets/index-UW0RyFFt.js +13 -0
- package/web/index.html +2 -2
- package/browser/editor/components/callout/page.mjs +0 -13
- package/dist/chunk-2PBALTR3.mjs +0 -57
- package/dist/chunk-3LA3EMDE.mjs +0 -15
- package/dist/chunk-5FRMCDPY.mjs +0 -16
- package/dist/chunk-5LEK2CBS.mjs +0 -2
- package/dist/chunk-5OFN5AHC.mjs +0 -2
- package/dist/chunk-5SIYS5HN.mjs +0 -2
- package/dist/chunk-5TEK3RAD.mjs +0 -2
- package/dist/chunk-7GHT2UCA.mjs +0 -15
- package/dist/chunk-ATCJYAR4.mjs +0 -2
- package/dist/chunk-C7HWOBIF.mjs +0 -2
- package/dist/chunk-E7VYDG2X.mjs +0 -2
- package/dist/chunk-FQGQH4KM.mjs +0 -2
- package/dist/chunk-GPF4YWWB.mjs +0 -9
- package/dist/chunk-GYQV7Y4J.mjs +0 -120
- package/dist/chunk-HQQMJCQJ.mjs +0 -10
- package/dist/chunk-IERUOGC3.mjs +0 -6
- package/dist/chunk-ITNLYXXV.mjs +0 -3
- package/dist/chunk-NFIBI4IP.mjs +0 -26
- package/dist/chunk-O52JMOCZ.mjs +0 -2
- package/dist/chunk-QMQUXTU4.mjs +0 -12
- package/dist/chunk-QVWVFC5K.mjs +0 -6
- package/dist/chunk-QZY6Q64C.mjs +0 -2
- package/dist/chunk-TDQP3DKQ.mjs +0 -10
- package/dist/chunk-V4CPWKMZ.mjs +0 -2
- package/dist/chunk-VACRB2NE.mjs +0 -3
- package/dist/chunk-W63L7367.mjs +0 -7
- package/dist/chunk-W7CXPCRV.mjs +0 -6
- package/dist/chunk-XINNG4F2.mjs +0 -4
- package/dist/chunk-YL2DPWVS.mjs +0 -4
- package/web/assets/index-B6Ai3wsa.js +0 -13
- package/web/assets/index-SYPC3EZy.css +0 -1
|
@@ -4,13 +4,13 @@ import { begin, done, made } from "../made.mjs";
|
|
|
4
4
|
import { selectsBeside } from "../beside.mjs";
|
|
5
5
|
import { caretAt, nearestOnLine, placeCaret, selectAt } from "../caret.mjs";
|
|
6
6
|
import { sectionOps } from "../page-objects.mjs";
|
|
7
|
-
import { endOf, hoverAt, innerBox, measure } from "../geometry.mjs";
|
|
7
|
+
import { contentOf, endOf, hoverAt, innerBox, measure } from "../geometry.mjs";
|
|
8
8
|
import { BOXED, HOLDS_ONE, accepts } from "../../components/registry.mjs";
|
|
9
9
|
import { DIALOG } from "../../components/kit.mjs";
|
|
10
10
|
|
|
11
11
|
// Where new blocks come from (docs/editor.md, "New blocks and conversions" and "Selecting
|
|
12
|
-
// blocks"): a click on the empty space below the last block of a box (
|
|
13
|
-
// caret into the empty one already at its end), Enter at the very start of a block (a paragraph
|
|
12
|
+
// blocks"): a click on the empty space below the last block of a box (not one whose end is its
|
|
13
|
+
// padding; a new paragraph there, or the caret into the empty one already at its end), Enter at the very start of a block (a paragraph
|
|
14
14
|
// before it), the handle's "+" (an empty block beside the block: addNear), and the block menu's new section after a block (components/section/page.mjs). A
|
|
15
15
|
// paragraph made here is written once it has text. "在下方新建一块" in a block's menu, like the "/"
|
|
16
16
|
// menu's blocks in a block with text, is the editor's insertBelow; blocks also come from conversions
|
|
@@ -49,16 +49,20 @@ export function createInsert({ struct, blocks }) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// A click on empty space (selection/marquee.mjs), below the last block of the box clicked (the
|
|
52
|
-
// page, a Sheet panel, a section, a
|
|
53
|
-
// page): a new paragraph at its end, or the caret into the empty paragraph already there
|
|
54
|
-
// (geometry.mjs endOf).
|
|
55
|
-
//
|
|
56
|
-
//
|
|
52
|
+
// page, a Sheet panel, a section, a column, a detail card's content, a flowchart's node; below the
|
|
53
|
+
// report, the page): a new paragraph at its end, or the caret into the empty paragraph already there
|
|
54
|
+
// (geometry.mjs endOf). Under the last block of a box whose end is its own padding (endIsPadding: a
|
|
55
|
+
// highlight block, a callout, a disclosure's content; the user's, 2026-11-06, as notion-lite's click-blank.js
|
|
56
|
+
// makes a block only below the page and in a column) it is a gap like any other, below. Right of
|
|
57
|
+
// the text area a boxed block (a code block, a callout, a file…: not text) or a row of columns
|
|
58
|
+
// stands in, at its height (boxedBeside), that block selected, so Delete takes it and Enter puts a
|
|
59
|
+
// paragraph after it; between a narrower block's right edge and its column's, as beside any block
|
|
60
|
+
// of no text (below). Elsewhere beside text, the caret goes into the text block on that line (the nearest of those
|
|
57
61
|
// side by side), where the line is nearest the click (Notion's way); in the gap between two
|
|
58
62
|
// blocks, or beside a block of no text, into the nearer of the blocks around it that is text, in the
|
|
59
63
|
// innermost box at that height (boxBeside: the press may be on the page's margin, the block in a
|
|
60
64
|
// Sheet's panel or a section), else in the boxes around that one, at the place nearest the click on
|
|
61
|
-
// its nearest line (
|
|
65
|
+
// its nearest line (somewhere in the box clicked or around it, when any is text). The page stays where it
|
|
62
66
|
// is, that text out of sight too (caretNear; notion-lite's click-blank.js scrolls no page either).
|
|
63
67
|
function clickBlank(x, y, target) {
|
|
64
68
|
// In an open detail card, its content is the page: never a block behind it (marquee.mjs topOf).
|
|
@@ -77,7 +81,7 @@ export function createInsert({ struct, blocks }) {
|
|
|
77
81
|
if (end?.focus) return caretAt(end.focus, "end");
|
|
78
82
|
if (end) return newParagraph(end.box, end.after);
|
|
79
83
|
const top = y + scrollY;
|
|
80
|
-
const beside = boxedBeside(main, x, y);
|
|
84
|
+
const beside = boxedBeside(main, x, y, target);
|
|
81
85
|
if (beside) return blocks().select(beside);
|
|
82
86
|
const host = hoverAt(measure(main, { top, bottom: top }), x + scrollX, top, { reach: Infinity, skip: (unit) => !unit.text });
|
|
83
87
|
if (host) return caretNear(host, x, y);
|
|
@@ -144,8 +148,8 @@ export function createInsert({ struct, blocks }) {
|
|
|
144
148
|
return Math.max(rect.top - y, y - rect.bottom, 0);
|
|
145
149
|
};
|
|
146
150
|
// A block that is no text of its own (its category: a card, a container of its own or a whole
|
|
147
|
-
// block
|
|
148
|
-
// boxes text flows in (a list, a quote, a section, columns).
|
|
151
|
+
// block, a highlight block, a callout; a code block, a formula block, a divider), as opposed to text
|
|
152
|
+
// blocks and the boxes text flows in (a list, a quote, a section, columns).
|
|
149
153
|
const boxed = (unit) => BOXED.has(unit.getAttribute("data-ge-unit-kind"));
|
|
150
154
|
// The text block of `unit` at its bottom (`last`) or its top: itself, or the last (first) of those it holds; none in a boxed block.
|
|
151
155
|
function textEdge(unit, last) {
|
|
@@ -153,14 +157,23 @@ export function createInsert({ struct, blocks }) {
|
|
|
153
157
|
const texts = [unit, ...unit.querySelectorAll("[data-ge-block], [data-ge-new]")].filter((node) => node.isContentEditable && node.getClientRects().length > 0 && !node.closest("details:not([open]) > :not(summary)"));
|
|
154
158
|
return (last ? texts.at(-1) : texts[0]) ?? null;
|
|
155
159
|
}
|
|
156
|
-
// A boxed block or a row of columns at height `y` with (x, y)
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
+
// A boxed block or a row of columns at height `y` with (x, y) right of the text area it stands in
|
|
161
|
+
// (the content box of its box: the page's column, a column of 分栏, a section's, a quote's, a
|
|
162
|
+
// highlight block's or a callout's, a disclosure's content) and right of the block itself (one
|
|
163
|
+
// wider than that area), or null: in the page's margin, or in the padding of the box it stands in;
|
|
164
|
+
// never between a narrower block's right edge and its area's (a table, a picture: the user's,
|
|
165
|
+
// 2026-11-07, a click there selected it), nor in another text area (the blank box `target` the
|
|
166
|
+
// press was on holds the block: a column further right is no place beside one in the column left
|
|
167
|
+
// of it). The innermost, but never a block inside a row of columns the click is to the right of too
|
|
168
|
+
// (the row is selected whole): past a column's edge is the gap to the next column, whose press is
|
|
169
|
+
// the gap's (columns/: it drags, a click alone changes nothing), or past the row, which is selected.
|
|
170
|
+
function boxedBeside(main, x, y, target) {
|
|
160
171
|
let found = null;
|
|
161
172
|
const right = (unit) => {
|
|
162
173
|
const rect = unit.getBoundingClientRect();
|
|
163
|
-
|
|
174
|
+
if (y < rect.top || y > rect.bottom || (target?.contains && !target.contains(unit))) return false;
|
|
175
|
+
const box = unit.parentElement;
|
|
176
|
+
return x > Math.max(rect.right, contentOf(box.getBoundingClientRect(), getComputedStyle(box)).right);
|
|
164
177
|
};
|
|
165
178
|
for (const unit of main.querySelectorAll(`[data-ge-unit-kind]`)) {
|
|
166
179
|
if (!selectsBeside(unit) || !unit.parentElement?.hasAttribute("data-ge-box") || unit.parentElement.closest("details:not([open])")) continue;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { holdsLongRun } from "../../shared/long-runs.mjs";
|
|
2
|
+
import { textFilters } from "../components/registry.mjs";
|
|
3
|
+
import { addStyle } from "./ui.mjs";
|
|
4
|
+
|
|
5
|
+
// A long run in the text the page editor edits (src/shared/long-runs.mjs: a path, a URL, a long
|
|
6
|
+
// identifier, more than 20 characters with no place to break inside it) breaks where its line ends,
|
|
7
|
+
// as the kit breaks it in the page a reader sees (runtime/spacing.js, a span that breaks anywhere,
|
|
8
|
+
// .doc-long-run). No span goes into text the editor keeps equal to the file, so a text block whose
|
|
9
|
+
// own text holds such a run breaks anywhere as a whole (data-ge-long-run: word-break: break-all),
|
|
10
|
+
// and an ordinary word in that block may then be parted at a line's end too, which no other block's
|
|
11
|
+
// is (the compromise docs/editor.md names). The rule's weight is none: a block's own (a table's
|
|
12
|
+
// header keep-all, a short field normal, a code block's lines) wins; what the block holds that is a
|
|
13
|
+
// block or an atom of its own ([contenteditable="false"]: a nested list, a formula) breaks as it would
|
|
14
|
+
// without it, and an item of that list is judged by its own text. Never a block of plain text (a code
|
|
15
|
+
// block's lines, a tab's name: textFilters false), which keeps its own rule; a text part of an
|
|
16
|
+
// instance of a component package is judged as any text block (its component's own word-break, where
|
|
17
|
+
// it says one, wins), though the kit holds no run in an instance's own region.
|
|
18
|
+
//
|
|
19
|
+
// Judged as editing starts, for every text block, and again for the blocks whose text changed, as it
|
|
20
|
+
// changes, whatever changed it (typing, a paste, undo, a rebuild from the file, a block made or made
|
|
21
|
+
// editable): the page's mutations are watched (childList, characterData, contenteditable), and only
|
|
22
|
+
// the blocks they stand in are judged, each by its own text (a regular expression over a block's
|
|
23
|
+
// words: no scan of the page per keystroke). The marker is no text of the file's: a data-ge-* attribute
|
|
24
|
+
// on the block itself (core.mjs), set or taken off only where it changes.
|
|
25
|
+
|
|
26
|
+
const MARK = "data-ge-long-run";
|
|
27
|
+
const HOST = '[contenteditable="true"]';
|
|
28
|
+
|
|
29
|
+
const STYLE = `
|
|
30
|
+
/* A text block holding a long run breaks anywhere (long-runs.mjs), of no weight: a block's own rule wins. */
|
|
31
|
+
:where([data-ge-editing] [data-ge-long-run]) { word-break: break-all; }
|
|
32
|
+
/* What it holds of its own (a nested list, a formula) breaks as the page's text does. */
|
|
33
|
+
:where([data-ge-editing] [data-ge-long-run] [contenteditable="false"]) { word-break: normal; }
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
// The text of `host` its own: not a block or an atom it holds (contenteditable="false"), a line break a space.
|
|
37
|
+
function ownText(host) {
|
|
38
|
+
const walker = host.ownerDocument.createTreeWalker(host, 5, { // NodeFilter.SHOW_ELEMENT | SHOW_TEXT
|
|
39
|
+
acceptNode: (node) => (node.nodeType === 1 && node !== host && node.hasAttribute("contenteditable") ? 2 : 1), // FILTER_REJECT, FILTER_ACCEPT
|
|
40
|
+
});
|
|
41
|
+
let text = "";
|
|
42
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
43
|
+
if (node.nodeType === 3) text += node.data;
|
|
44
|
+
else if (node.localName === "br") text += " ";
|
|
45
|
+
}
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Marks `element` as a text block holding a long run, or takes the mark off: whether it holds one. */
|
|
50
|
+
export function judgeLongRuns(element) {
|
|
51
|
+
const long = element.isConnected && element.matches(HOST) && textFilters(element) && holdsLongRun(ownText(element));
|
|
52
|
+
if (long !== element.hasAttribute(MARK)) element.toggleAttribute(MARK, long);
|
|
53
|
+
return long;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Judges every text block under `root` now, and each again as its text changes. */
|
|
57
|
+
export function followLongRuns(root) {
|
|
58
|
+
addStyle(STYLE);
|
|
59
|
+
for (const host of root.querySelectorAll(`${HOST}, [${MARK}]`)) judgeLongRuns(host);
|
|
60
|
+
new MutationObserver((records) => {
|
|
61
|
+
const judged = new Set();
|
|
62
|
+
for (const record of records) {
|
|
63
|
+
if (record.type === "attributes") judged.add(record.target); // made editable, or no longer
|
|
64
|
+
const at = record.target.nodeType === 1 ? record.target : record.target.parentElement;
|
|
65
|
+
const host = at?.closest(HOST);
|
|
66
|
+
if (host) judged.add(host);
|
|
67
|
+
for (const node of record.addedNodes) {
|
|
68
|
+
if (node.nodeType !== 1) continue;
|
|
69
|
+
if (node.matches(HOST)) judged.add(node);
|
|
70
|
+
for (const inner of node.querySelectorAll(HOST)) judged.add(inner);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
judged.forEach(judgeLongRuns);
|
|
74
|
+
}).observe(root, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ["contenteditable"] });
|
|
75
|
+
}
|
|
@@ -11,15 +11,17 @@
|
|
|
11
11
|
//
|
|
12
12
|
// Two shadows, because Notion has two surfaces: --ge-shadow, a card standing over the text (the
|
|
13
13
|
// format bar, the formula and link cards, the conflicts, a notice), and --ge-shadow-pop, a menu
|
|
14
|
-
// (Notion's large outer shadow). The
|
|
15
|
-
//
|
|
14
|
+
// (Notion's large outer shadow). The icon picker reads this palette too (emoji-picker/styles.mjs);
|
|
15
|
+
// emoji-picker/support.mjs keeps notion-lite's --c-* variables only for the media menu and the image's
|
|
16
|
+
// alignment bar.
|
|
16
17
|
|
|
17
18
|
export const TOKENS = `
|
|
18
19
|
:root {
|
|
19
20
|
--ge-tx: light-dark(#2c2c2b, #f0efed);
|
|
20
21
|
--ge-tx2: light-dark(#7d7a75, #ada9a3);
|
|
21
22
|
--ge-tx3: light-dark(#a19e99, #7d7a75);
|
|
22
|
-
--
|
|
23
|
+
/* The workbench's --icoPri (FinHarness-Demo's --ink-2). */
|
|
24
|
+
--ge-ico: light-dark(#5c5a55, #a9a69e);
|
|
23
25
|
--ge-ico2: light-dark(#8e8b86, #ada9a3);
|
|
24
26
|
/* The quietest icon gray (notion-lite's --c-icoTer): the block handle's dots, its bar and its +. */
|
|
25
27
|
--ge-ico3: light-dark(#ada9a3, #7d7a75);
|
|
@@ -61,5 +63,15 @@ export const TOKENS = `
|
|
|
61
63
|
--ge-note-hot-line: light-dark(rgb(222 160 0 / 90%), rgb(255 204 64 / 80%));
|
|
62
64
|
/* A card of 批注 in the margin under the pointer (notion-lite's --c-hovMarDisBac): a shade of the page. */
|
|
63
65
|
--ge-note-shade: light-dark(#f9f9f8, #202020);
|
|
66
|
+
/* The Agent's changes (changes/, 设置 › 显示变更): Notion's green and red backgrounds, the conflict card's two versions (conflicts.mjs);
|
|
67
|
+
words added get a 批注's look (a faint fill under a 2px underline) in green, blocks a fainter fill; words
|
|
68
|
+
taken out stand in red struck through on the red, deleted blocks' ghost on a fainter red. */
|
|
69
|
+
--ge-diff-add: light-dark(rgb(0 100 45 / 9%), rgb(108 255 172 / 15.7%));
|
|
70
|
+
--ge-diff-add-line: light-dark(rgb(68 131 97 / 60%), rgb(111 197 150 / 55%));
|
|
71
|
+
--ge-diff-add-fill: light-dark(rgb(0 100 45 / 5.5%), rgb(108 255 172 / 9%));
|
|
72
|
+
--ge-diff-add-text: light-dark(#448361, #6fc596);
|
|
73
|
+
--ge-diff-del: light-dark(rgb(223 22 0 / 9.4%), rgb(255 104 92 / 24%));
|
|
74
|
+
--ge-diff-del-fill: light-dark(rgb(223 22 0 / 4.5%), rgb(255 104 92 / 10%));
|
|
75
|
+
--ge-diff-del-text: light-dark(#cf5148, #ff8f85);
|
|
64
76
|
}
|
|
65
77
|
`;
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { OLD_OUTLINE_OPEN_KEY, OLD_PREFERENCES_KEY, OUTLINE_OPEN_KEY, PREFERENCES_KEY } from "../../shared/preferences.mjs";
|
|
2
|
+
import { LOOK_ATTRIBUTES } from "../../shared/document-look.mjs";
|
|
2
3
|
|
|
3
4
|
// The right side of the page while it is edited (docs/editor.md, "The outline and the right side"): the
|
|
4
5
|
// outline (目录, outline/) docked at the window's right edge, the cards of 批注 (annotate/) between it and
|
|
5
6
|
// the column, and where the body column goes beside them. The outline is a panel at the window's right
|
|
6
7
|
// edge, in full (its list, 200px unless the reader dragged its guide wider or narrower: `outlineWidth`;
|
|
7
|
-
// the
|
|
8
|
-
//
|
|
8
|
+
// the list 8px in, its guide a little further in, close to its top level's titles: outline/list.mjs
|
|
9
|
+
// LIST) or as its strip (36px: a short line a heading, its card shown while the pointer is
|
|
10
|
+
// on it), as the room allows; or as its strip wherever the reader collapsed it (a click on its guide:
|
|
11
|
+
// `collapsed`, the preference outlineCollapsed, until a click on the guide in the strip's card takes it
|
|
12
|
+
// back, which gives the list only where the room allows it).
|
|
9
13
|
//
|
|
10
14
|
// One rule places the column: in the middle of what the outline leaves of the page, as far from the
|
|
11
|
-
// window's left edge as from the outline (its
|
|
15
|
+
// window's left edge as from the outline (its list's edge, or its strip). With the outline the rooms at the
|
|
12
16
|
// column's two sides are the same, and they stay the same as the guide is dragged (each gives or takes
|
|
13
17
|
// half of what the list takes or gives) and as the window is resized; without it the column is
|
|
14
18
|
// centered in the page as ever. Each room is the edge room at least (64: the block handle at the left,
|
|
@@ -53,11 +57,13 @@ import { OLD_OUTLINE_OPEN_KEY, OLD_PREFERENCES_KEY, OUTLINE_OPEN_KEY, PREFERENCE
|
|
|
53
57
|
* The numbers: the edge room (the least room either side of the column keeps), the least it gives up
|
|
54
58
|
* to (the block handle's: its 38px, its 12px gap, 2px), the column, the least a full-width column
|
|
55
59
|
* keeps, the outline in full (its default width, the least and the most it is dragged to), its
|
|
56
|
-
*
|
|
57
|
-
*
|
|
60
|
+
* list's inset in its panel (`guide`: the column's right room ends there, at the list's edge, its
|
|
61
|
+
* guide's hairline LIST.guide further in, outline/list.mjs; the guide stood there
|
|
62
|
+
* until 2026-11-07, the numbers kept so the forms switch at the same widths), how far left of the guide's
|
|
63
|
+
* hairline its grip reaches (and how far short of the list what stands in the column's right room stays), its strip, the hysteresis; how far right of the column a card of 批注 stands (the button beside
|
|
58
64
|
* a block, past a section's strip, ends 52px out, and a card under the pointer comes 5px left: 3px
|
|
59
65
|
* clear; one focused comes 20px left, and no button shows then), a card's width, the least it gives up
|
|
60
|
-
* to, and the room past it (clear of the outline's guide and grip).
|
|
66
|
+
* to, and the room past it (clear of the outline's list: its guide and grip).
|
|
61
67
|
*/
|
|
62
68
|
export const MARGIN = Object.freeze({ edge: 64, start: 52, column: 712, least: 560, full: 200, fullMin: 160, fullMax: 360, guide: 8, grip: 4, strip: 36, hysteresis: 16, lane: 60, card: 308, cardLeast: 240, cardEnd: 20 });
|
|
63
69
|
|
|
@@ -71,19 +77,20 @@ export function outlineWidth(chosen, { full = 200, fullMin = 160, fullMax = 360
|
|
|
71
77
|
|
|
72
78
|
/**
|
|
73
79
|
* Where the right side's parts go on a page `width` px wide (its window less its scrollbar): `outline`,
|
|
74
|
-
* whether the outline is on, `
|
|
75
|
-
*
|
|
76
|
-
* (a form it laid out is kept for
|
|
80
|
+
* whether the outline is on, `collapsed`, whether the reader collapsed it to its strip, `notes`,
|
|
81
|
+
* whether the page has 批注, `full`, its width in full (outlineWidth), `wide`, a report read full
|
|
82
|
+
* width, `was`, the layout given last for the same request (a form it laid out is kept for
|
|
83
|
+
* `hysteresis` px under the width it needs).
|
|
77
84
|
* Returns { dock: the outline's width at the window's right edge (0, its strip's or its list's),
|
|
78
85
|
* outline: its form, "full" | "strip" | null, room: what the outline takes of the page's width beside
|
|
79
|
-
* the column's rooms (the list's less its
|
|
86
|
+
* the column's rooms (the list's less its inset, the strip's, 0), side: the least room either
|
|
80
87
|
* side of the column keeps, left: the room at the column's left on this page, right: the room right of
|
|
81
88
|
* it to the window's edge (its own and the outline's), column: its width, cards: the width of a card
|
|
82
89
|
* of 批注 standing right of it (0: they fold into markers), cardRoom: what the page's rule keeps right of
|
|
83
90
|
* the column for them (0: none), floor: the least room at its left }. Written with no outside name, so
|
|
84
91
|
* the page's head can carry it as it is (EARLY_MARGIN).
|
|
85
92
|
*/
|
|
86
|
-
export function marginLayout({ width, outline = false, notes = false, wide = false, was = null, edge = 64, start = 52, column = 712, least = 560, full = 200, guide = 8, strip = 36, hysteresis = 16, lane = 60, card = 308, cardLeast = 240, cardEnd = 20 }) {
|
|
93
|
+
export function marginLayout({ width, outline = false, collapsed = false, notes = false, wide = false, was = null, edge = 64, start = 52, column = 712, least = 560, full = 200, guide = 8, strip = 36, hysteresis = 16, lane = 60, card = 308, cardLeast = 240, cardEnd = 20 }) {
|
|
87
94
|
const body = wide ? least : column;
|
|
88
95
|
// The column in the middle of what the outline leaves, its rooms `side` at least (narrower only below
|
|
89
96
|
// that), and left of there as far as a card's room asks, down to `start`, the card giving what is short.
|
|
@@ -99,10 +106,11 @@ export function marginLayout({ width, outline = false, notes = false, wide = fal
|
|
|
99
106
|
if (!outline) return placed(null, 0, 0, edge);
|
|
100
107
|
// In full while the list, as wide as the reader gave it, leaves the column the handle's room at both
|
|
101
108
|
// sides (its rooms the edge room, less down to the handle's where the page is short); a form kept
|
|
102
|
-
// under the width it needs (the hysteresis) narrows the column by what is short. Else
|
|
103
|
-
|
|
109
|
+
// under the width it needs (the hysteresis) narrows the column by what is short. Else, or collapsed by
|
|
110
|
+
// the reader, its strip.
|
|
111
|
+
const spare = width + guide - body; // the page beside the column, the list's room counted from its edge (its inset `guide`)
|
|
104
112
|
const kept = was?.outline === "full" ? hysteresis : 0;
|
|
105
|
-
if (spare + kept >= 2 * start + full) return placed("full", full, full - guide, Math.max(start, Math.min(edge, (spare - full) / 2)));
|
|
113
|
+
if (!collapsed && spare + kept >= 2 * start + full) return placed("full", full, full - guide, Math.max(start, Math.min(edge, (spare - full) / 2)));
|
|
106
114
|
return placed("strip", strip, strip, edge);
|
|
107
115
|
}
|
|
108
116
|
|
|
@@ -143,8 +151,9 @@ const roomCss = ({ room, side, cardRoom = 0, floor = side }) => `:root[data-ge-m
|
|
|
143
151
|
|
|
144
152
|
// The head's script (EARLY_MARGIN): the outline's room before the page shows, when the report has it
|
|
145
153
|
// on (`keys.open`: the reports that have it on, shared/preferences.mjs outlineOpen; `slug`: this page's), as
|
|
146
|
-
// wide as the reader's preferences say (`keys.preferences`: outlineWidth,
|
|
147
|
-
// and the cards' room, when it has 批注 (`notes`, the
|
|
154
|
+
// wide as the reader's preferences say and in the form they chose (`keys.preferences`: outlineWidth,
|
|
155
|
+
// outlineCollapsed, the same for every report), and the cards' room, when it has 批注 (`notes`, the
|
|
156
|
+
// edit service's word).
|
|
148
157
|
// Never on a page opened on its own in a frame (?solo, the 写作偏好 dialog), which has none.
|
|
149
158
|
// In the head the page has no content yet, so no scrollbar: its width is read again at
|
|
150
159
|
// DOMContentLoaded, when the content is in and the kit has not shown it yet (a frame later), unless the
|
|
@@ -157,13 +166,17 @@ function earlyMargin(layout, widthOf, numbers, keys, slug, notes, room, early, c
|
|
|
157
166
|
const outline = Array.isArray(open) && open.includes(slug);
|
|
158
167
|
if (!outline && !notes) return;
|
|
159
168
|
const prefs = JSON.parse(read(keys.preferences, keys.oldPreferences) || "{}") || {};
|
|
160
|
-
const asked = { outline, notes, outlineWidth: Number(prefs.outlineWidth) || 0 };
|
|
169
|
+
const asked = { outline, notes, outlineWidth: Number(prefs.outlineWidth) || 0, collapsed: prefs.outlineCollapsed === true };
|
|
161
170
|
const root = document.documentElement;
|
|
162
171
|
const sheet = document.createElement("style");
|
|
163
172
|
sheet.setAttribute("data-doc-dev", "");
|
|
164
173
|
sheet.setAttribute(room, "");
|
|
165
174
|
const lay = () => {
|
|
166
|
-
|
|
175
|
+
// The column the kit's stylesheet gives (a document's look, its 正文宽度 alone, makes it another
|
|
176
|
+
// than 712px: src/shared/document-look.mjs), read as the editor reads it (numbers() below); the kit's
|
|
177
|
+
// stylesheet stands before this script in the head, so it is in.
|
|
178
|
+
const column = parseFloat(getComputedStyle(root).getPropertyValue("--doc-content-max-width")) || numbers.column;
|
|
179
|
+
const given = layout({ ...numbers, column, width: root.clientWidth, outline, collapsed: asked.collapsed, notes, full: widthOf(asked.outlineWidth, numbers), wide: root.getAttribute("data-doc-width") === "full" });
|
|
167
180
|
sheet.textContent = css(given);
|
|
168
181
|
sheet.setAttribute(early, JSON.stringify({ given, asked }));
|
|
169
182
|
};
|
|
@@ -186,7 +199,7 @@ export const earlyMarginScript = (slug, { notes = false } = {}) => {
|
|
|
186
199
|
|
|
187
200
|
// ---------- on the page ----------
|
|
188
201
|
|
|
189
|
-
const asked = { outline: false, notes: false, outlineWidth: 0 }; // what the outline and 批注 ask for
|
|
202
|
+
const asked = { outline: false, notes: false, outlineWidth: 0, collapsed: false }; // what the outline and 批注 ask for
|
|
190
203
|
const listeners = new Set();
|
|
191
204
|
let now = null; // the layout given last
|
|
192
205
|
let request = null; // what it was given for: { outline, wide } (the outline's width keeps the form laid out)
|
|
@@ -311,14 +324,16 @@ function start() {
|
|
|
311
324
|
addEventListener("resize", resized);
|
|
312
325
|
// The page's width changes without the window's too: its scrollbar comes or goes.
|
|
313
326
|
new ResizeObserver(resized).observe(document.documentElement);
|
|
314
|
-
// A report read full width, or no longer
|
|
315
|
-
|
|
327
|
+
// A report read full width, or no longer; the document's look changed (its column's width with its
|
|
328
|
+
// 正文宽度, never its 字体大小: the kit's ui.js puts it on the root, src/shared/document-look.mjs).
|
|
329
|
+
new MutationObserver(() => apply()).observe(document.documentElement, { attributes: true, attributeFilter: ["data-doc-width", ...Object.values(LOOK_ATTRIBUTES)] });
|
|
316
330
|
}
|
|
317
331
|
|
|
318
332
|
/**
|
|
319
333
|
* Ask for room: { outline } (whether it shows), { outlineWidth } (its width in full as the reader
|
|
320
|
-
* chose it, px; 0: the default), {
|
|
321
|
-
* changes it). The layout is given at once and
|
|
334
|
+
* chose it, px; 0: the default), { collapsed } (the reader collapsed it to its strip), { notes }
|
|
335
|
+
* (whether the page has 批注: the column slides when that changes it). The layout is given at once and
|
|
336
|
+
* again whenever the page's width changes it.
|
|
322
337
|
*/
|
|
323
338
|
export function setMargin(part) {
|
|
324
339
|
start();
|
|
@@ -331,7 +346,13 @@ export function setMargin(part) {
|
|
|
331
346
|
/** The layout given last ({ dock, outline, room, side, left, right, column, cards, cardRoom, floor }; see marginLayout). */
|
|
332
347
|
export const margin = () => now ?? layoutFor(isWide(), null);
|
|
333
348
|
|
|
334
|
-
/**
|
|
349
|
+
/**
|
|
350
|
+
* Whether the page's room gives the outline its list in full now, the reader's collapse aside: what a
|
|
351
|
+
* click on the guide in the strip's card would bring (the whole width needed, as for any strip going up).
|
|
352
|
+
*/
|
|
353
|
+
export const listFits = () => marginLayout({ ...MARGIN, width: pageWidth(), ...numbers(), ...asked, outline: true, collapsed: false, full: outlineWidth(asked.outlineWidth, MARGIN), wide: isWide(), was: null }).outline === "full";
|
|
354
|
+
|
|
355
|
+
/** How far right of the column what stands in its right room may reach, by `layout` (marginLayout): MARGIN.grip short of the outline's list (its guide and grip further in), or of its strip, or of the window's edge. */
|
|
335
356
|
export const rightRoom = ({ right, room } = margin()) => right - room - MARGIN.grip;
|
|
336
357
|
|
|
337
358
|
/** Call `listener(layout)` whenever the layout changes (the outline's form or width, the column's least rooms, the cards' width). */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Z } from "../../shared/layers.mjs";
|
|
2
2
|
import { addStyle, element, mount } from "./ui.mjs";
|
|
3
3
|
import { hideTooltip, tooltip } from "./tooltip.mjs";
|
|
4
|
-
import { svg } from "../components/icons.mjs";
|
|
4
|
+
import { SECTION_ICON, svg } from "../components/icons.mjs";
|
|
5
5
|
import { t } from "../../shared/i18n/index.mjs";
|
|
6
6
|
|
|
7
7
|
// The editor's menu card, used by the "/" menu (slash-menu/: block types, filtered as the reader
|
|
@@ -12,16 +12,31 @@ import { t } from "../../shared/i18n/index.mjs";
|
|
|
12
12
|
// the page with a block selected). A press on a row never moves the caret. Colors follow the
|
|
13
13
|
// report's light or dark theme (light-dark()). A row may open a submenu (Notion's 颜色 ›): a second
|
|
14
14
|
// card right of it, opened by resting on the row, a click, Enter or →, and closed by ←, by resting
|
|
15
|
-
// on another row, or with the menu; while it is open from the keys they move in it.
|
|
15
|
+
// on another row, or with the menu; while it is open from the keys they move in it. A row that both
|
|
16
|
+
// runs and opens a submenu (a split row: a table's 一键优化 ›) is two parts side by side, each with
|
|
17
|
+
// its own grey under the pointer: its words, which a click or Enter runs, and its › at its right,
|
|
18
|
+
// which opens the submenu (resting on it, a click, or →); resting on its words opens nothing.
|
|
19
|
+
//
|
|
20
|
+
// A card is as wide as its rows (Notion's menus: 180px at least), up to FIT.most, past which a row's
|
|
21
|
+
// words are cut by an ellipsis, so a short submenu (一键优化 ›: 自动调整列宽, 识别数字列; 对齐 ›) is no
|
|
22
|
+
// wider than its words; it once took 265px whatever it held. A card whose rows change as it is used
|
|
23
|
+
// keeps the one width its caller gives (`width`: the block menu's grid, the "/" menu, a code block's
|
|
24
|
+
// languages), or SEARCHING when it has a search field and gives none, so it never jumps as rows come
|
|
25
|
+
// and go.
|
|
26
|
+
|
|
27
|
+
const FIT = { least: 180, most: 320 }; // a card with no width given: as wide as its rows, between these
|
|
28
|
+
const SEARCHING = 240; // a card with a search field and no width given: its rows filtered as typed
|
|
16
29
|
|
|
17
30
|
const STYLE = `
|
|
18
31
|
.ge-pop {
|
|
19
|
-
position: fixed; left: 0; top: 0; z-index: ${Z.menu}; width:
|
|
32
|
+
position: fixed; left: 0; top: 0; z-index: ${Z.menu}; width: max-content; min-width: ${FIT.least}px; max-width: min(${FIT.most}px, calc(100vw - 16px)); max-height: calc(100vh - 24px);
|
|
20
33
|
overflow-x: hidden; overflow-y: auto; padding: 4px 0; border-radius: 10px; background: var(--ge-bg); color: var(--ge-tx);
|
|
21
34
|
box-shadow: var(--ge-shadow-pop);
|
|
22
35
|
font: 14px/1.2 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI Variable Display", "Segoe UI", Helvetica, "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
|
|
23
36
|
user-select: none; scrollbar-width: thin; scrollbar-color: var(--ge-line-strong) transparent; animation: ge-pop-in .12s ease-out;
|
|
24
37
|
}
|
|
38
|
+
/* A width given (data-sized: opts.width, placed by place()): that width, whatever the rows hold. */
|
|
39
|
+
.ge-pop[data-sized] { min-width: 0; max-width: calc(100vw - 16px); }
|
|
25
40
|
@keyframes ge-pop-in { from { opacity: 0; transform: scale(.98); } }
|
|
26
41
|
@media (prefers-reduced-motion: reduce) { .ge-pop { animation: none; } }
|
|
27
42
|
.ge-pop-title { display: flex; align-items: center; gap: 8px; padding: 8px 12px; color: var(--ge-tx2); font-size: 12px; font-weight: 500; line-height: 14px; }
|
|
@@ -52,9 +67,21 @@ const STYLE = `
|
|
|
52
67
|
.ge-pop-arrow { color: var(--ge-tx3); }
|
|
53
68
|
.ge-pop-arrow svg { display: block; width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
|
|
54
69
|
.ge-pop-row[aria-expanded="true"] { background: var(--ge-hover); }
|
|
70
|
+
/* A split row (run and submenu both): its words (.ge-pop-main) and its › (.ge-pop-more, 32px wide:
|
|
71
|
+
the › where another submenu row has its own) side by side, 2px apart, each grey on its own: the words
|
|
72
|
+
when the row is highlighted and the pointer is not on the ›, the › under the pointer and while its
|
|
73
|
+
submenu is open. The row itself takes no grey. */
|
|
74
|
+
.ge-pop-row[data-split] { gap: 2px; padding: 0; }
|
|
75
|
+
.ge-pop-row[data-split][data-active], .ge-pop-row[data-split][aria-expanded="true"] { background: transparent; }
|
|
76
|
+
.ge-pop-main { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; align-self: stretch; padding: 0 8px; border-radius: 6px; }
|
|
77
|
+
.ge-pop-more { flex: none; display: flex; align-items: center; justify-content: center; width: 32px; align-self: stretch; border-radius: 6px; }
|
|
78
|
+
.ge-pop-row[data-split][data-active]:not([data-more]) > .ge-pop-main,
|
|
79
|
+
.ge-pop-row[data-split][data-more] > .ge-pop-more,
|
|
80
|
+
.ge-pop-row[data-split][aria-expanded="true"] > .ge-pop-more { background: var(--ge-hover); }
|
|
55
81
|
.ge-pop-check svg { display: block; width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
|
|
56
82
|
.ge-pop-sep { height: 1px; margin: 3px 12px 4px; background: var(--ge-line); }
|
|
57
|
-
|
|
83
|
+
/* A note wraps at the card's width and never widens it (no width of its own: contain). */
|
|
84
|
+
.ge-pop-note { contain: inline-size; padding: 0 12px 6px 40px; color: var(--ge-tx3); font-size: 12px; line-height: 1.5; }
|
|
58
85
|
.ge-pop-empty { padding: 8px 12px; color: var(--ge-tx3); }
|
|
59
86
|
/* What a row makes, shown beside it under the pointer (a component package's preview.svg). */
|
|
60
87
|
.ge-pop-preview { display: block; width: 240px; max-height: 180px; border-radius: 4px; object-fit: contain; background: var(--ge-bg); }
|
|
@@ -63,7 +90,7 @@ const STYLE = `
|
|
|
63
90
|
.ge-pop-search input:focus { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ge-tx3) 60%, transparent); }
|
|
64
91
|
.ge-pop-search input::placeholder { color: var(--ge-tx3); }
|
|
65
92
|
/* Rows in a card that is there already (inside: a link card's pages): under what stands in it, as wide as it, no card of their own. */
|
|
66
|
-
.ge-pop[data-inside] { position: static; flex: 0 0 100%; width: auto; max-width: none; padding: 4px 0 0; border-radius: 0; background: none; box-shadow: none; animation: none; }
|
|
93
|
+
.ge-pop[data-inside] { position: static; flex: 0 0 100%; width: auto; min-width: 0; max-width: none; padding: 4px 0 0; border-radius: 0; background: none; box-shadow: none; animation: none; }
|
|
67
94
|
.ge-pop[data-inside] .ge-pop-row { width: 100%; margin-inline: 0; }
|
|
68
95
|
`;
|
|
69
96
|
|
|
@@ -78,8 +105,7 @@ export const ICONS = {
|
|
|
78
105
|
outer: svg('<rect x="3.25" y="3.25" width="13.5" height="13.5" rx="2.5" stroke-dasharray="2.2 2.2"/><rect x="6.5" y="6.5" width="7" height="7" rx="1.25"/>'),
|
|
79
106
|
section: svg(`${SECTION_LINES}<path d="M14.5 12v5M12 14.5h5"/>`),
|
|
80
107
|
dissolve: svg(`${SECTION_LINES}<path d="m12.75 12.75 3.5 3.5M16.25 12.75l-3.5 3.5"/>`), // section's plus, turned 45°
|
|
81
|
-
//
|
|
82
|
-
group: svg('<path d="M6 3.75h-.75a2 2 0 0 0-2 2v8.5a2 2 0 0 0 2 2H6M14 3.75h.75a2 2 0 0 1 2 2v8.5a2 2 0 0 1-2 2H14"/><path d="M7.25 7.75h5.5M7.25 12.25h5.5"/>'),
|
|
108
|
+
group: SECTION_ICON, // blocks gathered into a new section: their lines between brackets (components/icons.mjs)
|
|
83
109
|
center: svg('<rect x="5.25" y="4.25" width="9.5" height="11.5" rx="1.75"/><path d="M2.75 4v12M17.25 4v12"/>'),
|
|
84
110
|
full: svg('<rect x="3.25" y="4.25" width="13.5" height="11.5" rx="1.75"/><path d="m6.5 10h7M6.5 10l1.75-1.75M6.5 10l1.75 1.75M13.5 10l-1.75-1.75M13.5 10l-1.75 1.75"/>'),
|
|
85
111
|
reset: svg('<path d="M4.75 6.75A6 6 0 1 1 4 10.5"/><path d="M4.5 3.5v3.5H8"/>'),
|
|
@@ -105,11 +131,13 @@ export const menuOpen = () => opened.size > 0;
|
|
|
105
131
|
* it off; preview: { src, says }, a picture of what the row makes, where it has one, and a line on it,
|
|
106
132
|
* shown at the row's right while the pointer rests on it, notion-lite's row tooltip) (or { label, icon?, submenu:
|
|
107
133
|
* () => items, width?, search? } opening a submenu, with a search field at its top when `search` is
|
|
108
|
-
* given, as `opts.search` below, and `search.empty` the words shown when nothing is found
|
|
134
|
+
* given, as `opts.search` below, and `search.empty` the words shown when nothing is found; a row
|
|
135
|
+
* with both `run` and `submenu` is a split row: its words run, its › opens the submenu), titles
|
|
109
136
|
* { title, icon? }, separators { separator: true } and grids { node, cells, columns } (type-grid.mjs: the
|
|
110
137
|
* node shown as it is, its cells, buttons that act on their own click, highlighted as rows are).
|
|
111
|
-
* opts: { label, empty, highlight, onClose, around, side, width } — width: the card's, in px,
|
|
112
|
-
*
|
|
138
|
+
* opts: { label, empty, highlight, onClose, around, side, width } — width: the card's, in px, whatever
|
|
139
|
+
* its rows hold (the block menu: as wide as its grid of types), where without it the card is as wide
|
|
140
|
+
* as its rows (FIT; SEARCHING with a search field). around: the rect of the block a handle
|
|
113
141
|
* (the anchor) stands for; the menu keeps clear of that block: left of it beside the handle, else
|
|
114
142
|
* below or above it (scrolling inside when only part of it fits), and over it only when the block
|
|
115
143
|
* leaves no room at all. side: "right": beside the anchor, right of it (left of it when there is no
|
|
@@ -139,25 +167,32 @@ export function createMenu() {
|
|
|
139
167
|
let subKeys = false; // opened from the keys: they move in it
|
|
140
168
|
let subTimer = 0;
|
|
141
169
|
let pointedRow = null;
|
|
170
|
+
let pointedMore = false; // the pointer on a split row's › (not on its words)
|
|
142
171
|
|
|
143
172
|
// The pointer over a row highlights it. In a menu that starts with nothing highlighted (the block
|
|
144
173
|
// menu), leaving the row takes the highlight off again, as :hover would; a row the keys moved to
|
|
145
|
-
// stays, and so does the "/" menu's, the row Enter runs.
|
|
146
|
-
|
|
174
|
+
// stays, and so does the "/" menu's, the row Enter runs. On a split row, `more`: the pointer is on
|
|
175
|
+
// its › (data-more on the row, which greys the › instead of the words), the part that opens.
|
|
176
|
+
const point = (row, more = false) => {
|
|
147
177
|
highlight(rows.indexOf(row), false);
|
|
148
178
|
pointed = true;
|
|
149
|
-
if (row
|
|
179
|
+
if (row.split) row.toggleAttribute("data-more", more);
|
|
180
|
+
if (row === pointedRow && more === pointedMore) return;
|
|
150
181
|
pointedRow = row;
|
|
182
|
+
pointedMore = more;
|
|
151
183
|
clearTimeout(subTimer);
|
|
152
184
|
// Notion's: a submenu opens 80ms after the pointer rests on its row (200ms while another is
|
|
153
|
-
// open), and goes 250ms after the pointer rests elsewhere.
|
|
154
|
-
|
|
155
|
-
|
|
185
|
+
// open), and goes 250ms after the pointer rests elsewhere (a split row's words being elsewhere).
|
|
186
|
+
const opens = Boolean(row.submenu) && (!row.split || more);
|
|
187
|
+
if (opens && row !== subRow) subTimer = setTimeout(() => openSub(row), subRow ? 200 : 80);
|
|
188
|
+
else if (!opens && subRow) subTimer = setTimeout(closeSub, 250);
|
|
156
189
|
};
|
|
157
190
|
const leave = (row) => {
|
|
158
191
|
if (pointed && rows[active] === row && options.highlight === false) highlight(-1);
|
|
192
|
+
row.removeAttribute("data-more");
|
|
159
193
|
if (pointedRow !== row) return;
|
|
160
194
|
pointedRow = null;
|
|
195
|
+
pointedMore = false;
|
|
161
196
|
clearTimeout(subTimer); // passing over a row opens nothing
|
|
162
197
|
if (subRow === row && !subKeys) subTimer = setTimeout(closeSub, 250); // unless the pointer goes into it
|
|
163
198
|
};
|
|
@@ -180,12 +215,17 @@ export function createMenu() {
|
|
|
180
215
|
children.push(item.node);
|
|
181
216
|
} else {
|
|
182
217
|
const row = element("button", { type: "button", class: "ge-pop-row", role: "menuitem" });
|
|
218
|
+
// A split row's words go in a part of their own, its › in another (CSS above).
|
|
219
|
+
row.split = Boolean(item.run && item.submenu);
|
|
220
|
+
const words = row.split ? element("span", { class: "ge-pop-main" }) : row;
|
|
221
|
+
if (row.split) row.setAttribute("data-split", "");
|
|
183
222
|
if (item.long) row.setAttribute("data-long", "");
|
|
184
|
-
if (options.icons !== false)
|
|
185
|
-
|
|
186
|
-
if (item.hint)
|
|
223
|
+
if (options.icons !== false) words.append(element("span", { class: "ge-pop-icon", html: item.icon ?? "" }));
|
|
224
|
+
words.append(element("span", { class: "ge-pop-label", text: item.label }));
|
|
225
|
+
if (item.hint) words.append(element("span", { class: "ge-pop-hint", text: item.hint }));
|
|
226
|
+
if (row.split) row.append(words, element("span", { class: "ge-pop-more", html: `<span class="ge-pop-arrow">${ICONS.chevron}</span>` }));
|
|
227
|
+
else if (item.submenu) row.append(element("span", { class: "ge-pop-arrow", html: ICONS.chevron }));
|
|
187
228
|
if (item.submenu) {
|
|
188
|
-
row.append(element("span", { class: "ge-pop-arrow", html: ICONS.chevron }));
|
|
189
229
|
row.setAttribute("aria-haspopup", "menu");
|
|
190
230
|
row.submenu = item.submenu;
|
|
191
231
|
row.subWidth = item.width;
|
|
@@ -199,9 +239,10 @@ export function createMenu() {
|
|
|
199
239
|
row.disabled = Boolean(item.disabled || item.current);
|
|
200
240
|
row.run = item.run;
|
|
201
241
|
row.addEventListener("mousedown", (event) => event.preventDefault());
|
|
202
|
-
|
|
242
|
+
const onMore = (event) => row.split && Boolean(event.target.closest?.(".ge-pop-more"));
|
|
243
|
+
row.addEventListener("pointermove", (event) => point(row, onMore(event)));
|
|
203
244
|
row.addEventListener("pointerleave", () => leave(row));
|
|
204
|
-
row.addEventListener("click", () => run(row));
|
|
245
|
+
row.addEventListener("click", (event) => run(row, onMore(event)));
|
|
205
246
|
rows.push(row);
|
|
206
247
|
children.push(row);
|
|
207
248
|
if (item.note) children.push(element("div", { class: "ge-pop-note", text: item.note }));
|
|
@@ -335,9 +376,11 @@ export function createMenu() {
|
|
|
335
376
|
sub?.close();
|
|
336
377
|
}
|
|
337
378
|
|
|
338
|
-
|
|
379
|
+
// A row's work: its submenu opened (a split row's only from its ›: `more`), a grid's cell's own
|
|
380
|
+
// click, or the menu closed and its action run (Enter and a click on a split row's words).
|
|
381
|
+
function run(row, more = false) {
|
|
339
382
|
if (!row || row.disabled) return false;
|
|
340
|
-
if (row.submenu) {
|
|
383
|
+
if (row.submenu && (!row.split || more)) {
|
|
341
384
|
openSub(row, true);
|
|
342
385
|
return true;
|
|
343
386
|
}
|
|
@@ -391,10 +434,11 @@ export function createMenu() {
|
|
|
391
434
|
const menu = {
|
|
392
435
|
open(items, rect, opts = {}) {
|
|
393
436
|
close();
|
|
394
|
-
options = opts;
|
|
437
|
+
options = opts.search && !opts.width ? { ...opts, width: SEARCHING } : opts;
|
|
395
438
|
anchor = rect;
|
|
396
439
|
opened.add(menu);
|
|
397
440
|
node = element("div", { class: "ge-pop", role: "menu", "aria-label": opts.label ?? t("菜单") });
|
|
441
|
+
if (options.width) node.setAttribute("data-sized", "");
|
|
398
442
|
if (opts.inside) {
|
|
399
443
|
node.setAttribute("data-inside", "");
|
|
400
444
|
opts.inside.append(node);
|
|
@@ -30,6 +30,30 @@ export function commonRun(xs, ys) {
|
|
|
30
30
|
return [...head, ...middle, ...tail];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// A word diff of two texts, the reader's version against the file's (the conflict card's), or an
|
|
34
|
+
// Agent's baseline against the page (changes/): runs of { tag, text }, tag null
|
|
35
|
+
// for words both have, "del" for the reader's alone and "ins" for the file's alone (an ins before
|
|
36
|
+
// the del it stands beside). Words are runs of letters and digits, spaces, single other characters
|
|
37
|
+
// (each Chinese character on its own); at most 1500 words of each (commonRun above finds what is shared).
|
|
38
|
+
export function wordDiff(mine, theirs) {
|
|
39
|
+
const tokens = (text) => (text.match(/[A-Za-z0-9]+|\s+|[^A-Za-z0-9\s]/gu) ?? []).slice(0, 1500);
|
|
40
|
+
const a = tokens(mine);
|
|
41
|
+
const b = tokens(theirs);
|
|
42
|
+
const runs = [];
|
|
43
|
+
const add = (tag, text) => {
|
|
44
|
+
if (runs.at(-1)?.tag === tag) runs.at(-1).text += text;
|
|
45
|
+
else runs.push({ tag, text });
|
|
46
|
+
};
|
|
47
|
+
let [i, j] = [0, 0];
|
|
48
|
+
for (const [x, y] of [...commonRun(a, b), [a.length, b.length]]) {
|
|
49
|
+
while (j < y) add("ins", b[j++]);
|
|
50
|
+
while (i < x) add("del", a[i++]);
|
|
51
|
+
if (x < a.length) add(null, a[i++]);
|
|
52
|
+
j++;
|
|
53
|
+
}
|
|
54
|
+
return runs;
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
// ---------- merging a block both sides changed ----------
|
|
34
58
|
|
|
35
59
|
// Tokens of a block's inline HTML: a tag, an entity, a word, a run of spaces, or any other single
|
|
@@ -24,7 +24,7 @@ import { routine } from "./quiet.mjs";
|
|
|
24
24
|
// so that the box has the whole window to be as wide as.
|
|
25
25
|
|
|
26
26
|
const STYLE = `
|
|
27
|
-
.ge-toast { position: fixed; left: 0; right: 0; bottom: 24px; z-index: ${Z.toast}; display: flex; align-items: center; gap: 10px; box-sizing: border-box; width: fit-content; max-width: min(640px, calc(100vw - 32px)); margin: 0 auto; padding: 8px 14px; border-radius: 8px; background: var(--ge-toast-bg); color: #f0efed; box-shadow: var(--ge-shadow); font: 14px/1.45 var(--doc-font, system-ui, sans-serif); opacity: 0; transform: translateY(10px); pointer-events: none; transition: opacity .18s, transform .18s; }
|
|
27
|
+
.ge-toast { position: fixed; left: 0; right: 0; bottom: 24px; z-index: ${Z.toast}; display: flex; align-items: center; gap: 10px; box-sizing: border-box; width: fit-content; max-width: min(640px, calc(100vw - 32px)); margin: 0 auto; padding: 8px 14px; border-radius: 8px; background: var(--ge-toast-bg); color: #f0efed; box-shadow: var(--ge-shadow); font: 14px/1.45 var(--doc-sans-font, var(--doc-font, system-ui, sans-serif)); opacity: 0; transform: translateY(10px); pointer-events: none; transition: opacity .18s, transform .18s; }
|
|
28
28
|
.ge-toast[data-show] { opacity: 1; transform: none; }
|
|
29
29
|
.ge-toast[data-tone="error"]::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: 50%; background: #ff6b6b; }
|
|
30
30
|
`;
|