@formicoidea/labre-core 0.34.1 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_pkgs/affine-block-surface/extensions/editor-anchored-panel.d.ts +18 -2
- package/dist/_pkgs/affine-block-surface/extensions/editor-anchored-panel.js +141 -8
- package/dist/_pkgs/affine-block-surface/extensions/map-quality-widget.js +16 -22
- package/dist/_pkgs/affine-block-surface/extensions/reading-widget.d.ts +18 -0
- package/dist/_pkgs/affine-block-surface/extensions/reading-widget.js +35 -12
- package/dist/_pkgs/affine-block-surface/extensions/reading.d.ts +8 -0
- package/dist/_pkgs/affine-block-surface/extensions/reading.js +17 -6
- package/dist/_pkgs/affine-block-surface/extensions/tags-toolbar.js +10 -2
- package/dist/_pkgs/affine-block-surface/extensions/validation-toolbar.js +9 -2
- package/dist/_pkgs/affine-block-surface/framework-background/def.d.ts +14 -11
- package/dist/_pkgs/affine-block-surface/framework-background/renderer.js +14 -7
- package/dist/_pkgs/affine-components/highlight-dropdown-menu/highlight-duotone-icon.js +8 -8
- package/dist/_pkgs/affine-components/highlight-dropdown-menu/text-duotone-icon.js +11 -11
- package/dist/_pkgs/affine-components/icons/text.js +426 -426
- package/dist/_pkgs/affine-components/toolbar/menu-button.js +27 -0
- package/dist/_pkgs/affine-gfx-connector/direction/direction-reveal.d.ts +2 -2
- package/dist/_pkgs/affine-gfx-connector/direction/direction-reveal.js +1 -1
- package/dist/_pkgs/affine-gfx-connector/direction/direction-widget.d.ts +1 -1
- package/dist/_pkgs/affine-gfx-connector/direction/direction-widget.js +10 -1
- package/dist/_pkgs/affine-model/elements/c4/board.d.ts +49 -0
- package/dist/_pkgs/affine-model/elements/c4/board.js +68 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/menu/slide-menu.js +1 -1
- package/dist/_pkgs/affine-widget-keyboard-toolbar/icons.js +98 -98
- package/dist/_pkgs/data-view/core/group-by/renderer/boolean-group.js +11 -11
- package/dist/_pkgs/data-view/core/utils/menu-title.js +17 -17
- package/dist/_pkgs/data-view/view-presets/table/pc-virtual/group/top/group-title.js +18 -18
- package/dist/_pkgs/std/gfx/index.d.ts +1 -1
- package/dist/_pkgs/std/gfx/index.js +1 -1
- package/dist/_pkgs/std/gfx/interactivity/gfx-view-event-handler.d.ts +57 -0
- package/dist/_pkgs/std/gfx/interactivity/gfx-view-event-handler.js +80 -25
- package/dist/_pkgs/std/gfx/model/surface/pivot.d.ts +55 -1
- package/dist/_pkgs/std/gfx/model/surface/pivot.js +87 -0
- package/dist/_pkgs/std/gfx/model/surface/role.d.ts +12 -1
- package/package.json +1 -1
|
@@ -89,7 +89,19 @@ export declare abstract class EditorAnchoredPanel extends WidgetComponent<RootBl
|
|
|
89
89
|
protected anchorBox(): AnchoredPanelBox;
|
|
90
90
|
/**
|
|
91
91
|
* The panel's frame: the box, the dialog semantics and the pointer
|
|
92
|
-
* swallowing. The subclass passes its own
|
|
92
|
+
* swallowing. The subclass passes its own two halves and nothing else.
|
|
93
|
+
*
|
|
94
|
+
* ## Why the header is a parameter and not a line of the body
|
|
95
|
+
*
|
|
96
|
+
* Because the split is the whole fix. Both panels rendered their title as the
|
|
97
|
+
* first node of a body that was itself the scrolling box, so a reading long
|
|
98
|
+
* enough to scroll scrolled its own title away. Making the header an argument
|
|
99
|
+
* is what forces a consumer to say which of its markup stays put — a default
|
|
100
|
+
* would have quietly kept the old behaviour for whoever forgot.
|
|
101
|
+
*
|
|
102
|
+
* The header is where a panel puts its title and, if it has one, the actions
|
|
103
|
+
* that belong beside it: Map quality's close button rides there and stays
|
|
104
|
+
* reachable however far the checklist runs.
|
|
93
105
|
*
|
|
94
106
|
* Deliberately NOT `aria-modal`: it promises everything behind the dialog is
|
|
95
107
|
* inert, and these panels promise the opposite — the canvas stays usable
|
|
@@ -105,7 +117,11 @@ export declare abstract class EditorAnchoredPanel extends WidgetComponent<RootBl
|
|
|
105
117
|
variant: string;
|
|
106
118
|
/** The element the panel is about, when it is about one. */
|
|
107
119
|
elementId?: string;
|
|
108
|
-
},
|
|
120
|
+
},
|
|
121
|
+
/** Pinned: the title, and whatever must stay beside it. */
|
|
122
|
+
header: unknown,
|
|
123
|
+
/** Scrolled: everything the panel has to say. */
|
|
124
|
+
body: unknown): TemplateResult;
|
|
109
125
|
/**
|
|
110
126
|
* Click-away, Escape, and the two things that move the bar under the panel.
|
|
111
127
|
*
|
|
@@ -31,8 +31,9 @@ import { styleMap } from 'lit/directives/style-map.js';
|
|
|
31
31
|
* ## What is shared, and what is not
|
|
32
32
|
*
|
|
33
33
|
* Shared: the host geometry and its layer, the panel's box and chrome, the
|
|
34
|
-
* click-away and Escape contracts, and
|
|
35
|
-
* single line of CONTENT — each panel
|
|
34
|
+
* pinned header over a scrolling body, the click-away and Escape contracts, and
|
|
35
|
+
* the re-measure wiring. Not shared: a single line of CONTENT — each panel
|
|
36
|
+
* passes its own header and body to
|
|
36
37
|
* {@link EditorAnchoredPanel.renderAnchoredPanel}, and neither knows the other
|
|
37
38
|
* exists.
|
|
38
39
|
*/
|
|
@@ -91,6 +92,32 @@ const PANEL_MIN_HEIGHT = 160;
|
|
|
91
92
|
* wants.
|
|
92
93
|
*/
|
|
93
94
|
export const EDITOR_ANCHORED_PANEL_Z_INDEX = 'calc(var(--affine-z-index-popover, 1000) + 10)';
|
|
95
|
+
/**
|
|
96
|
+
* The corner radius of the senior button bar, which this panel wears too.
|
|
97
|
+
*
|
|
98
|
+
* `edgeless-toolbar` rounds at 16 and so, since #199, does every menu that
|
|
99
|
+
* slides out of it. The panel is the same family of chrome floating over the
|
|
100
|
+
* same bar; at 8 it read as a foreign box parked above the toolbar.
|
|
101
|
+
*/
|
|
102
|
+
const PANEL_RADIUS = 16;
|
|
103
|
+
/**
|
|
104
|
+
* The inset between the panel's border and the words inside it.
|
|
105
|
+
*
|
|
106
|
+
* It has to clear the corner arc, not merely look comfortable: an inner
|
|
107
|
+
* rectangle inset by less than `r × (1 − 1/√2)` ≈ 4.7px has its own corners cut
|
|
108
|
+
* by a 16px radius. Every inset below is at or above that, which is why the
|
|
109
|
+
* scrolling box never bleeds into the rounded edge.
|
|
110
|
+
*/
|
|
111
|
+
const PANEL_INLINE_PADDING = 16;
|
|
112
|
+
/**
|
|
113
|
+
* The width of the scrollbar the body reserves, and the width the thumb takes.
|
|
114
|
+
*
|
|
115
|
+
* Reserved permanently (`scrollbar-gutter: stable`) rather than appearing with
|
|
116
|
+
* the overflow: the panel's content is recomputed on every board change, so a
|
|
117
|
+
* gutter that came and went would shift the text sideways under the reader
|
|
118
|
+
* while they are reading it.
|
|
119
|
+
*/
|
|
120
|
+
const PANEL_SCROLLBAR_WIDTH = 8;
|
|
94
121
|
/**
|
|
95
122
|
* The host geometry, the layer and the panel's chrome — the half of the pattern
|
|
96
123
|
* that is CSS.
|
|
@@ -123,12 +150,27 @@ export const editorAnchoredPanelStyles = css `
|
|
|
123
150
|
pointer-events: none;
|
|
124
151
|
}
|
|
125
152
|
|
|
153
|
+
/*
|
|
154
|
+
The FRAME, and nothing that scrolls.
|
|
155
|
+
|
|
156
|
+
\`overflow: hidden\` rather than \`overflow-y: auto\`: the panel owns the
|
|
157
|
+
rounded corners, so it is the box that must clip — and a box that clips is
|
|
158
|
+
the only one whose children can be trusted not to paint over the arc. The
|
|
159
|
+
scrolling moved one level in, to \`.editor-anchored-panel-body\`, which is
|
|
160
|
+
what pins the header.
|
|
161
|
+
|
|
162
|
+
A column, because \`max-height\` has to be shared: the header takes what it
|
|
163
|
+
needs and the body takes the rest. The measured maximum
|
|
164
|
+
({@link EditorAnchoredPanel.anchorBox}) therefore still means what it says —
|
|
165
|
+
the whole panel, header included, never exceeds it.
|
|
166
|
+
*/
|
|
126
167
|
.editor-anchored-panel {
|
|
127
168
|
position: absolute;
|
|
128
169
|
box-sizing: border-box;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
overflow: hidden;
|
|
173
|
+
border-radius: ${unsafeCSS(PANEL_RADIUS)}px;
|
|
132
174
|
border: 1px solid var(--affine-border-color);
|
|
133
175
|
background: var(--affine-background-overlay-panel-color, #fff);
|
|
134
176
|
box-shadow: var(--affine-shadow-2);
|
|
@@ -138,6 +180,73 @@ export const editorAnchoredPanelStyles = css `
|
|
|
138
180
|
line-height: 1.4;
|
|
139
181
|
pointer-events: auto;
|
|
140
182
|
}
|
|
183
|
+
|
|
184
|
+
/*
|
|
185
|
+
The header stays. It is the sentence that says what the panel is about, and
|
|
186
|
+
a title that scrolls out of its own panel leaves the reader looking at a
|
|
187
|
+
list of values with nothing naming them.
|
|
188
|
+
|
|
189
|
+
\`flex: 0 0 auto\` so a long title wraps rather than being squeezed, and the
|
|
190
|
+
bottom border so the body visibly passes UNDER it instead of appearing to
|
|
191
|
+
end mid-air.
|
|
192
|
+
*/
|
|
193
|
+
.editor-anchored-panel-header {
|
|
194
|
+
flex: 0 0 auto;
|
|
195
|
+
padding: 12px ${unsafeCSS(PANEL_INLINE_PADDING)}px 8px;
|
|
196
|
+
border-bottom: 1px solid var(--affine-border-color);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/*
|
|
200
|
+
The one box that scrolls.
|
|
201
|
+
|
|
202
|
+
\`min-height: 0\` is belt-and-braces, not load-bearing: a flex child's
|
|
203
|
+
automatic minimum size is its content, but \`overflow-y: auto\` already
|
|
204
|
+
resets it to zero (CSS flexbox § 4.5) — the suite stays green with the
|
|
205
|
+
line removed (lead's mutation probe). Spelled out anyway so the shrink
|
|
206
|
+
survives the day someone moves the overflow off this box.
|
|
207
|
+
|
|
208
|
+
The right inset is split — \`padding-right\` plus the reserved gutter — so
|
|
209
|
+
the text sits the same distance from both edges while the scrollbar keeps a
|
|
210
|
+
lane of its own. The bottom inset is a MARGIN rather than padding: padding
|
|
211
|
+
is inside the scrolling box, so the scrollbar would run down to it and into
|
|
212
|
+
the bottom arc; a margin ends the scrolling box early and leaves the corner
|
|
213
|
+
to the frame.
|
|
214
|
+
*/
|
|
215
|
+
.editor-anchored-panel-body {
|
|
216
|
+
flex: 1 1 auto;
|
|
217
|
+
min-height: 0;
|
|
218
|
+
overflow-y: auto;
|
|
219
|
+
scrollbar-gutter: stable;
|
|
220
|
+
padding: 12px ${unsafeCSS(PANEL_INLINE_PADDING - PANEL_SCROLLBAR_WIDTH)}px 0
|
|
221
|
+
${unsafeCSS(PANEL_INLINE_PADDING)}px;
|
|
222
|
+
margin-bottom: 12px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/*
|
|
226
|
+
The house scrollbar: thin, invisible at rest, and only drawn while the
|
|
227
|
+
pointer is over the box it belongs to. Same values as \`table-block-css\`
|
|
228
|
+
and the data-view scrollers, so the editor has one scrollbar and not three.
|
|
229
|
+
*/
|
|
230
|
+
.editor-anchored-panel-body::-webkit-scrollbar {
|
|
231
|
+
width: ${unsafeCSS(PANEL_SCROLLBAR_WIDTH)}px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.editor-anchored-panel-body::-webkit-scrollbar-thumb {
|
|
235
|
+
border-radius: ${unsafeCSS(PANEL_SCROLLBAR_WIDTH / 2)}px;
|
|
236
|
+
background-color: transparent;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.editor-anchored-panel-body::-webkit-scrollbar-track {
|
|
240
|
+
background-color: transparent;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.editor-anchored-panel-body:hover::-webkit-scrollbar-thumb {
|
|
244
|
+
background-color: var(--affine-black-30);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.editor-anchored-panel-body:hover::-webkit-scrollbar-track {
|
|
248
|
+
background-color: var(--affine-hover-color);
|
|
249
|
+
}
|
|
141
250
|
`;
|
|
142
251
|
/**
|
|
143
252
|
* The base every editor-anchored info panel extends.
|
|
@@ -233,7 +342,19 @@ export class EditorAnchoredPanel extends WidgetComponent {
|
|
|
233
342
|
}
|
|
234
343
|
/**
|
|
235
344
|
* The panel's frame: the box, the dialog semantics and the pointer
|
|
236
|
-
* swallowing. The subclass passes its own
|
|
345
|
+
* swallowing. The subclass passes its own two halves and nothing else.
|
|
346
|
+
*
|
|
347
|
+
* ## Why the header is a parameter and not a line of the body
|
|
348
|
+
*
|
|
349
|
+
* Because the split is the whole fix. Both panels rendered their title as the
|
|
350
|
+
* first node of a body that was itself the scrolling box, so a reading long
|
|
351
|
+
* enough to scroll scrolled its own title away. Making the header an argument
|
|
352
|
+
* is what forces a consumer to say which of its markup stays put — a default
|
|
353
|
+
* would have quietly kept the old behaviour for whoever forgot.
|
|
354
|
+
*
|
|
355
|
+
* The header is where a panel puts its title and, if it has one, the actions
|
|
356
|
+
* that belong beside it: Map quality's close button rides there and stays
|
|
357
|
+
* reachable however far the checklist runs.
|
|
237
358
|
*
|
|
238
359
|
* Deliberately NOT `aria-modal`: it promises everything behind the dialog is
|
|
239
360
|
* inert, and these panels promise the opposite — the canvas stays usable
|
|
@@ -242,7 +363,11 @@ export class EditorAnchoredPanel extends WidgetComponent {
|
|
|
242
363
|
* screen reader, and trapping Tab from a host that is still interactive would
|
|
243
364
|
* be the library taking something that is not its to take.
|
|
244
365
|
*/
|
|
245
|
-
renderAnchoredPanel(options,
|
|
366
|
+
renderAnchoredPanel(options,
|
|
367
|
+
/** Pinned: the title, and whatever must stay beside it. */
|
|
368
|
+
header,
|
|
369
|
+
/** Scrolled: everything the panel has to say. */
|
|
370
|
+
body) {
|
|
246
371
|
const { left, width, bottom, maxHeight } = this.anchorBox();
|
|
247
372
|
return html `<div
|
|
248
373
|
class="editor-anchored-panel ${options.variant}"
|
|
@@ -261,7 +386,15 @@ export class EditorAnchoredPanel extends WidgetComponent {
|
|
|
261
386
|
@pointerup=${this.swallow}
|
|
262
387
|
@click=${this.swallow}
|
|
263
388
|
>
|
|
264
|
-
|
|
389
|
+
<div
|
|
390
|
+
class="editor-anchored-panel-header"
|
|
391
|
+
data-testid="anchored-panel-header"
|
|
392
|
+
>
|
|
393
|
+
${header}
|
|
394
|
+
</div>
|
|
395
|
+
<div class="editor-anchored-panel-body" data-testid="anchored-panel-body">
|
|
396
|
+
${body}
|
|
397
|
+
</div>
|
|
265
398
|
</div>`;
|
|
266
399
|
}
|
|
267
400
|
/**
|
|
@@ -149,14 +149,9 @@ let MapQualityWidget = (() => {
|
|
|
149
149
|
static { this.styles = [
|
|
150
150
|
editorAnchoredPanelStyles,
|
|
151
151
|
css `
|
|
152
|
-
/* The box, the layer
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
display: flex;
|
|
156
|
-
flex-direction: column;
|
|
157
|
-
gap: 12px;
|
|
158
|
-
}
|
|
159
|
-
|
|
152
|
+
/* The box, the layer, the anchoring AND the column of header over body
|
|
153
|
+
all come from the shared pattern now — what is left here is the two
|
|
154
|
+
rows this panel puts inside those two slots. */
|
|
160
155
|
.map-quality-head {
|
|
161
156
|
display: flex;
|
|
162
157
|
align-items: center;
|
|
@@ -449,20 +444,19 @@ let MapQualityWidget = (() => {
|
|
|
449
444
|
elementId: element.id,
|
|
450
445
|
label: title,
|
|
451
446
|
}, html `<div class="map-quality-head">
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
${this._renderNudges(element)}`);
|
|
447
|
+
<span>${title}</span>
|
|
448
|
+
<button
|
|
449
|
+
class="map-quality-close"
|
|
450
|
+
type="button"
|
|
451
|
+
data-testid="map-quality-close"
|
|
452
|
+
aria-label=${translateKey(this.std, 'com.labre.validation.map-quality.close', 'Close')}
|
|
453
|
+
@pointerdown=${this.swallow}
|
|
454
|
+
@pointerup=${this.swallow}
|
|
455
|
+
@click=${this._close}
|
|
456
|
+
>
|
|
457
|
+
×
|
|
458
|
+
</button>
|
|
459
|
+
</div>`, this._renderNudges(element));
|
|
466
460
|
}
|
|
467
461
|
};
|
|
468
462
|
})();
|
|
@@ -121,6 +121,13 @@ export declare class ReadingProposalWidget extends EditorAnchoredPanel {
|
|
|
121
121
|
* cannot choose a document, so with no picker there is nothing to offer and
|
|
122
122
|
* the affordance is HIDDEN rather than disabled — the `QuickSearchProvider`
|
|
123
123
|
* precedent, and the same rule `queryPivotProperties` follows.
|
|
124
|
+
*
|
|
125
|
+
* "Linked" is the TOLERANT question (`resolvePivotBinding`): the reading is
|
|
126
|
+
* resolved on the element carrying the role, but a host is free to have
|
|
127
|
+
* stamped the composite a plain click selects, and a component that is bound
|
|
128
|
+
* must never be reported as unlinked. Offering the link is the strict
|
|
129
|
+
* question by construction — the button only shows when nothing in the chain
|
|
130
|
+
* is bound, and it still stamps the role-carrying element.
|
|
124
131
|
*/
|
|
125
132
|
private _renderRecord;
|
|
126
133
|
/** The drift line: informative, never blocking, with one way to settle it. */
|
|
@@ -138,6 +145,17 @@ export declare class ReadingProposalWidget extends EditorAnchoredPanel {
|
|
|
138
145
|
* board, and the seam has no field for it: the drift line reports it so the
|
|
139
146
|
* user can settle it on whichever side is wrong, and the library does not
|
|
140
147
|
* invent a transport for a fact the host never agreed to receive.
|
|
148
|
+
*
|
|
149
|
+
* The patch is built on the element that CARRIES the binding, not on the one
|
|
150
|
+
* the reading was resolved for. They are the same element in every ordinary
|
|
151
|
+
* case; they differ only when the composite is what a host bound, and there
|
|
152
|
+
* the resolved element is the only honest choice. `elementId` is half of the
|
|
153
|
+
* occurrence key (`pivotDocId` + `elementId`) that the materiality publisher
|
|
154
|
+
* tracks and RETRACTS on: publishing under the role-carrying element's id,
|
|
155
|
+
* against a record it does not itself point at, would create an occurrence
|
|
156
|
+
* no watcher ever tracked and therefore none can ever retract — host data
|
|
157
|
+
* left attributed to an occurrence that does not exist, which is exactly the
|
|
158
|
+
* leak ADR 0006 § 4.3 forbids.
|
|
141
159
|
*/
|
|
142
160
|
private readonly _updateRecord;
|
|
143
161
|
render(): TemplateResult | typeof nothing;
|
|
@@ -34,7 +34,7 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
|
|
|
34
34
|
};
|
|
35
35
|
import { buildOccurrencePatch, getUniverseRegistry, publishOccurrenceMaterialities, translateKey, } from '../../affine-shared/services/index.js';
|
|
36
36
|
import { getRegisteredCommands, runCommand, WidgetViewExtension, } from '../../std/index.js';
|
|
37
|
-
import {
|
|
37
|
+
import { resolvePivotBinding } from '../../std/gfx/index.js';
|
|
38
38
|
import { effect } from '@preact/signals-core';
|
|
39
39
|
import { css, html, nothing } from 'lit';
|
|
40
40
|
import { state } from 'lit/decorators.js';
|
|
@@ -157,13 +157,27 @@ let ReadingProposalWidget = (() => {
|
|
|
157
157
|
* board, and the seam has no field for it: the drift line reports it so the
|
|
158
158
|
* user can settle it on whichever side is wrong, and the library does not
|
|
159
159
|
* invent a transport for a fact the host never agreed to receive.
|
|
160
|
+
*
|
|
161
|
+
* The patch is built on the element that CARRIES the binding, not on the one
|
|
162
|
+
* the reading was resolved for. They are the same element in every ordinary
|
|
163
|
+
* case; they differ only when the composite is what a host bound, and there
|
|
164
|
+
* the resolved element is the only honest choice. `elementId` is half of the
|
|
165
|
+
* occurrence key (`pivotDocId` + `elementId`) that the materiality publisher
|
|
166
|
+
* tracks and RETRACTS on: publishing under the role-carrying element's id,
|
|
167
|
+
* against a record it does not itself point at, would create an occurrence
|
|
168
|
+
* no watcher ever tracked and therefore none can ever retract — host data
|
|
169
|
+
* left attributed to an occurrence that does not exist, which is exactly the
|
|
170
|
+
* leak ADR 0006 § 4.3 forbids.
|
|
160
171
|
*/
|
|
161
172
|
this._updateRecord = (elementId, pivotDocId) => (event) => {
|
|
162
173
|
event.stopPropagation();
|
|
163
174
|
const element = this.gfx.surface?.getElementById(elementId);
|
|
164
175
|
if (!element)
|
|
165
176
|
return;
|
|
166
|
-
|
|
177
|
+
const bound = resolvePivotBinding(element);
|
|
178
|
+
if (!bound || bound.pivotDocId !== pivotDocId)
|
|
179
|
+
return;
|
|
180
|
+
publishOccurrenceMaterialities(this.std, buildOccurrencePatch(bound, pivotDocId));
|
|
167
181
|
// The disagreement has been answered; the next local change re-checks it
|
|
168
182
|
// from scratch, so nothing here has to remember it.
|
|
169
183
|
const manager = this._manager;
|
|
@@ -180,11 +194,14 @@ let ReadingProposalWidget = (() => {
|
|
|
180
194
|
static { this.styles = [
|
|
181
195
|
editorAnchoredPanelStyles,
|
|
182
196
|
css `
|
|
197
|
+
/* Pinned by the shared frame, which owns its padding; all this adds is
|
|
198
|
+
the weight that makes it read as a title. */
|
|
183
199
|
.reading-title {
|
|
184
200
|
font-weight: 600;
|
|
185
|
-
margin-bottom: 8px;
|
|
186
201
|
}
|
|
187
202
|
|
|
203
|
+
/* The first field sits directly under the pinned header, so the rule that
|
|
204
|
+
separates two fields would draw a second line against the header's. */
|
|
188
205
|
.reading-field + .reading-field {
|
|
189
206
|
margin-top: 10px;
|
|
190
207
|
padding-top: 10px;
|
|
@@ -484,13 +501,20 @@ let ReadingProposalWidget = (() => {
|
|
|
484
501
|
* cannot choose a document, so with no picker there is nothing to offer and
|
|
485
502
|
* the affordance is HIDDEN rather than disabled — the `QuickSearchProvider`
|
|
486
503
|
* precedent, and the same rule `queryPivotProperties` follows.
|
|
504
|
+
*
|
|
505
|
+
* "Linked" is the TOLERANT question (`resolvePivotBinding`): the reading is
|
|
506
|
+
* resolved on the element carrying the role, but a host is free to have
|
|
507
|
+
* stamped the composite a plain click selects, and a component that is bound
|
|
508
|
+
* must never be reported as unlinked. Offering the link is the strict
|
|
509
|
+
* question by construction — the button only shows when nothing in the chain
|
|
510
|
+
* is bound, and it still stamps the role-carrying element.
|
|
487
511
|
*/
|
|
488
512
|
_renderRecord(reading, writable) {
|
|
489
513
|
const surface = this.gfx.surface;
|
|
490
514
|
const element = surface?.getElementById(reading.elementId);
|
|
491
515
|
if (!element)
|
|
492
516
|
return nothing;
|
|
493
|
-
const bound =
|
|
517
|
+
const bound = resolvePivotBinding(element) !== undefined;
|
|
494
518
|
const picker = this.std.getOptional(PivotRecordPickerProvider);
|
|
495
519
|
return this._field('reading-record', translateKey(this.std, 'com.labre.reading.field.record', 'Record'), html `<div class="reading-value">
|
|
496
520
|
${bound
|
|
@@ -573,14 +597,13 @@ let ReadingProposalWidget = (() => {
|
|
|
573
597
|
elementId,
|
|
574
598
|
label: translateKey(this.std, 'com.labre.reading.panel.label', 'Proposed record'),
|
|
575
599
|
}, html `<div class="reading-title">
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
${writable ? this._renderDrift(reading) : nothing}`);
|
|
600
|
+
${translateKey(this.std, 'com.labre.reading.panel.title', 'What this map says about this component')}
|
|
601
|
+
</div>`, html `${this._renderNodeType(reading, profile)}
|
|
602
|
+
${this._renderNature(reading, profile, record, writable)}
|
|
603
|
+
${this._renderRelations(reading)} ${this._renderValueFlow(reading)}
|
|
604
|
+
${this._renderPhase(reading)} ${this._renderNaming(reading)}
|
|
605
|
+
${this._renderRecord(reading, writable)}
|
|
606
|
+
${writable ? this._renderDrift(reading) : nothing}`);
|
|
584
607
|
}
|
|
585
608
|
};
|
|
586
609
|
})();
|
|
@@ -364,6 +364,14 @@ export interface ReadingDrift {
|
|
|
364
364
|
* fields, a dangling record, a still-loading one — and the caller shows the
|
|
365
365
|
* local reading alone. That is the `queryPivotProperties` contract: "this
|
|
366
366
|
* editor has no host data layer" is not an error to put in front of a user.
|
|
367
|
+
*
|
|
368
|
+
* The binding is resolved through {@link resolvePivotBinding} rather than read
|
|
369
|
+
* off `element` directly: a business framework draws its artefacts as
|
|
370
|
+
* composites, and a host that stamped the GROUP a click selects — rather than
|
|
371
|
+
* the circle that carries the role — is still describing this component. The
|
|
372
|
+
* whole record side of the reading therefore starts from the RESOLVED binding,
|
|
373
|
+
* drift included, so a panel and a drift line can never disagree about which
|
|
374
|
+
* record they are talking about.
|
|
367
375
|
*/
|
|
368
376
|
export declare function readRecord(std: BlockStdScope, element: GfxPrimitiveElementModel, profile: ReadingProfile): RecordReading | undefined;
|
|
369
377
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getUniverseRegistry, hostLanguage, queryPivotProperties, } from '../../affine-shared/services/index.js';
|
|
2
2
|
import { createIdentifier } from '../../global/di/index.js';
|
|
3
3
|
import { LifeCycleWatcher, } from '../../std/index.js';
|
|
4
|
-
import { GfxControllerIdentifier, GfxPrimitiveElementModel,
|
|
4
|
+
import { GfxControllerIdentifier, GfxPrimitiveElementModel, readElementTags, resolvePivotBinding, roleIsA, } from '../../std/gfx/index.js';
|
|
5
5
|
import { effect, signal } from '@preact/signals-core';
|
|
6
6
|
// Straight at the two leaf modules rather than at the barrel, for the reason
|
|
7
7
|
// `validation.ts` states: `index.js` also re-exports the RENDERER, and a
|
|
@@ -458,14 +458,23 @@ function propertyStrings(snapshot, key) {
|
|
|
458
458
|
* fields, a dangling record, a still-loading one — and the caller shows the
|
|
459
459
|
* local reading alone. That is the `queryPivotProperties` contract: "this
|
|
460
460
|
* editor has no host data layer" is not an error to put in front of a user.
|
|
461
|
+
*
|
|
462
|
+
* The binding is resolved through {@link resolvePivotBinding} rather than read
|
|
463
|
+
* off `element` directly: a business framework draws its artefacts as
|
|
464
|
+
* composites, and a host that stamped the GROUP a click selects — rather than
|
|
465
|
+
* the circle that carries the role — is still describing this component. The
|
|
466
|
+
* whole record side of the reading therefore starts from the RESOLVED binding,
|
|
467
|
+
* drift included, so a panel and a drift line can never disagree about which
|
|
468
|
+
* record they are talking about.
|
|
461
469
|
*/
|
|
462
470
|
export function readRecord(std, element, profile) {
|
|
463
|
-
|
|
471
|
+
const bound = resolvePivotBinding(element);
|
|
472
|
+
if (!bound)
|
|
464
473
|
return undefined;
|
|
465
474
|
const keys = profile.recordKeys;
|
|
466
475
|
if (!keys?.nature && !keys?.phase)
|
|
467
476
|
return undefined;
|
|
468
|
-
const state = queryPivotProperties(std,
|
|
477
|
+
const state = queryPivotProperties(std, bound.pivotDocId);
|
|
469
478
|
if (!state)
|
|
470
479
|
return undefined;
|
|
471
480
|
const current = state.value;
|
|
@@ -482,7 +491,7 @@ export function readRecord(std, element, profile) {
|
|
|
482
491
|
? resolveRecordNature(getUniverseRegistry(std).tag(tagId), raw)
|
|
483
492
|
: { resolved: [], unknown: [] };
|
|
484
493
|
return {
|
|
485
|
-
pivotDocId:
|
|
494
|
+
pivotDocId: bound.pivotDocId,
|
|
486
495
|
...(resolved.length ? { nature: resolved } : {}),
|
|
487
496
|
...(unknown.length ? { unknownNature: unknown } : {}),
|
|
488
497
|
...(phase.length ? { phase: phase[0] } : {}),
|
|
@@ -732,8 +741,10 @@ export class ReadingManager extends LifeCycleWatcher {
|
|
|
732
741
|
if (!surface || !element)
|
|
733
742
|
return;
|
|
734
743
|
// Only a LINKED element can drift: without a record there is nothing to
|
|
735
|
-
// disagree with, and the reading is just a reading.
|
|
736
|
-
|
|
744
|
+
// disagree with, and the reading is just a reading. "Linked" is the same
|
|
745
|
+
// tolerant question the panel asks — a binding carried by the composite is
|
|
746
|
+
// this component's binding (`resolvePivotBinding`).
|
|
747
|
+
if (!resolvePivotBinding(element)) {
|
|
737
748
|
if (this.drift$.value?.elementId === elementId)
|
|
738
749
|
this.drift$.value = null;
|
|
739
750
|
return;
|
|
@@ -48,7 +48,14 @@ const TAG_SET_ID = 'tag.set';
|
|
|
48
48
|
* ids already written on an element go unread until the flag comes back, which
|
|
49
49
|
* costs nothing, because they were never needed to load or paint anything.
|
|
50
50
|
*/
|
|
51
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* The tick on a selected value, drawn inline like the neighbouring modules'.
|
|
53
|
+
*
|
|
54
|
+
* Inline because this package does not depend on `@blocksuite/icons`, and to
|
|
55
|
+
* the RIGHT of the label, rendered only when the value is on: `data-option` on
|
|
56
|
+
* the row gives it the house geometry and the primary colour (`menu-button.ts`),
|
|
57
|
+
* the same one the Regular/Semibold menu uses.
|
|
58
|
+
*/
|
|
52
59
|
const CheckIcon = html `<svg
|
|
53
60
|
width="20"
|
|
54
61
|
height="20"
|
|
@@ -188,6 +195,7 @@ function renderSection(ctx, target, def) {
|
|
|
188
195
|
const on = selected.includes(value.id);
|
|
189
196
|
const valueLabel = value.label || value.id;
|
|
190
197
|
return html `<editor-menu-action
|
|
198
|
+
data-option
|
|
191
199
|
data-testid="element-tag-option"
|
|
192
200
|
data-tag-id=${def.id}
|
|
193
201
|
data-value-id=${value.id}
|
|
@@ -196,8 +204,8 @@ function renderSection(ctx, target, def) {
|
|
|
196
204
|
aria-pressed=${on}
|
|
197
205
|
@click=${() => toggleValue(ctx, target, def, value.id)}
|
|
198
206
|
>
|
|
199
|
-
${on ? CheckIcon : html `<span style="width: 20px;"></span>`}
|
|
200
207
|
<span class="label">${valueLabel}</span>
|
|
208
|
+
${on ? CheckIcon : nothing}
|
|
201
209
|
</editor-menu-action>`;
|
|
202
210
|
});
|
|
203
211
|
return html `<div
|
|
@@ -9,6 +9,9 @@ import { ValidationManager } from './validation.js';
|
|
|
9
9
|
* `@blocksuite/icons`, which this package does not depend on and which is not
|
|
10
10
|
* worth a new dependency for one glyph — the same call the framework toolbars
|
|
11
11
|
* already make for their own icons.
|
|
12
|
+
*
|
|
13
|
+
* It sits at the END of the row and only when the profile is in force:
|
|
14
|
+
* `data-option` gives the row the native geometry and the primary colour.
|
|
12
15
|
*/
|
|
13
16
|
const CheckIcon = html `<svg
|
|
14
17
|
width="20"
|
|
@@ -136,6 +139,10 @@ function renderSection(label, testid, items) {
|
|
|
136
139
|
function renderMapQualitySection(ctx, element) {
|
|
137
140
|
const label = translateKey(ctx.std, 'com.labre.validation.map-quality.open', 'Map quality…');
|
|
138
141
|
return renderSection(translateKey(ctx.std, 'com.labre.validation.map-quality.section', 'Map quality'), 'validation-map-quality-section', [
|
|
142
|
+
// No `data-option`, and no spacer: this row OPENS something, it is not
|
|
143
|
+
// one of N choices. It held a 20 px gutter only to line its label up with
|
|
144
|
+
// the profile rows above, whose tick used to sit on the left; the tick
|
|
145
|
+
// has moved right, so the gutter is gone from both.
|
|
139
146
|
html `<editor-menu-action
|
|
140
147
|
data-testid="validation-map-quality-open"
|
|
141
148
|
aria-label=${label}
|
|
@@ -143,7 +150,6 @@ function renderMapQualitySection(ctx, element) {
|
|
|
143
150
|
ctx.std.getOptional(ValidationManager)?.openMapQuality(element);
|
|
144
151
|
}}
|
|
145
152
|
>
|
|
146
|
-
<span style="width: 20px;"></span>
|
|
147
153
|
<span class="label">${label}</span>
|
|
148
154
|
</editor-menu-action>`,
|
|
149
155
|
]);
|
|
@@ -159,6 +165,7 @@ function renderSections(ctx, target, quality) {
|
|
|
159
165
|
const options = profiles.map(profile => {
|
|
160
166
|
const selected = profile.id === active?.id;
|
|
161
167
|
return html `<editor-menu-action
|
|
168
|
+
data-option
|
|
162
169
|
data-testid="validation-profile-option"
|
|
163
170
|
data-profile-id=${profile.id}
|
|
164
171
|
data-selected=${selected ? 'true' : nothing}
|
|
@@ -166,10 +173,10 @@ function renderSections(ctx, target, quality) {
|
|
|
166
173
|
aria-pressed=${selected}
|
|
167
174
|
@click=${() => pickProfile(ctx, element, profile)}
|
|
168
175
|
>
|
|
169
|
-
${selected ? CheckIcon : html `<span style="width: 20px;"></span>`}
|
|
170
176
|
<span class="label"
|
|
171
177
|
>${translateKey(ctx.std, profile.labelKey, profile.fallback)}</span
|
|
172
178
|
>
|
|
179
|
+
${selected ? CheckIcon : nothing}
|
|
173
180
|
</editor-menu-action>`;
|
|
174
181
|
});
|
|
175
182
|
sections.push(renderSection(translateKey(ctx.std, 'com.labre.validation.profile.section', 'Profile'), 'validation-profile-section', options));
|
|
@@ -382,12 +382,13 @@ export interface BackgroundSurfaceDef {
|
|
|
382
382
|
/**
|
|
383
383
|
* A filled strip of FIXED width, painted over one margin of the card.
|
|
384
384
|
*
|
|
385
|
-
* The
|
|
386
|
-
* ({@link BackgroundGeometry.margin})
|
|
385
|
+
* The thickness of a band IS the margin it covers
|
|
386
|
+
* ({@link BackgroundGeometry.margin}) — `margin.left` for a `'left'` band,
|
|
387
|
+
* `margin.top` for a `'top'` one — never a second number: the margin is
|
|
387
388
|
* already the room the plot gives up to the furniture, and letting a
|
|
388
|
-
* declaration state a different
|
|
389
|
-
* one day will not. An element
|
|
390
|
-
* the element, which is the only case where the two can disagree.
|
|
389
|
+
* declaration state a different thickness would be two numbers that must agree
|
|
390
|
+
* and one day will not. An element smaller than its own margin clamps the band
|
|
391
|
+
* to the element, which is the only case where the two can disagree.
|
|
391
392
|
*
|
|
392
393
|
* A band is part of the CARD, not a layer over it: it is painted over the
|
|
393
394
|
* card's fill and under its border, so the frame keeps outlining the whole
|
|
@@ -395,16 +396,18 @@ export interface BackgroundSurfaceDef {
|
|
|
395
396
|
* them, and anchored in plot ratios — the plot edge is the band's inner edge,
|
|
396
397
|
* so `x: 0` with a negative `dx` walks back INTO the band.
|
|
397
398
|
*
|
|
398
|
-
* Named
|
|
399
|
+
* Named consumers: the BPMN pool, a lane whose participant name is written up a
|
|
399
400
|
* filled band along its left edge, with a {@link divider} between that band and
|
|
400
|
-
* the flow area
|
|
401
|
+
* the flow area; and the C4 board, whose diagram title is written across a
|
|
402
|
+
* header strip along its top edge.
|
|
401
403
|
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
404
|
+
* `'left'` and `'top'` — the two a framework has actually asked for. The union
|
|
405
|
+
* grows the day one asks for a right or a bottom band, not in anticipation of
|
|
406
|
+
* one, which is the same philosophy the wash direction follows
|
|
407
|
+
* ({@link BackgroundWashDef.stops}).
|
|
405
408
|
*/
|
|
406
409
|
export interface BackgroundSideBandDef {
|
|
407
|
-
side: 'left';
|
|
410
|
+
side: 'left' | 'top';
|
|
408
411
|
/** Tint over the whole strip. Absent means the card shows through. */
|
|
409
412
|
fill?: BackgroundColor;
|
|
410
413
|
/** The line between the band and the plot. Absent means none. */
|
|
@@ -162,15 +162,19 @@ export function createFrameworkBackgroundRenderer(def) {
|
|
|
162
162
|
// down to the operation.
|
|
163
163
|
const bands = def.chrome?.sideBands;
|
|
164
164
|
if (bands?.length) {
|
|
165
|
-
// The band IS the margin strip, clamped to an element narrower than its
|
|
166
|
-
// own margin — the one case where the two can disagree.
|
|
167
|
-
const bandWidth = Math.min(x0, w);
|
|
168
165
|
for (const band of bands) {
|
|
169
|
-
|
|
166
|
+
const top = band.side === 'top';
|
|
167
|
+
// The band IS the margin strip it covers, clamped to an element smaller
|
|
168
|
+
// than its own margin — the one case where the two can disagree.
|
|
169
|
+
const thickness = top ? Math.min(y0, h) : Math.min(x0, w);
|
|
170
|
+
if (thickness <= 0)
|
|
170
171
|
continue;
|
|
171
172
|
if (band.fill) {
|
|
172
173
|
ctx.fillStyle = color(band.fill);
|
|
173
|
-
|
|
174
|
+
if (top)
|
|
175
|
+
ctx.fillRect(0, 0, w, thickness);
|
|
176
|
+
else
|
|
177
|
+
ctx.fillRect(0, 0, thickness, h);
|
|
174
178
|
}
|
|
175
179
|
const divider = band.divider;
|
|
176
180
|
if (divider) {
|
|
@@ -178,8 +182,11 @@ export function createFrameworkBackgroundRenderer(def) {
|
|
|
178
182
|
ctx.lineWidth = divider.width;
|
|
179
183
|
if (divider.dash?.length)
|
|
180
184
|
ctx.setLineDash([...divider.dash]);
|
|
181
|
-
//
|
|
182
|
-
|
|
185
|
+
// The full span: the strip a margin gives up runs the whole side.
|
|
186
|
+
if (top)
|
|
187
|
+
line(0, thickness, w, thickness);
|
|
188
|
+
else
|
|
189
|
+
line(thickness, 0, thickness, h);
|
|
183
190
|
if (divider.dash?.length)
|
|
184
191
|
ctx.setLineDash([]);
|
|
185
192
|
}
|