@formicoidea/labre-core 0.52.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/_pkgs/affine-block-data-view/data-view-block.js +1 -1
  2. package/dist/_pkgs/affine-block-database/components/title/index.js +7 -2
  3. package/dist/_pkgs/affine-block-database/database-block-styles.js +9 -1
  4. package/dist/_pkgs/affine-block-embed-doc/page-drop/page-drop.d.ts +70 -0
  5. package/dist/_pkgs/affine-block-embed-doc/page-drop/page-drop.js +196 -0
  6. package/dist/_pkgs/affine-block-embed-doc/view.js +3 -0
  7. package/dist/_pkgs/affine-block-frame/frame-template.d.ts +11 -0
  8. package/dist/_pkgs/affine-block-frame/frame-template.js +24 -4
  9. package/dist/_pkgs/affine-block-frame/translations.d.ts +3 -0
  10. package/dist/_pkgs/affine-block-frame/translations.js +7 -0
  11. package/dist/_pkgs/affine-block-surface/extensions/export-svg/render.js +9 -3
  12. package/dist/_pkgs/affine-block-surface/framework-background/renderer.js +9 -5
  13. package/dist/_pkgs/affine-block-surface/index.d.ts +1 -0
  14. package/dist/_pkgs/affine-block-surface/index.js +1 -0
  15. package/dist/_pkgs/affine-block-surface/renderer/canvas-renderer.d.ts +55 -3
  16. package/dist/_pkgs/affine-block-surface/renderer/canvas-renderer.js +139 -6
  17. package/dist/_pkgs/affine-block-surface/renderer/notation-tint.d.ts +92 -0
  18. package/dist/_pkgs/affine-block-surface/renderer/notation-tint.js +113 -0
  19. package/dist/_pkgs/affine-block-surface/surface-block.js +2 -2
  20. package/dist/_pkgs/affine-components/color-picker/styles.js +1 -1
  21. package/dist/_pkgs/affine-components/context-menu/input.js +2 -1
  22. package/dist/_pkgs/affine-components/context-menu/menu-renderer.d.ts +7 -0
  23. package/dist/_pkgs/affine-components/context-menu/menu-renderer.js +53 -13
  24. package/dist/_pkgs/affine-components/drop-indicator/file-drop-manager.js +15 -0
  25. package/dist/_pkgs/affine-gfx-shape/element-renderer/shape/index.js +4 -2
  26. package/dist/_pkgs/affine-inline-link/link-node/link-popup/styles.js +1 -1
  27. package/dist/_pkgs/affine-inline-reference/reference-node/reference-popup/reference-popup.js +9 -9
  28. package/dist/_pkgs/affine-shared/consts/notation.d.ts +1 -0
  29. package/dist/_pkgs/affine-shared/consts/notation.js +29 -0
  30. package/dist/_pkgs/affine-shared/services/frame-template-service.d.ts +5 -0
  31. package/dist/_pkgs/affine-shared/services/index.d.ts +1 -0
  32. package/dist/_pkgs/affine-shared/services/index.js +1 -0
  33. package/dist/_pkgs/affine-shared/services/page-drop-service.d.ts +61 -0
  34. package/dist/_pkgs/affine-shared/services/page-drop-service.js +30 -0
  35. package/dist/_pkgs/affine-shared/services/thread-marks-service.d.ts +32 -1
  36. package/dist/_pkgs/affine-shared/styles/scrollbar-style.js +1 -1
  37. package/dist/_pkgs/affine-widget-edgeless-auto-connect/index.js +5 -5
  38. package/dist/_pkgs/affine-widget-edgeless-selected-rect/edgeless-auto-complete.js +2 -2
  39. package/dist/_pkgs/affine-widget-keyboard-toolbar/config.js +6 -6
  40. package/dist/_pkgs/affine-widget-keyboard-toolbar/styles.js +1 -1
  41. package/dist/_pkgs/affine-widget-linked-doc/config.d.ts +8 -0
  42. package/dist/_pkgs/affine-widget-linked-doc/config.js +8 -1
  43. package/dist/_pkgs/affine-widget-linked-doc/linked-doc-popover.js +40 -32
  44. package/dist/_pkgs/affine-widget-linked-doc/styles.js +1 -1
  45. package/dist/_pkgs/affine-widget-linked-doc/translations.d.ts +2 -0
  46. package/dist/_pkgs/affine-widget-linked-doc/translations.js +6 -0
  47. package/dist/_pkgs/affine-widget-thread-marks/excerpt.d.ts +6 -2
  48. package/dist/_pkgs/affine-widget-thread-marks/excerpt.js +8 -0
  49. package/dist/_pkgs/affine-widget-thread-marks/index.d.ts +1 -0
  50. package/dist/_pkgs/affine-widget-thread-marks/index.js +1 -0
  51. package/dist/_pkgs/affine-widget-thread-marks/text-range.d.ts +26 -0
  52. package/dist/_pkgs/affine-widget-thread-marks/text-range.js +59 -0
  53. package/dist/_pkgs/affine-widget-thread-marks/toolbar.d.ts +4 -3
  54. package/dist/_pkgs/affine-widget-thread-marks/toolbar.js +24 -8
  55. package/dist/_pkgs/affine-widget-thread-marks/widget.d.ts +33 -2
  56. package/dist/_pkgs/affine-widget-thread-marks/widget.js +121 -5
  57. package/dist/_pkgs/affine-widget-toolbar/utils.d.ts +2 -1
  58. package/dist/_pkgs/affine-widget-toolbar/utils.js +5 -2
  59. package/dist/_pkgs/data-view/core/common/properties.js +2 -2
  60. package/dist/_pkgs/data-view/core/common/property-menu.d.ts +3 -3
  61. package/dist/_pkgs/data-view/core/common/property-menu.js +7 -7
  62. package/dist/_pkgs/data-view/core/component/overflow/overflow.js +4 -1
  63. package/dist/_pkgs/data-view/core/group-by/renderer/base.d.ts +4 -1
  64. package/dist/_pkgs/data-view/core/group-by/renderer/base.js +17 -2
  65. package/dist/_pkgs/data-view/core/group-by/renderer/number-group.js +1 -1
  66. package/dist/_pkgs/data-view/core/group-by/renderer/select-group.d.ts +0 -3
  67. package/dist/_pkgs/data-view/core/group-by/renderer/select-group.js +63 -118
  68. package/dist/_pkgs/data-view/core/group-by/renderer/string-group.js +1 -1
  69. package/dist/_pkgs/data-view/core/group-by/setting.js +2 -2
  70. package/dist/_pkgs/data-view/core/utils/wording.d.ts +6 -2
  71. package/dist/_pkgs/data-view/core/utils/wording.js +5 -3
  72. package/dist/_pkgs/data-view/core/view/default-name.d.ts +6 -0
  73. package/dist/_pkgs/data-view/core/view/default-name.js +8 -0
  74. package/dist/_pkgs/data-view/translations.d.ts +47 -0
  75. package/dist/_pkgs/data-view/translations.js +124 -0
  76. package/dist/_pkgs/data-view/view-presets/table/mobile/column-header.d.ts +3 -1
  77. package/dist/_pkgs/data-view/view-presets/table/mobile/column-header.js +102 -124
  78. package/dist/_pkgs/data-view/view-presets/table/mobile/group.js +4 -2
  79. package/dist/_pkgs/data-view/view-presets/table/mobile/header.js +17 -9
  80. package/dist/_pkgs/data-view/view-presets/table/mobile/row.js +20 -11
  81. package/dist/_pkgs/data-view/view-presets/table/mobile/table-view-style.d.ts +17 -0
  82. package/dist/_pkgs/data-view/view-presets/table/mobile/table-view-style.js +31 -6
  83. package/dist/_pkgs/data-view/view-presets/table/mobile/table-view-ui-logic.js +2 -2
  84. package/dist/_pkgs/data-view/view-presets/table/pc/group.js +4 -2
  85. package/dist/_pkgs/data-view/widget-presets/quick-setting-bar/filter/list-view.d.ts +3 -1
  86. package/dist/_pkgs/data-view/widget-presets/quick-setting-bar/filter/list-view.js +23 -3
  87. package/dist/_pkgs/data-view/widget-presets/tools/presets/search/search.d.ts +3 -0
  88. package/dist/_pkgs/data-view/widget-presets/tools/presets/search/search.js +37 -5
  89. package/dist/_pkgs/data-view/widget-presets/tools/presets/table-add-row/add-row.d.ts +3 -0
  90. package/dist/_pkgs/data-view/widget-presets/tools/presets/table-add-row/add-row.js +99 -35
  91. package/dist/_pkgs/data-view/widget-presets/tools/presets/view-options/view-options.js +30 -22
  92. package/dist/_pkgs/data-view/widget-presets/tools/presets/view-options/view-settings.js +8 -6
  93. package/dist/_pkgs/data-view/widget-presets/views-bar/views-view.js +15 -0
  94. package/dist/host-panels.d.ts +1 -1
  95. package/dist/host-panels.js +2 -1
  96. package/package.json +1 -1
@@ -0,0 +1,26 @@
1
+ import type { ThreadTextRange } from '../affine-shared/services/index.js';
2
+ import * as Y from 'yjs';
3
+ /** Where a range sits in its block's text today, by index. */
4
+ export interface ResolvedThreadRange {
5
+ index: number;
6
+ length: number;
7
+ }
8
+ /**
9
+ * The anchor of `length` characters from `index` in `yText` (ADR 0038,
10
+ * amendment of 2026-10-15), or `null` for an empty run or one outside the
11
+ * text.
12
+ *
13
+ * `start` sticks to the first selected character (`assoc = 0`) and `end` to
14
+ * the last one (`assoc = -1`), so typing right after the run does not grow
15
+ * it. Creating a relative position only reads the item ids: nothing is
16
+ * written to the document (held by `text-range.unit.spec.ts`).
17
+ */
18
+ export declare function encodeThreadRange(yText: Y.Text, index: number, length: number): ThreadTextRange | null;
19
+ /**
20
+ * Where `range` sits in `yText` now, or `null` when it cannot say: the text
21
+ * is detached, a string is not a position, a position belongs to another
22
+ * text, or the run collapsed because its text was deleted. Never throws: a
23
+ * host's stored anchor may be anything, and an unresolvable range simply is
24
+ * not drawn.
25
+ */
26
+ export declare function resolveThreadRange(yText: Y.Text, range: ThreadTextRange): ResolvedThreadRange | null;
@@ -0,0 +1,59 @@
1
+ import * as Y from 'yjs';
2
+ function toBase64(bytes) {
3
+ let binary = '';
4
+ for (const byte of bytes)
5
+ binary += String.fromCharCode(byte);
6
+ return btoa(binary);
7
+ }
8
+ function fromBase64(text) {
9
+ return Uint8Array.from(atob(text), char => char.charCodeAt(0));
10
+ }
11
+ /**
12
+ * The anchor of `length` characters from `index` in `yText` (ADR 0038,
13
+ * amendment of 2026-10-15), or `null` for an empty run or one outside the
14
+ * text.
15
+ *
16
+ * `start` sticks to the first selected character (`assoc = 0`) and `end` to
17
+ * the last one (`assoc = -1`), so typing right after the run does not grow
18
+ * it. Creating a relative position only reads the item ids: nothing is
19
+ * written to the document (held by `text-range.unit.spec.ts`).
20
+ */
21
+ export function encodeThreadRange(yText, index, length) {
22
+ if (!(length > 0) || index < 0 || index + length > yText.length) {
23
+ return null;
24
+ }
25
+ const start = Y.createRelativePositionFromTypeIndex(yText, index, 0);
26
+ const end = Y.createRelativePositionFromTypeIndex(yText, index + length, -1);
27
+ return {
28
+ start: toBase64(Y.encodeRelativePosition(start)),
29
+ end: toBase64(Y.encodeRelativePosition(end)),
30
+ };
31
+ }
32
+ function absoluteIndex(encoded, yText, doc) {
33
+ const position = Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(fromBase64(encoded)), doc);
34
+ // A position of another text (a host that mixed up two anchors) is not
35
+ // this block's run.
36
+ return position && position.type === yText ? position.index : null;
37
+ }
38
+ /**
39
+ * Where `range` sits in `yText` now, or `null` when it cannot say: the text
40
+ * is detached, a string is not a position, a position belongs to another
41
+ * text, or the run collapsed because its text was deleted. Never throws: a
42
+ * host's stored anchor may be anything, and an unresolvable range simply is
43
+ * not drawn.
44
+ */
45
+ export function resolveThreadRange(yText, range) {
46
+ const doc = yText.doc;
47
+ if (!doc)
48
+ return null;
49
+ try {
50
+ const start = absoluteIndex(range.start, yText, doc);
51
+ const end = absoluteIndex(range.end, yText, doc);
52
+ if (start === null || end === null || end <= start)
53
+ return null;
54
+ return { index: start, length: end - start };
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
@@ -9,15 +9,16 @@ import { type ThreadTarget } from './excerpt.js';
9
9
  * - On the canvas, exactly one selected model: a canvas element (shape,
10
10
  * connector, group, framework board) is an `element`, a canvas block (note,
11
11
  * frame, image, embed) a `block`.
12
- * - A text selection within one block: that block (the grain is the block,
13
- * never a range of text).
12
+ * - A text selection: the block it starts in, with the selected run of that
13
+ * block's text (ADR 0038, amendment of 2026-10-15).
14
14
  * - One selected block, or the block a hover toolbar is drawn for.
15
15
  */
16
16
  export declare function threadTargetOf(ctx: ToolbarContext): ThreadTarget | null;
17
17
  /**
18
18
  * Ask the host to open the discussion of `target`, with the excerpt the
19
19
  * library computes. Nothing is written: no prop, no id in the document.
20
- * Without the seam, nothing happens.
20
+ * Without the seam, nothing happens. `range` is handed over only when there
21
+ * is one, so a host comparing requests sees the first shape for a whole block.
21
22
  */
22
23
  export declare function openThreadOn(std: BlockStdScope, target: ThreadTarget): void;
23
24
  export declare const THREAD_DISCUSS_ACTION_ID = "g.thread-discuss";
@@ -3,6 +3,7 @@ import { ToolbarModuleExtension, toolbarModuleKey, ThreadMarksProvider, } from '
3
3
  import { BlockFlavourIdentifier, BlockSelection, TextSelection, } from '../std/index.js';
4
4
  import { GfxPrimitiveElementModel } from '../std/gfx/index.js';
5
5
  import { threadExcerpt } from './excerpt.js';
6
+ import { encodeThreadRange } from './text-range.js';
6
7
  import { THREAD_DISCUSS } from './translations.js';
7
8
  /** Containers a discussion never anchors to: the page and the canvas itself. */
8
9
  const NOT_A_TARGET = new Set(['affine:page', 'affine:surface']);
@@ -14,6 +15,22 @@ function blockTarget(id, ctx) {
14
15
  return null;
15
16
  return { targetType: 'block', targetId: model.id };
16
17
  }
18
+ /**
19
+ * The block a text selection starts in, with the selected run of it. A
20
+ * selection over several blocks anchors to the first one, on the part of it
21
+ * that is selected (ADR 0038, amendment of 2026-10-15). A run that cannot be
22
+ * encoded (an empty one, a block with no text) leaves the block alone.
23
+ */
24
+ function textTarget(text, ctx) {
25
+ const target = blockTarget(text.from.blockId, ctx);
26
+ if (!target)
27
+ return null;
28
+ const yText = ctx.store.getModelById(target.targetId)?.text?.yText;
29
+ const range = yText
30
+ ? encodeThreadRange(yText, text.from.index, text.from.length)
31
+ : null;
32
+ return range ? { ...target, range } : target;
33
+ }
17
34
  /**
18
35
  * The ONE target a toolbar row speaks for, or `null` when it speaks for
19
36
  * several (a discussion anchors to one block or one element, labre ADR-0048).
@@ -21,8 +38,8 @@ function blockTarget(id, ctx) {
21
38
  * - On the canvas, exactly one selected model: a canvas element (shape,
22
39
  * connector, group, framework board) is an `element`, a canvas block (note,
23
40
  * frame, image, embed) a `block`.
24
- * - A text selection within one block: that block (the grain is the block,
25
- * never a range of text).
41
+ * - A text selection: the block it starts in, with the selected run of that
42
+ * block's text (ADR 0038, amendment of 2026-10-15).
26
43
  * - One selected block, or the block a hover toolbar is drawn for.
27
44
  */
28
45
  export function threadTargetOf(ctx) {
@@ -38,11 +55,8 @@ export function threadTargetOf(ctx) {
38
55
  : blockTarget(model.id, ctx);
39
56
  }
40
57
  const text = ctx.selection.find(TextSelection);
41
- if (text) {
42
- const from = text.from.blockId;
43
- const to = text.to?.blockId ?? from;
44
- return from === to ? blockTarget(from, ctx) : null;
45
- }
58
+ if (text)
59
+ return textTarget(text, ctx);
46
60
  if (ctx.selection.filter(BlockSelection).length > 1)
47
61
  return null;
48
62
  return blockTarget(ctx.getCurrentModelBy(BlockSelection)?.id, ctx);
@@ -50,7 +64,8 @@ export function threadTargetOf(ctx) {
50
64
  /**
51
65
  * Ask the host to open the discussion of `target`, with the excerpt the
52
66
  * library computes. Nothing is written: no prop, no id in the document.
53
- * Without the seam, nothing happens.
67
+ * Without the seam, nothing happens. `range` is handed over only when there
68
+ * is one, so a host comparing requests sees the first shape for a whole block.
54
69
  */
55
70
  export function openThreadOn(std, target) {
56
71
  const service = std.getOptional(ThreadMarksProvider);
@@ -61,6 +76,7 @@ export function openThreadOn(std, target) {
61
76
  targetType: target.targetType,
62
77
  targetId: target.targetId,
63
78
  excerpt: threadExcerpt(std, target),
79
+ ...(target.range ? { range: target.range } : {}),
64
80
  });
65
81
  }
66
82
  export const THREAD_DISCUSS_ACTION_ID = 'g.thread-discuss';
@@ -1,5 +1,7 @@
1
+ import { type SelectionRect } from '../affine-shared/commands/index.js';
1
2
  import { type ThreadMark } from '../affine-shared/services/index.js';
2
3
  import { WidgetComponent } from '../std/index.js';
4
+ import { nothing } from 'lit';
3
5
  export declare const AFFINE_THREAD_MARKS_WIDGET = "affine-thread-marks-widget";
4
6
  /**
5
7
  * Where a mark sits, relative to the widget's own origin: beside a block of
@@ -15,6 +17,8 @@ export interface ThreadMarkPlace {
15
17
  export interface PlacedThreadMark extends ThreadMarkPlace {
16
18
  mark: ThreadMark;
17
19
  }
20
+ /** Is the point (viewport coordinates) inside one of the rects? */
21
+ export declare function hitsThreadRange(rects: readonly SelectionRect[], x: number, y: number): boolean;
18
22
  /**
19
23
  * The marks to draw, from what the host says and where the editor finds each
20
24
  * target. Pure, so the "ignore what is absent" rule is held by a test:
@@ -22,7 +26,9 @@ export interface PlacedThreadMark extends ThreadMarkPlace {
22
26
  * - a target `locate` cannot find (deleted, of the other view, hidden for this
23
27
  * viewer) is skipped, never an error: an orphan anchor has no mark;
24
28
  * - a mark at `count <= 0` is not drawn;
25
- * - two marks on one target are one mark: counts add up, unread wins.
29
+ * - two marks on one target are one mark: counts add up, unread wins, and
30
+ * the run of text it opens is kept only when every merged mark names the
31
+ * same one (a mark that stands for several discussions opens the block).
26
32
  */
27
33
  export declare function placeThreadMarks(marks: readonly ThreadMark[], locate: (mark: ThreadMark) => ThreadMarkPlace | null): PlacedThreadMark[];
28
34
  /**
@@ -31,6 +37,15 @@ export declare function placeThreadMarks(marks: readonly ThreadMark[], locate: (
31
37
  * on the canvas on the corner of each marked element or block. A click asks
32
38
  * the host to open the discussion, exactly as "Discuss" does.
33
39
  *
40
+ * A mark that carries a `range` also highlights that run of its block's text
41
+ * in the page view (ADR 0038, amendment of 2026-10-15). The run is resolved
42
+ * from its Yjs relative positions on every render and drawn as the range's
43
+ * client rects (the technique of `doc-remote-selection`), never as a mark in
44
+ * the text. The highlight takes no pointer event, so nothing blocks placing
45
+ * the caret or selecting; a click that lands on it with nothing selected
46
+ * opens its discussion. A run that no longer resolves is not drawn, and the
47
+ * block's own mark stays.
48
+ *
34
49
  * It reads the seam and the layout, and writes nothing. Positions are
35
50
  * client rects taken relative to the widget's own box and recomputed, once a
36
51
  * frame at most, when the marks, the document, the canvas or the window
@@ -41,8 +56,24 @@ export declare class ThreadMarksWidget extends WidgetComponent {
41
56
  private readonly _refresh;
42
57
  private get _edgeless();
43
58
  connectedCallback(): void;
59
+ /**
60
+ * A click on a highlighted run opens its discussion. The click is not
61
+ * consumed: the caret lands where it was put. A click that ends a drag
62
+ * (something is selected) is a selection, not a request.
63
+ */
64
+ private readonly _onClick;
65
+ /** Open the discussion of the run under the point, if there is one. */
66
+ private _openRangeAt;
67
+ private _resolve;
68
+ /**
69
+ * The runs to highlight, in viewport coordinates, page view only: on the
70
+ * canvas a note's text may sit under a shape, and this overlay would paint
71
+ * over it. `ponytail:` a run in a canvas note keeps only its block's mark;
72
+ * drawing it there needs the canvas stacking, if a reader asks for it.
73
+ */
74
+ private _ranges;
44
75
  private _locate;
45
- render(): unknown;
76
+ render(): import("lit-html").TemplateResult<1> | typeof nothing;
46
77
  }
47
78
  declare global {
48
79
  interface HTMLElementTagNameMap {
@@ -1,17 +1,27 @@
1
1
  import { CommentIcon } from '@blocksuite/icons/lit';
2
2
  import { isPainted } from '../affine-block-surface/index.js';
3
+ import { getRangeRects, } from '../affine-shared/commands/index.js';
3
4
  import { ThreadMarksProvider, translateKey, } from '../affine-shared/services/index.js';
4
5
  import { isInsideEdgelessEditor, requestThrottledConnectedFrame, } from '../affine-shared/utils/index.js';
5
- import { WidgetComponent } from '../std/index.js';
6
+ import { TextSelection, WidgetComponent } from '../std/index.js';
6
7
  import { GfxBlockElementModel, GfxControllerIdentifier, GfxPrimitiveElementModel, } from '../std/gfx/index.js';
7
8
  import { effect } from '@preact/signals-core';
8
9
  import { cssVarV2 } from '@toeverything/theme/v2';
9
10
  import { css, html, nothing, unsafeCSS } from 'lit';
10
11
  import { repeat } from 'lit/directives/repeat.js';
11
12
  import { styleMap } from 'lit/directives/style-map.js';
13
+ import { resolveThreadRange } from './text-range.js';
12
14
  import { openThreadOn } from './toolbar.js';
13
15
  import { THREAD_DISCUSS } from './translations.js';
14
16
  export const AFFINE_THREAD_MARKS_WIDGET = 'affine-thread-marks-widget';
17
+ /** Is the point (viewport coordinates) inside one of the rects? */
18
+ export function hitsThreadRange(rects, x, y) {
19
+ return rects.some(rect => x >= rect.left &&
20
+ x <= rect.left + rect.width &&
21
+ y >= rect.top &&
22
+ y <= rect.top + rect.height);
23
+ }
24
+ const sameRange = (a, b) => a?.start === b?.start && a?.end === b?.end;
15
25
  /**
16
26
  * The marks to draw, from what the host says and where the editor finds each
17
27
  * target. Pure, so the "ignore what is absent" rule is held by a test:
@@ -19,7 +29,9 @@ export const AFFINE_THREAD_MARKS_WIDGET = 'affine-thread-marks-widget';
19
29
  * - a target `locate` cannot find (deleted, of the other view, hidden for this
20
30
  * viewer) is skipped, never an error: an orphan anchor has no mark;
21
31
  * - a mark at `count <= 0` is not drawn;
22
- * - two marks on one target are one mark: counts add up, unread wins.
32
+ * - two marks on one target are one mark: counts add up, unread wins, and
33
+ * the run of text it opens is kept only when every merged mark names the
34
+ * same one (a mark that stands for several discussions opens the block).
23
35
  */
24
36
  export function placeThreadMarks(marks, locate) {
25
37
  const merged = new Map();
@@ -33,6 +45,7 @@ export function placeThreadMarks(marks, locate) {
33
45
  ...seen,
34
46
  count: seen.count + mark.count,
35
47
  unread: seen.unread || mark.unread,
48
+ range: sameRange(seen.range, mark.range) ? seen.range : undefined,
36
49
  }
37
50
  : mark);
38
51
  }
@@ -50,6 +63,15 @@ export function placeThreadMarks(marks, locate) {
50
63
  * on the canvas on the corner of each marked element or block. A click asks
51
64
  * the host to open the discussion, exactly as "Discuss" does.
52
65
  *
66
+ * A mark that carries a `range` also highlights that run of its block's text
67
+ * in the page view (ADR 0038, amendment of 2026-10-15). The run is resolved
68
+ * from its Yjs relative positions on every render and drawn as the range's
69
+ * client rects (the technique of `doc-remote-selection`), never as a mark in
70
+ * the text. The highlight takes no pointer event, so nothing blocks placing
71
+ * the caret or selecting; a click that lands on it with nothing selected
72
+ * opens its discussion. A run that no longer resolves is not drawn, and the
73
+ * block's own mark stays.
74
+ *
53
75
  * It reads the seam and the layout, and writes nothing. Positions are
54
76
  * client rects taken relative to the widget's own box and recomputed, once a
55
77
  * frame at most, when the marks, the document, the canvas or the window
@@ -61,6 +83,16 @@ export class ThreadMarksWidget extends WidgetComponent {
61
83
  this._refresh = requestThrottledConnectedFrame(() => {
62
84
  this.requestUpdate();
63
85
  }, this);
86
+ /**
87
+ * A click on a highlighted run opens its discussion. The click is not
88
+ * consumed: the caret lands where it was put. A click that ends a drag
89
+ * (something is selected) is a selection, not a request.
90
+ */
91
+ this._onClick = (event) => {
92
+ if (window.getSelection()?.isCollapsed === false)
93
+ return;
94
+ this._openRangeAt(event.clientX, event.clientY);
95
+ };
64
96
  }
65
97
  static { this.styles = css `
66
98
  :host {
@@ -88,6 +120,16 @@ export class ThreadMarksWidget extends WidgetComponent {
88
120
  pointer-events: auto;
89
121
  }
90
122
 
123
+ .thread-range {
124
+ position: absolute;
125
+ top: 0;
126
+ left: 0;
127
+ background-color: ${unsafeCSS(cssVarV2('block/comment/highlightDefault'))};
128
+ border-bottom: 2px solid
129
+ ${unsafeCSS(cssVarV2('block/comment/highlightUnderline'))};
130
+ pointer-events: none;
131
+ }
132
+
91
133
  .thread-mark[data-unread] {
92
134
  color: ${unsafeCSS(cssVarV2('text/emphasis'))};
93
135
  }
@@ -116,6 +158,12 @@ export class ThreadMarksWidget extends WidgetComponent {
116
158
  _disposables.addFromEvent(window, 'resize', this._refresh);
117
159
  // Capture: the page view scrolls in a container, not the window.
118
160
  _disposables.addFromEvent(window, 'scroll', this._refresh, true);
161
+ // A highlighted run moves with every keystroke, local or remote, and a
162
+ // text edit is no block update: follow the document itself.
163
+ const doc = std.store.spaceDoc;
164
+ doc.on('update', this._refresh);
165
+ _disposables.add(() => doc.off('update', this._refresh));
166
+ _disposables.addFromEvent(std.host, 'click', this._onClick);
119
167
  const gfx = std.getOptional(GfxControllerIdentifier);
120
168
  if (gfx) {
121
169
  _disposables.add(gfx.viewport.viewportUpdated.subscribe(this._refresh));
@@ -134,6 +182,62 @@ export class ThreadMarksWidget extends WidgetComponent {
134
182
  }));
135
183
  }
136
184
  }
185
+ /** Open the discussion of the run under the point, if there is one. */
186
+ _openRangeAt(x, y) {
187
+ const service = this.std.getOptional(ThreadMarksProvider);
188
+ if (!service)
189
+ return;
190
+ const hit = this._ranges(service.marks$.peek()).find(({ rects }) => hitsThreadRange(rects, x, y));
191
+ if (!hit)
192
+ return;
193
+ openThreadOn(this.std, {
194
+ targetType: 'block',
195
+ targetId: hit.mark.targetId,
196
+ range: hit.mark.range,
197
+ });
198
+ }
199
+ _resolve(mark) {
200
+ if (mark.targetType !== 'block' || !mark.range)
201
+ return null;
202
+ const yText = this.std.store.getModelById(mark.targetId)?.text?.yText;
203
+ return yText ? resolveThreadRange(yText, mark.range) : null;
204
+ }
205
+ /**
206
+ * The runs to highlight, in viewport coordinates, page view only: on the
207
+ * canvas a note's text may sit under a shape, and this overlay would paint
208
+ * over it. `ponytail:` a run in a canvas note keeps only its block's mark;
209
+ * drawing it there needs the canvas stacking, if a reader asks for it.
210
+ */
211
+ _ranges(marks) {
212
+ if (this._edgeless)
213
+ return [];
214
+ const drawn = [];
215
+ for (const mark of marks) {
216
+ if (!(mark.count > 0))
217
+ continue;
218
+ const run = this._resolve(mark);
219
+ if (!run)
220
+ continue;
221
+ let range;
222
+ try {
223
+ range = this.std.range.textSelectionToRange(this.std.selection.create(TextSelection, {
224
+ from: { blockId: mark.targetId, ...run },
225
+ to: null,
226
+ }));
227
+ }
228
+ catch {
229
+ // The inline editor has not rendered the latest text yet: the next
230
+ // frame draws it.
231
+ range = null;
232
+ }
233
+ if (!range)
234
+ continue;
235
+ const rects = getRangeRects(range, null);
236
+ if (rects.length > 0)
237
+ drawn.push({ mark, rects });
238
+ }
239
+ return drawn;
240
+ }
137
241
  _locate(mark, origin) {
138
242
  const edgeless = this._edgeless;
139
243
  if (edgeless) {
@@ -180,10 +284,21 @@ export class ThreadMarksWidget extends WidgetComponent {
180
284
  return nothing;
181
285
  const origin = this.getBoundingClientRect();
182
286
  const placed = placeThreadMarks(marks, mark => this._locate(mark, origin));
183
- if (placed.length === 0)
287
+ const ranges = this._ranges(marks);
288
+ if (placed.length === 0 && ranges.length === 0)
184
289
  return nothing;
185
290
  const label = translateKey(this.std, ...THREAD_DISCUSS);
186
- return repeat(placed, ({ mark }) => `${mark.targetType}:${mark.targetId}`, ({ mark, x, y, anchor }) => html `<icon-button
291
+ const highlights = ranges.flatMap(({ mark, rects }) => rects.map(rect => html `<div
292
+ class="thread-range"
293
+ data-testid="thread-range"
294
+ data-target-id=${mark.targetId}
295
+ style=${styleMap({
296
+ width: `${rect.width}px`,
297
+ height: `${rect.height}px`,
298
+ transform: `translate(${rect.left - origin.left}px, ${rect.top - origin.top}px)`,
299
+ })}
300
+ ></div>`));
301
+ return html `${highlights}${repeat(placed, ({ mark }) => `${mark.targetType}:${mark.targetId}`, ({ mark, x, y, anchor }) => html `<icon-button
187
302
  class="thread-mark"
188
303
  data-testid="thread-mark"
189
304
  data-target-type=${mark.targetType}
@@ -206,9 +321,10 @@ export class ThreadMarksWidget extends WidgetComponent {
206
321
  openThreadOn(this.std, {
207
322
  targetType: mark.targetType,
208
323
  targetId: mark.targetId,
324
+ range: mark.range,
209
325
  });
210
326
  }}
211
327
  >${CommentIcon({ width: '16px', height: '16px' })}</icon-button
212
- >`);
328
+ >`)}`;
213
329
  }
214
330
  }
@@ -1,5 +1,5 @@
1
1
  import { type EditorToolbar } from '../affine-components/toolbar/index.js';
2
- import { ActionPlacement, type ToolbarActions, type ToolbarContext, type ToolbarLayoutStep, type ToolbarPlacement } from '../affine-shared/services/index.js';
2
+ import { ActionPlacement, type ToolbarAction, type ToolbarActions, type ToolbarContext, type ToolbarLayoutStep, type ToolbarPlacement } from '../affine-shared/services/index.js';
3
3
  import type { AutoUpdateOptions, ReferenceElement, SideObject } from '@floating-ui/dom';
4
4
  export declare const sideMap: Map<string, {
5
5
  top: number;
@@ -33,6 +33,7 @@ export declare function combine(actions: ToolbarActions, context: ToolbarContext
33
33
  content?: ((cx: ToolbarContext) => import("lit-html").TemplateResult | null) | (import("lit-html").TemplateResult | null);
34
34
  run?: (cx: ToolbarContext) => void;
35
35
  }[];
36
+ export declare function group(actions: ToolbarAction[]): ToolbarAction[];
36
37
  /**
37
38
  * How much of the row an entry has given up, decided by {@link ToolbarFitter}
38
39
  * and re-applied by every render until the room changes again.
@@ -210,7 +210,7 @@ export function combine(actions, context) {
210
210
  });
211
211
  return filtered;
212
212
  }
213
- function group(actions) {
213
+ export function group(actions) {
214
214
  const grouped = groupBy(actions, a => a.id);
215
215
  const paired = toPairs(grouped).map(([_, items]) => {
216
216
  if (items.length === 1)
@@ -222,7 +222,10 @@ function group(actions) {
222
222
  });
223
223
  return paired;
224
224
  }
225
- const merge = (a, b) => mergeWith(a, b, (obj, src) => Array.isArray(obj) ? group(obj.concat(src)) : src);
225
+ // Only a group's `actions` is a list of entries to merge by id; any other
226
+ // array (a `labelWording` tuple) is a value, and grouping it turned the
227
+ // wording into a keyless object the host's i18next choked on.
228
+ const merge = (a, b) => mergeWith(a, b, (obj, src, key) => key === 'actions' && Array.isArray(obj) ? group(obj.concat(src)) : src);
226
229
  export function emptyToolbarLayout() {
227
230
  return { shrunk: new Set(), collapsed: new Set() };
228
231
  }
@@ -147,11 +147,11 @@ let DataViewPropertiesSettingView = (() => {
147
147
  width: 4px;
148
148
  height: 12px;
149
149
  border-radius: 1px;
150
- background-color: #efeff0;
150
+ background-color: var(--affine-border-color);
151
151
  }
152
152
 
153
153
  .property-item:hover .property-item-drag-bar {
154
- background-color: #c0bfc1;
154
+ background-color: var(--affine-placeholder-color);
155
155
  }
156
156
 
157
157
  .property-item-icon {
@@ -2,7 +2,7 @@ import { type MenuConfig, type PopupTarget } from '../../../affine-components/co
2
2
  import { type Wording } from '../utils/wording.js';
3
3
  import type { Property } from '../view-manager/property.js';
4
4
  import type { SingleView } from '../view-manager/single-view.js';
5
- export declare const inputConfig: (property: Property) => (menu: import("../../../affine-components/context-menu/menu.js").Menu) => import("lit-html").TemplateResult<1> | undefined;
5
+ export declare const inputConfig: (property: Property, wording?: Wording) => (menu: import("../../../affine-components/context-menu/menu.js").Menu) => import("lit-html").TemplateResult<1> | undefined;
6
6
  export declare const typeConfig: (property: Property, wording?: Wording) => (menu: import("../../../affine-components/context-menu/menu.js").Menu, index: number) => import("lit-html").TemplateResult | undefined;
7
7
  /** What a property menu's opener adds around the shared entries. */
8
8
  export type PropertyMenuSlots = {
@@ -28,8 +28,8 @@ export type PropertyMenuSlots = {
28
28
  * ponytail: "Number Format" stays a table slot (its bar is a table/pc
29
29
  * element, behind `enable_number_formatting`, off in Labre), so the
30
30
  * properties list does not offer it. Move the bar to core and the entry into
31
- * this list when the flag ships. The virtual and mobile table headers still
32
- * spell their own copy of these entries; point them here in a dedicated pass.
31
+ * this list when the flag ships. The virtual table header still spells its
32
+ * own copy of these entries; point it here in a dedicated pass.
33
33
  */
34
34
  export declare const propertyMenuItems: (property: Property, slots?: PropertyMenuSlots) => MenuConfig[];
35
35
  export declare const popPropertyMenu: (target: PopupTarget, property: Property, slots?: PropertyMenuSlots) => import("../../../affine-components/context-menu/menu-renderer.js").MenuHandler;
@@ -2,10 +2,10 @@ import { menu, popMenu, } from '../../../affine-components/context-menu/index.js
2
2
  import { TOOLBAR_DELETE, TOOLBAR_DUPLICATE, } from '../../../affine-shared/services/index.js';
3
3
  import { DeleteIcon, DuplicateIcon, ViewIcon } from '@blocksuite/icons/lit';
4
4
  import { html } from 'lit/static-html.js';
5
- import { DATA_VIEW_PROPERTY_HIDE_IN_VIEW, DATA_VIEW_PROPERTY_TYPE, } from '../../translations.js';
5
+ import { DATA_VIEW_PROPERTY_HIDE_IN_VIEW, DATA_VIEW_PROPERTY_NAME, DATA_VIEW_PROPERTY_TYPE, DATA_VIEW_PROPERTY_TYPE_ENTRY, } from '../../translations.js';
6
6
  import { renderUniLit } from '../utils/uni-component/index.js';
7
7
  import { englishWording } from '../utils/wording.js';
8
- export const inputConfig = (property) => {
8
+ export const inputConfig = (property, wording = englishWording) => {
9
9
  return menu.input({
10
10
  prefix: html `
11
11
  <div class="affine-database-column-type-menu-icon">
@@ -13,7 +13,7 @@ export const inputConfig = (property) => {
13
13
  </div>
14
14
  `,
15
15
  initialValue: property.name$.value,
16
- placeholder: 'Property name',
16
+ placeholder: wording(DATA_VIEW_PROPERTY_NAME),
17
17
  // Persist on blur so the typed name survives the menu being dismissed by
18
18
  // a click outside, which never fires the completion path.
19
19
  onBlur: text => {
@@ -25,7 +25,7 @@ export const typeConfig = (property, wording = englishWording) => {
25
25
  return menu.group({
26
26
  items: [
27
27
  menu.subMenu({
28
- name: 'Type',
28
+ name: wording(DATA_VIEW_PROPERTY_TYPE_ENTRY),
29
29
  hide: () => !property.typeCanSet,
30
30
  postfix: html ` <div
31
31
  class="affine-database-column-type-icon"
@@ -75,13 +75,13 @@ export const typeConfig = (property, wording = englishWording) => {
75
75
  * ponytail: "Number Format" stays a table slot (its bar is a table/pc
76
76
  * element, behind `enable_number_formatting`, off in Labre), so the
77
77
  * properties list does not offer it. Move the bar to core and the entry into
78
- * this list when the flag ships. The virtual and mobile table headers still
79
- * spell their own copy of these entries; point them here in a dedicated pass.
78
+ * this list when the flag ships. The virtual table header still spells its
79
+ * own copy of these entries; point it here in a dedicated pass.
80
80
  */
81
81
  export const propertyMenuItems = (property, slots = {}) => {
82
82
  const wording = slots.wording ?? englishWording;
83
83
  return [
84
- inputConfig(property),
84
+ inputConfig(property, wording),
85
85
  typeConfig(property, wording),
86
86
  ...(slots.afterType ?? []),
87
87
  menu.group({
@@ -136,7 +136,10 @@ let Overflow = (() => {
136
136
  const moreWidth = moreButton?.getBoundingClientRect().width;
137
137
  if (moreWidth !== undefined)
138
138
  this.moreExtraWidth = moreWidth;
139
- const widths = Array.from(this.items, item => item.getBoundingClientRect().width);
139
+ // The natural width: a host may let a drawn item give way (the views
140
+ // bar clips its pinned tab), and a shrunk width fed back here would make
141
+ // room that is not there, then take it back, pass after pass.
142
+ const widths = Array.from(this.items, item => item.scrollWidth);
140
143
  const drawn = fitOverflow({
141
144
  widths,
142
145
  container: this.getBoundingClientRect().width,
@@ -1,10 +1,13 @@
1
- import { ShadowlessElement } from '../../../../std/index.js';
1
+ import { type BlockStdScope, ShadowlessElement } from '../../../../std/index.js';
2
2
  import type { Group } from '../trait.js';
3
3
  import type { GroupRenderProps } from '../types.js';
4
4
  declare const BaseGroup_base: typeof ShadowlessElement & import("../../../../global/utils/types.js").Constructor<import("../../../../global/lit/with-disposable.js").DisposableClass>;
5
5
  export declare class BaseGroup<JsonValue, Data extends Record<string, unknown>> extends BaseGroup_base implements GroupRenderProps<JsonValue, Data> {
6
6
  accessor group: Group<unknown, JsonValue, Data>;
7
7
  accessor readonly: boolean;
8
+ /** Absent outside an editor host: the wordings read English. */
9
+ accessor std: BlockStdScope;
10
+ get ungrouped(): string;
8
11
  updateData(data: Data): void;
9
12
  updateValue(value: JsonValue): void;
10
13
  get value(): JsonValue;