@issuegraph/editor 0.10.0 → 0.23.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/README.md +38 -7
- package/dist/a11y/baseline.d.ts.map +1 -1
- package/dist/a11y/baseline.js +4 -0
- package/dist/a11y/baseline.js.map +1 -1
- package/dist/audit/findings.d.ts +107 -1
- package/dist/audit/findings.d.ts.map +1 -1
- package/dist/audit/findings.js +114 -7
- package/dist/audit/findings.js.map +1 -1
- package/dist/audit/panel.d.ts +277 -0
- package/dist/audit/panel.d.ts.map +1 -0
- package/dist/audit/panel.js +462 -0
- package/dist/audit/panel.js.map +1 -0
- package/dist/audit/refused.d.ts +99 -0
- package/dist/audit/refused.d.ts.map +1 -0
- package/dist/audit/refused.js +276 -0
- package/dist/audit/refused.js.map +1 -0
- package/dist/audit/styles.d.ts +1 -1
- package/dist/audit/styles.d.ts.map +1 -1
- package/dist/audit/styles.js +428 -0
- package/dist/audit/styles.js.map +1 -1
- package/dist/audit/surface.d.ts +54 -12
- package/dist/audit/surface.d.ts.map +1 -1
- package/dist/audit/surface.js +54 -14
- package/dist/audit/surface.js.map +1 -1
- package/dist/create/draft.d.ts +19 -0
- package/dist/create/draft.d.ts.map +1 -1
- package/dist/create/draft.js +21 -0
- package/dist/create/draft.js.map +1 -1
- package/dist/create/keys.d.ts +68 -8
- package/dist/create/keys.d.ts.map +1 -1
- package/dist/create/keys.js +43 -5
- package/dist/create/keys.js.map +1 -1
- package/dist/firstpass/render.d.ts.map +1 -1
- package/dist/firstpass/render.js +10 -9
- package/dist/firstpass/render.js.map +1 -1
- package/dist/index.d.ts +40 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -13
- package/dist/index.js.map +1 -1
- package/dist/overlay/render.d.ts +11 -3
- package/dist/overlay/render.d.ts.map +1 -1
- package/dist/overlay/render.js +24 -7
- package/dist/overlay/render.js.map +1 -1
- package/dist/overlay/request.d.ts +71 -0
- package/dist/overlay/request.d.ts.map +1 -0
- package/dist/overlay/request.js +129 -0
- package/dist/overlay/request.js.map +1 -0
- package/dist/overlay/styles.d.ts +1 -1
- package/dist/overlay/styles.d.ts.map +1 -1
- package/dist/overlay/styles.js +38 -7
- package/dist/overlay/styles.js.map +1 -1
- package/dist/reevaluate/parts.d.ts +130 -0
- package/dist/reevaluate/parts.d.ts.map +1 -0
- package/dist/reevaluate/parts.js +260 -0
- package/dist/reevaluate/parts.js.map +1 -0
- package/dist/reevaluate/render.d.ts +18 -11
- package/dist/reevaluate/render.d.ts.map +1 -1
- package/dist/reevaluate/render.js +21 -135
- package/dist/reevaluate/render.js.map +1 -1
- package/dist/reevaluate/styles.d.ts +1 -1
- package/dist/reevaluate/styles.d.ts.map +1 -1
- package/dist/reevaluate/styles.js +26 -0
- package/dist/reevaluate/styles.js.map +1 -1
- package/dist/reevaluate/view.d.ts +16 -1
- package/dist/reevaluate/view.d.ts.map +1 -1
- package/dist/reevaluate/view.js +21 -1
- package/dist/reevaluate/view.js.map +1 -1
- package/dist/reevaluate/words.d.ts +7 -0
- package/dist/reevaluate/words.d.ts.map +1 -1
- package/dist/scale/ladder.d.ts +62 -6
- package/dist/scale/ladder.d.ts.map +1 -1
- package/dist/scale/ladder.js +127 -16
- package/dist/scale/ladder.js.map +1 -1
- package/dist/scale/render.d.ts +44 -0
- package/dist/scale/render.d.ts.map +1 -1
- package/dist/scale/render.js +223 -30
- package/dist/scale/render.js.map +1 -1
- package/dist/scale/styles.d.ts +11 -5
- package/dist/scale/styles.d.ts.map +1 -1
- package/dist/scale/styles.js +185 -4
- package/dist/scale/styles.js.map +1 -1
- package/dist/workspace/host.d.ts +83 -1
- package/dist/workspace/host.d.ts.map +1 -1
- package/dist/workspace/host.js +197 -4
- package/dist/workspace/host.js.map +1 -1
- package/dist/workspace/inspector.d.ts +33 -7
- package/dist/workspace/inspector.d.ts.map +1 -1
- package/dist/workspace/inspector.js +15 -0
- package/dist/workspace/inspector.js.map +1 -1
- package/dist/workspace/mount.d.ts +23 -1
- package/dist/workspace/mount.d.ts.map +1 -1
- package/dist/workspace/mount.js +605 -20
- package/dist/workspace/mount.js.map +1 -1
- package/dist/workspace/rail.d.ts +5 -2
- package/dist/workspace/rail.d.ts.map +1 -1
- package/dist/workspace/rail.js.map +1 -1
- package/dist/workspace/render.d.ts +390 -3
- package/dist/workspace/render.d.ts.map +1 -1
- package/dist/workspace/render.js +924 -133
- package/dist/workspace/render.js.map +1 -1
- package/dist/workspace/selection.d.ts +11 -0
- package/dist/workspace/selection.d.ts.map +1 -1
- package/dist/workspace/selection.js +10 -0
- package/dist/workspace/selection.js.map +1 -1
- package/dist/workspace/skeleton.d.ts +33 -0
- package/dist/workspace/skeleton.d.ts.map +1 -0
- package/dist/workspace/skeleton.js +56 -0
- package/dist/workspace/skeleton.js.map +1 -0
- package/dist/workspace/styles.d.ts +1 -1
- package/dist/workspace/styles.d.ts.map +1 -1
- package/dist/workspace/styles.js +931 -19
- package/dist/workspace/styles.js.map +1 -1
- package/package.json +4 -4
package/dist/workspace/render.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* |-----------|-------------------------------------------------------|
|
|
10
10
|
* | header | `auditOverlay` + `renderAuditHeader` |
|
|
11
11
|
* | rail | `renderViewer(…, { projection: 'linear' })`, windowed |
|
|
12
|
-
* | canvas | `renderScaleLadder`
|
|
12
|
+
* | canvas | `canvasToolbar` + `renderScaleLadder` |
|
|
13
13
|
* | inspector | `inspectorView`, rendered here |
|
|
14
14
|
*
|
|
15
15
|
* ## The audit left-bar is applied to the rail's SPEC, never to its markup
|
|
@@ -49,10 +49,16 @@
|
|
|
49
49
|
* `styles.ts` writes structure against its custom properties.
|
|
50
50
|
*/
|
|
51
51
|
import { KEY_ATTRIBUTE, element, glyphAndLabel, hiddenGlyph, identity, labelFrom, normalizeDocument, renderMarkup, provenanceClause, renderViewer, resolveTheme, themeCss, treatmentFor, viewerStylesheet, } from '@issuegraph/viewer';
|
|
52
|
+
import { edgeIdentity } from '@issuegraph/core';
|
|
53
|
+
import { renderAuditPanel } from "../audit/panel.js";
|
|
54
|
+
import { renderEncodingRefusedBlock } from "../audit/refused.js";
|
|
52
55
|
import { auditStylesheet } from "../audit/styles.js";
|
|
53
56
|
import { IDLE_CREATE_DRAFT } from "../create/draft.js";
|
|
54
|
-
import { KIND_KEYS } from "../create/keys.js";
|
|
57
|
+
import { KIND_KEYS, RELATE_KEY } from "../create/keys.js";
|
|
55
58
|
import { OVERLAY_TREATMENTS, treatmentForState } from "../overlay/grammar.js";
|
|
59
|
+
import { DELTA_ATTRIBUTE, chipSpec, deltaKind, summarySpec, textOf } from "../reevaluate/parts.js";
|
|
60
|
+
import { reevaluateStylesheet } from "../reevaluate/styles.js";
|
|
61
|
+
import { reevaluateView } from "../reevaluate/view.js";
|
|
56
62
|
import { edgeOverlayStylesheet } from "../overlay/styles.js";
|
|
57
63
|
import { AUDIT_SEVERITY_ATTRIBUTE, auditFilterKeeps, auditOverlay, heaviestRow, renderAuditHeader, } from "../audit/surface.js";
|
|
58
64
|
import { INITIAL_SCALE_STATE } from "../scale/commands.js";
|
|
@@ -75,6 +81,175 @@ export const ZONES = Object.freeze(['header', 'rail', 'canvas', 'inspector']);
|
|
|
75
81
|
function zone(name, inner) {
|
|
76
82
|
return `<section class="ig-zone" data-zone="${name}">${inner}</section>`;
|
|
77
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* §17f's canvas caption — what this canvas is drawing, and out of how much.
|
|
86
|
+
*
|
|
87
|
+
* THE GAP IS THE `direct` TIER, AND ONLY IT. The zone is not silent in general:
|
|
88
|
+
* `renderScaleLadder` appends the ladder's own chrome after the graph, so a
|
|
89
|
+
* focused canvas already carries a "return to every component" button, a
|
|
90
|
+
* declining one already prints a counted refusal sentence, and the isolated
|
|
91
|
+
* chip and the search are already there. What none of them states is the RATIO
|
|
92
|
+
* on the one tier where the canvas succeeds — it narrows to a component, draws
|
|
93
|
+
* it, and says nothing about the rest — so a canvas showing 6 of 312 issues
|
|
94
|
+
* read exactly like a backlog that has 6.
|
|
95
|
+
*
|
|
96
|
+
* SO THE STATEMENT IS SCOPED TO THAT TIER, deliberately. Above the node budget
|
|
97
|
+
* `renderScaleLadder` draws NO graph at all — the canvas is `null` and the
|
|
98
|
+
* refusal takes its place — and a caption reading "1500 out of 2000 drawn here"
|
|
99
|
+
* over an empty canvas would state a thing the surface does not do. That is the
|
|
100
|
+
* defect {@link CanvasWords} refuses on the frame's own `1 hop`, and refusing it
|
|
101
|
+
* there while committing it here would be worse than not drawing the row.
|
|
102
|
+
*
|
|
103
|
+
* THE NUMBERS ARE THE LADDER'S AND THE DOCUMENT'S, ON ONE BASIS. `nodeCount` is
|
|
104
|
+
* "the nodes the canvas would draw", the same value the refusal is computed
|
|
105
|
+
* from, so the caption cannot disagree with the ladder beneath it. The total is
|
|
106
|
+
* the NORMALIZED document's — the one every zone in this function derives from —
|
|
107
|
+
* never the raw input, whose counts no zone drew, and never `ladder.canvas`,
|
|
108
|
+
* which would print `6 out of 6`.
|
|
109
|
+
*
|
|
110
|
+
* THE COUNT EXCLUDES ISSUES WITH NO RELATIONSHIP, and the surface says so
|
|
111
|
+
* elsewhere in its own words — at the FOOT OF THE RAIL where §17a puts it
|
|
112
|
+
* (`railFooter`), or, on a host that has not worded that row, in the ladder's
|
|
113
|
+
* own chip directly beneath this caption. This sentence read "the chip below"
|
|
114
|
+
* until the rail grew the footer, and the count is now in exactly one of those
|
|
115
|
+
* two places rather than reliably in the nearer one. Nothing else in this
|
|
116
|
+
* comment moves with it: the argument below is about the two NUMBERS, not about
|
|
117
|
+
* where the second one is drawn. UNFOCUSED the two are complementary and do sum to the total:
|
|
118
|
+
* `nodeCount` is every issue carrying a relationship and the chip is every issue
|
|
119
|
+
* carrying none. FOCUSED they do not, and an earlier draft of this comment said
|
|
120
|
+
* they did — `nodeCount` is then ONE component's members, so the shortfall is
|
|
121
|
+
* the chip's population PLUS every other component. What holds either way, and
|
|
122
|
+
* what the row actually relies on, is that the two count DISJOINT sets: they can
|
|
123
|
+
* never contradict, whatever they leave between them. So the row does not
|
|
124
|
+
* restate the chip, and no third number may be derived by subtracting one from
|
|
125
|
+
* the other.
|
|
126
|
+
*/
|
|
127
|
+
function canvasToolbar(ladder, document, words) {
|
|
128
|
+
if (words === undefined)
|
|
129
|
+
return null;
|
|
130
|
+
const focused = ladder.focus;
|
|
131
|
+
const statement = ladder.tier !== 'direct'
|
|
132
|
+
? null
|
|
133
|
+
: element('p', { class: 'ig-canvas-caption' }, [
|
|
134
|
+
// NO CLAUSE AT ALL WHEN NOTHING IS FOCUSED, rather than the label with
|
|
135
|
+
// an empty value after it. An unfocused canvas is drawing every
|
|
136
|
+
// component, which is a true thing the counts already say.
|
|
137
|
+
focused === null
|
|
138
|
+
? null
|
|
139
|
+
: element('span', { class: 'ig-canvas-focus' }, [
|
|
140
|
+
element('span', {}, [words.focus]),
|
|
141
|
+
// THE PUNCTUATION IS THE PACKAGE'S, AND IT HAS TO ACTUALLY BE
|
|
142
|
+
// DRAWN. `CanvasWords.focus` tells a host to omit the colon
|
|
143
|
+
// because this draws it; an earlier revision made that promise
|
|
144
|
+
// and then emitted only a space, so a host that obeyed the
|
|
145
|
+
// contract got `focus #512`. The interpunct after the key is
|
|
146
|
+
// the frame's own typography for the same reason.
|
|
147
|
+
': ',
|
|
148
|
+
// NOT `identity()`, AND NOT BECAUSE OF THE URL. That helper is
|
|
149
|
+
// right about who owns a tracker's link shape — but it renders
|
|
150
|
+
// an ANCHOR when the host supplied a url, and an anchor here is
|
|
151
|
+
// focusable inside a region that is replaced wholesale on every
|
|
152
|
+
// mounted redraw. `focusedKey()` and `commandFocusToken()` both
|
|
153
|
+
// read null for it, so the restore falls through to the first
|
|
154
|
+
// rail row and `WorkspaceHandle.update`'s promise to keep focus
|
|
155
|
+
// is broken by a caption. The caption is a READ-OUT: the canvas
|
|
156
|
+
// below it already draws this issue as a focusable node, and the
|
|
157
|
+
// rail already links it. So the key is drawn as text, which
|
|
158
|
+
// invents no URL and takes no focus.
|
|
159
|
+
element('span', { class: 'ig-id' }, [focused]),
|
|
160
|
+
' · ',
|
|
161
|
+
]),
|
|
162
|
+
element('span', { class: 'ig-canvas-shown' }, [
|
|
163
|
+
element('span', { class: 'ig-canvas-count' }, [String(ladder.nodeCount)]),
|
|
164
|
+
' ',
|
|
165
|
+
element('span', {}, [words.of]),
|
|
166
|
+
' ',
|
|
167
|
+
element('span', { class: 'ig-canvas-count' }, [String(document.issues.length)]),
|
|
168
|
+
' ',
|
|
169
|
+
element('span', {}, [words.shown]),
|
|
170
|
+
]),
|
|
171
|
+
]);
|
|
172
|
+
// A STATE, NOT A CONTROL, AND NOT A ROLE. It carries text, so it is announced
|
|
173
|
+
// in reading order on its own; `role="img"` with an `aria-label` — the shape
|
|
174
|
+
// `station()` uses — is for an indicator with NO text, and giving one to this
|
|
175
|
+
// would replace the host's word with a copy of itself. A `button` would be
|
|
176
|
+
// the dead control `headerControls` refuses to draw.
|
|
177
|
+
const pill = words.editMode === undefined
|
|
178
|
+
? null
|
|
179
|
+
: element('span', { class: 'ig-edit-mode' }, [words.editMode]);
|
|
180
|
+
// NOTHING TO SAY, NO ROW. Both halves are conditional and independently so,
|
|
181
|
+
// and an empty bordered band above the canvas is chrome that carries no fact.
|
|
182
|
+
if (statement === null && pill === null)
|
|
183
|
+
return null;
|
|
184
|
+
return element('div', { class: 'ig-canvas-toolbar' }, [statement, pill]);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* §17a's rail footer — the isolated count and the control that opens it.
|
|
188
|
+
*
|
|
189
|
+
* THE FACT IS THE DOCUMENT'S, NOT THE CANVAS'S. `ladder.isolated` is
|
|
190
|
+
* `document.issues` minus everything in a component, so it is a property of the
|
|
191
|
+
* backlog rather than of how far the graph narrowed. Frame 17a draws it at the
|
|
192
|
+
* foot of the order rail and gives the canvas toolbar the projection toggle,
|
|
193
|
+
* the focus statement and the edit-mode pill instead — so this is where the
|
|
194
|
+
* count belongs, and `renderScaleLadder` is told not to draw its own.
|
|
195
|
+
*
|
|
196
|
+
* THE COUNT AND THE CONTROL ONLY — THE LIST IS DRAWN IN THE CANVAS. Two reasons
|
|
197
|
+
* it cannot live in this zone, and the second is the load-bearing one. The
|
|
198
|
+
* footer is sticky, and an element taller than its scrollport cannot stick: the
|
|
199
|
+
* browser clamps it, so a list inside the footer would lose the pin exactly as
|
|
200
|
+
* it grew. And the rail is VIRTUALIZED — `railRowAt` turns a scroll offset into
|
|
201
|
+
* a row index by `floor((scrollTop - chrome) / pitch)`, and `railSpacer` stands
|
|
202
|
+
* in for the rows outside the window on that same fixed pitch — so content of
|
|
203
|
+
* arbitrary height anywhere in this scroll track makes every offset beneath it
|
|
204
|
+
* name the wrong row, and the window is then re-cut against a geometry that no
|
|
205
|
+
* longer holds. `renderScaleLadder` keeps drawing the list in the canvas zone,
|
|
206
|
+
* which has no such arithmetic; only the control moves here.
|
|
207
|
+
*
|
|
208
|
+
* NOTHING TO SAY, NO ROW — the rule `canvasToolbar` follows and `isolatedSpec`
|
|
209
|
+
* follows before it. With no words there is no footer, and with no isolated
|
|
210
|
+
* issues the count is not a fact anyone asked for.
|
|
211
|
+
*/
|
|
212
|
+
function railFooter(isolated, words, listId) {
|
|
213
|
+
if (words === undefined)
|
|
214
|
+
return null;
|
|
215
|
+
if (isolated.count === 0)
|
|
216
|
+
return null;
|
|
217
|
+
return element('div', { class: 'ig-rail-footer' }, [
|
|
218
|
+
// NO CLASS ON THE WRAPPER. It needs no rule of its own — the footer sets the
|
|
219
|
+
// muted body colour and `.ig-rail-count` lifts the number out of it — and a
|
|
220
|
+
// class with no rule is exactly what `styles.test.ts`'s second direction
|
|
221
|
+
// refuses.
|
|
222
|
+
element('span', {}, [
|
|
223
|
+
element('span', { class: 'ig-rail-count' }, [String(isolated.count)]),
|
|
224
|
+
' ',
|
|
225
|
+
element('span', {}, [words.isolated]),
|
|
226
|
+
]),
|
|
227
|
+
// A REAL BUTTON, NOT THE FRAME'S `cursor:pointer` SPAN. It is a control, and
|
|
228
|
+
// a span that looks pressable is the dead control `headerControls` refuses
|
|
229
|
+
// to draw; the frame's span is a mock's shorthand for one.
|
|
230
|
+
element('button', {
|
|
231
|
+
type: 'button',
|
|
232
|
+
class: 'ig-rail-isolated-toggle',
|
|
233
|
+
'aria-expanded': isolated.open ? 'true' : 'false',
|
|
234
|
+
// NAMES WHAT IT OPENS, because what it opens is not beside it. This
|
|
235
|
+
// toggle is in the rail and its list is drawn by the ladder in the
|
|
236
|
+
// canvas, so `aria-expanded` alone tells a screen-reader user that
|
|
237
|
+
// something opened and nothing about where — the one thing the adjacent
|
|
238
|
+
// disclosure it replaced never had to say. `scrollIntoView` moves the
|
|
239
|
+
// visual viewport and not accessibility focus, so it does not answer
|
|
240
|
+
// this either.
|
|
241
|
+
//
|
|
242
|
+
// ONLY WHILE THE LIST EXISTS, AND ONLY WITH AN ID TO NAME. A shut
|
|
243
|
+
// disclosure renders no list, so naming one would be a dangling
|
|
244
|
+
// reference the a11y baseline refuses — and there would be nothing to
|
|
245
|
+
// reach. With no `surfaceId` there is no value that would be unique on
|
|
246
|
+
// the host's page, and a guessed one is the collision `searchSpec`
|
|
247
|
+
// avoids.
|
|
248
|
+
...(isolated.open && listId !== undefined ? { 'aria-controls': listId } : {}),
|
|
249
|
+
'data-ig-command': isolated.open ? 'close-isolated' : 'open-isolated',
|
|
250
|
+
}, [isolated.open ? words.hide : words.show]),
|
|
251
|
+
]);
|
|
252
|
+
}
|
|
78
253
|
/**
|
|
79
254
|
* The height the rows outside the window would have taken.
|
|
80
255
|
*
|
|
@@ -116,14 +291,7 @@ function severityForRow(overlay, members) {
|
|
|
116
291
|
// the justification was the defect, not the loop.
|
|
117
292
|
return overlay === null ? undefined : heaviestRow(overlay, members)?.severity;
|
|
118
293
|
}
|
|
119
|
-
|
|
120
|
-
* Add `data-ig-audit` to the rail's keyed rows.
|
|
121
|
-
*
|
|
122
|
-
* A total walk that rebuilds the tree rather than mutating it: `ElementSpec` is
|
|
123
|
-
* `readonly` throughout, and a mutating walk would also be visible to the
|
|
124
|
-
* caller's own copy of `scene.root`.
|
|
125
|
-
*/
|
|
126
|
-
function markRail(root, severityOf) {
|
|
294
|
+
function markRail(root, severityOf, deltaOf) {
|
|
127
295
|
// TYPED AS `ElementSpec -> ElementSpec` AT THE BOUNDARY, with the child walk
|
|
128
296
|
// kept inside. A single function over `SpecChild` would hand `renderMarkup` a
|
|
129
297
|
// union it does not take, and the obvious repair — casting the result back —
|
|
@@ -137,10 +305,56 @@ function markRail(root, severityOf) {
|
|
|
137
305
|
// resolves to nothing today and to something the day a host names an issue
|
|
138
306
|
// that. Narrowed rather than coerced.
|
|
139
307
|
const severity = typeof key === 'string' ? severityOf(key) : undefined;
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
308
|
+
const delta = typeof key === 'string' ? deltaOf(key) : undefined;
|
|
309
|
+
const walked = spec.children?.map(markChild);
|
|
310
|
+
// THE CHIP IS APPENDED TO THE ROW, WHICH IS WHY IT IS DRAWN HERE AND NOT
|
|
311
|
+
// IN A MOUNT. §17c puts the effect in place — "only affected rows carry a
|
|
312
|
+
// delta chip · unaffected rows are left completely alone" — and
|
|
313
|
+
// `reevaluate/render.ts` deferred that to "the change that assembles the
|
|
314
|
+
// workspace" on the ground that placing a chip needs the row's geometry.
|
|
315
|
+
// It does not: the row is a SPEC here, keyed and reachable, and this walk
|
|
316
|
+
// was already composing one decoration onto it. So the chip is composed
|
|
317
|
+
// like the audit's mark rather than positioned like an overlay, and no
|
|
318
|
+
// geometry is consulted at all.
|
|
319
|
+
//
|
|
320
|
+
// AN UNAFFECTED ROW IS RETURNED WITH ITS OWN `children` ARRAY, not a
|
|
321
|
+
// rebuilt one carrying an appended `null` — `deltaOf` answering `undefined`
|
|
322
|
+
// has to leave the row byte-identical, which is the design's rule made
|
|
323
|
+
// structural rather than asserted.
|
|
324
|
+
const chip = delta === undefined ? null : chipSpec(delta.chip, delta.words, { placed: true });
|
|
325
|
+
const children = chip === null ? walked : [...(walked ?? []), chip];
|
|
326
|
+
const marked = { ...spec.attrs };
|
|
327
|
+
if (severity !== undefined)
|
|
328
|
+
marked[AUDIT_SEVERITY_ATTRIBUTE] = severity;
|
|
329
|
+
// THE CHIP IS IN THE ROW'S NAME, OR IT IS SILENT.
|
|
330
|
+
//
|
|
331
|
+
// Layer 1 gives the row an `aria-label`, and an accessible name computed
|
|
332
|
+
// from `aria-label` WINS over descendant text — so a chip appended here is
|
|
333
|
+
// seen and not heard. The rail is the surface a reader arrows through, and
|
|
334
|
+
// "which row moved, and by how much" is exactly what §17c puts on the row;
|
|
335
|
+
// the summary above carries aggregate counts and cannot recover it.
|
|
336
|
+
//
|
|
337
|
+
// APPENDED TO THE NAME rather than hung off `aria-describedby`, and the
|
|
338
|
+
// reason is that the alternative costs more than it looks. A description
|
|
339
|
+
// needs an id, this package emits none anywhere, and `a11y/baseline.ts`
|
|
340
|
+
// records what ids cost here — a host-derived token reaching an attribute
|
|
341
|
+
// that other code resolves, which is the injection class the mount's focus
|
|
342
|
+
// token was written to avoid. A name needs no id and is announced as the
|
|
343
|
+
// reader arrives rather than after it.
|
|
344
|
+
//
|
|
345
|
+
// The separator is layer 1's own: `slotLabel` joins its three parts with
|
|
346
|
+
// the same em dash, so this reads as one more part rather than a second
|
|
347
|
+
// sentence in a different hand.
|
|
348
|
+
const label = marked['aria-label'];
|
|
349
|
+
if (chip !== null && typeof label === 'string') {
|
|
350
|
+
marked['aria-label'] = `${label} — ${textOf(chip)}`;
|
|
351
|
+
}
|
|
352
|
+
// OMITTED WHEN THE CHIP HAS NO DRAWABLE KIND, rather than stamped empty.
|
|
353
|
+
// `DELTA_ATTRIBUTE` records why: an attribute on every row with one value
|
|
354
|
+
// meaning "nothing" is marking the row, not leaving it alone.
|
|
355
|
+
if (delta?.kind !== undefined)
|
|
356
|
+
marked[DELTA_ATTRIBUTE] = delta.kind;
|
|
357
|
+
const attrs = severity === undefined && delta?.kind === undefined ? spec.attrs : marked;
|
|
144
358
|
return {
|
|
145
359
|
...spec,
|
|
146
360
|
...(attrs === undefined ? {} : { attrs }),
|
|
@@ -477,7 +691,13 @@ function refusalCapsule(relationship, code, subject, words) {
|
|
|
477
691
|
]);
|
|
478
692
|
}
|
|
479
693
|
/**
|
|
480
|
-
*
|
|
694
|
+
* The numbered kind list, its heading, and the control that withdraws from it.
|
|
695
|
+
*
|
|
696
|
+
* DRAWN IN BOTH FRAMES, AND THIS FUNCTION ANSWERS TO BOTH. §17b fixes the
|
|
697
|
+
* keyboard loop the digits belong to; §17a draws the same list in the inspector
|
|
698
|
+
* with a heading over it and the digit as a chip at each row's end. Those two
|
|
699
|
+
* placements are §17a's, taken here under autnmy/issuegraph#147; the flip, the
|
|
700
|
+
* target search and delete/retype remain §17b's and are not drawn here.
|
|
481
701
|
*
|
|
482
702
|
* THE DIGITS ARE `create/keys.ts`'s OWN, through `KIND_KEYS`. Drawing
|
|
483
703
|
* `index + 1` over the kind order here would be a second construction of the
|
|
@@ -495,13 +715,19 @@ function refusalCapsule(relationship, code, subject, words) {
|
|
|
495
715
|
* remove and BEGIN a relationship, and must supply its own target picker.
|
|
496
716
|
*
|
|
497
717
|
* `source` NAMES THE DRAFT WHEN IT IS NOT THIS PANEL'S OWN, and is `null` when
|
|
498
|
-
* it is — see {@link
|
|
718
|
+
* it is — see {@link createStep} for why the list is drawn either way. Written
|
|
499
719
|
* as the reference beside the host's phrase, which is `whyRankSpec`'s own shape
|
|
500
720
|
* for the same problem: the package names the issue and the host writes the
|
|
501
721
|
* English around it.
|
|
502
722
|
*/
|
|
503
723
|
function kindListSpec(words, source) {
|
|
504
724
|
return element('div', { class: 'ig-inspector-add' }, [
|
|
725
|
+
// §17a HEADS THE KINDS, in the caps treatment the stylesheet already
|
|
726
|
+
// declares for three headings and until now drew only two of. Without it
|
|
727
|
+
// the five numbered rows run straight on from the relationship rows above,
|
|
728
|
+
// and nothing on screen says the numbers choose a kind rather than name
|
|
729
|
+
// more relationships.
|
|
730
|
+
element('h3', { class: 'ig-inspector-heading' }, [words.addRelationshipHeading]),
|
|
505
731
|
source === null
|
|
506
732
|
? null
|
|
507
733
|
: element('p', { class: 'ig-inspector-source' }, [
|
|
@@ -518,8 +744,13 @@ function kindListSpec(words, source) {
|
|
|
518
744
|
'data-ig-value': entry.edgeKind,
|
|
519
745
|
'data-edge': entry.edgeKind,
|
|
520
746
|
}, [
|
|
521
|
-
|
|
747
|
+
// §17a PUTS THE DIGIT LAST, IN A CHIP. The kind is what the reader
|
|
748
|
+
// is choosing between, so it comes first in the reading order and
|
|
749
|
+
// the key follows it — "blocked-by, 1" rather than "1,
|
|
750
|
+
// blocked-by". The chip is what carries the "this is a key"
|
|
751
|
+
// signal a left-hand column used to carry by alignment alone.
|
|
522
752
|
...glyphAndLabel(treatment.glyph, treatment.label),
|
|
753
|
+
element('span', { class: 'ig-kind-digit' }, [entry.key]),
|
|
523
754
|
]),
|
|
524
755
|
]);
|
|
525
756
|
})),
|
|
@@ -547,17 +778,41 @@ function kindListSpec(words, source) {
|
|
|
547
778
|
* this package in the business of explaining a hold whose vocabulary belongs to
|
|
548
779
|
* the reader that produced it.
|
|
549
780
|
*
|
|
550
|
-
*
|
|
551
|
-
* `
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
781
|
+
* A RANK AND A HOLD ARE NO LONGER EXCLUSIVE, which is what frame 17a always
|
|
782
|
+
* asked for: `#512` at rank 2 *and* "Held until #488 closes". `RULINGS.md` §1
|
|
783
|
+
* (Claude Design, 2026-09-20) settled it and `@issuegraph/derive` now ranks a
|
|
784
|
+
* held slot whose blocker is inside the previewed order, so the state the
|
|
785
|
+
* frame draws is representable and PR #126's em dash applies only to the other
|
|
786
|
+
* arm — a slot whose blocker is outside the order, which still has no number.
|
|
787
|
+
*
|
|
788
|
+
* SO THE TWO CHANNELS ARE READ FROM DIFFERENT FIELDS, AND NEITHER IS `rank`.
|
|
789
|
+
* `data-held` is READINESS — `ready` is the slot's own verdict and the only
|
|
790
|
+
* field that answers "may this start", exactly as layer 1's `slotPosition`
|
|
791
|
+
* now reads it for the row's accessible name. The HEADING is the rank: a
|
|
792
|
+
* number when the order stated a position, and otherwise a word for what is
|
|
793
|
+
* missing.
|
|
794
|
+
*
|
|
795
|
+
* THREE HEADINGS, NOT TWO, because `rank === null` and "held" came apart. A
|
|
796
|
+
* placed slot names its number, held or not. An unplaced HELD slot says so —
|
|
797
|
+
* PR #126's em dash, in words. An unplaced READY slot is the case the old
|
|
798
|
+
* one-field expression got flatly wrong: it was told "why held" while having
|
|
799
|
+
* no hold to show. `@issuegraph/derive` cannot emit one, but `ViewerDocument`
|
|
800
|
+
* is a public port and a host composes one by hand, so it is reachable from
|
|
801
|
+
* outside this repo — and leaving the inference here would ship the defect
|
|
802
|
+
* this change exists to retire in the package next door. It reuses the rank
|
|
803
|
+
* word with the rail's own em dash rather than taking a new `WorkspaceWords`
|
|
804
|
+
* entry: the fact is "there is no number", which is what the rank cell says
|
|
805
|
+
* with the same character, and a required word would break every host.
|
|
555
806
|
*/
|
|
556
807
|
function whyRankSpec(why, words, holds) {
|
|
557
|
-
const held = why.
|
|
808
|
+
const held = !why.ready;
|
|
558
809
|
return element('div', { class: 'ig-why-rank', 'data-held': held ? 'true' : 'false' }, [
|
|
559
810
|
element('h3', { class: 'ig-why-rank-heading' }, [
|
|
560
|
-
|
|
811
|
+
why.rank !== null
|
|
812
|
+
? `${words.whyRank} ${String(why.rank)}`
|
|
813
|
+
: held
|
|
814
|
+
? words.whyHeld
|
|
815
|
+
: `${words.whyRank} —`,
|
|
561
816
|
]),
|
|
562
817
|
element('p', { class: 'ig-why-rank-sentence' }, [
|
|
563
818
|
provenanceClause(why.provenance),
|
|
@@ -580,6 +835,98 @@ function whyRankSpec(why, words, holds) {
|
|
|
580
835
|
holds.length === 0 ? null : element('ul', { class: 'ig-inspector-holds' }, holds),
|
|
581
836
|
]);
|
|
582
837
|
}
|
|
838
|
+
/**
|
|
839
|
+
* One header member, rendered, or the empty string.
|
|
840
|
+
*
|
|
841
|
+
* The zone is assembled by concatenating already-rendered markup — the audit
|
|
842
|
+
* header arrives that way and owns its own `aria-pressed` — so a member that is
|
|
843
|
+
* a spec has to be rendered before it joins them, and an absent member has to
|
|
844
|
+
* join as nothing rather than as a gap.
|
|
845
|
+
*/
|
|
846
|
+
function specMarkup(spec) {
|
|
847
|
+
return spec === null ? '' : renderMarkup(spec);
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* §17a's `312 open · 64 encoded` — how much of the backlog carries relationships.
|
|
851
|
+
*
|
|
852
|
+
* OMITTED WHEN THE HOST STATED NO NUMBERS, never defaulted, on this zone's
|
|
853
|
+
* standing rule: a zero the reader can trust and a zero nobody computed are
|
|
854
|
+
* different facts. `Adoption.counts` is optional precisely so a host that
|
|
855
|
+
* cannot count says nothing.
|
|
856
|
+
*
|
|
857
|
+
* THE TOTAL LEADS, WHICH IS THE FRAME'S ORDER AND NOT LAYER 1's. §16a words the
|
|
858
|
+
* same pair as `64 of 312 declare relationships`, subject first. §17a reads the
|
|
859
|
+
* backlog first and the encoded subset second, and the two surfaces are allowed
|
|
860
|
+
* to differ: this is the header of a grooming workspace, where "how much is
|
|
861
|
+
* left to encode" is the question, and that one is answered by the pair read in
|
|
862
|
+
* this direction.
|
|
863
|
+
*/
|
|
864
|
+
function adoptionCountsSpec(adoption, words) {
|
|
865
|
+
const counts = adoption?.counts;
|
|
866
|
+
if (counts === undefined)
|
|
867
|
+
return null;
|
|
868
|
+
return element('span', { class: 'ig-workspace-counts' }, [
|
|
869
|
+
element('span', { class: 'ig-id' }, [String(counts.total)]),
|
|
870
|
+
` ${words.open} · `,
|
|
871
|
+
element('span', { class: 'ig-id' }, [String(counts.declaring)]),
|
|
872
|
+
` ${words.encoded}`,
|
|
873
|
+
]);
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* §17a's `as of 14:32 ↻` — how fresh the read is, and the way to take another.
|
|
877
|
+
*
|
|
878
|
+
* THE CONTROL IS PUBLISHED AND NOT WIRED, the shape layer 1 already gives it:
|
|
879
|
+
* refreshing a mirror is fetching, which no layer here does, so the button
|
|
880
|
+
* carries `refresh` on the command attribute and the host that can listens. A
|
|
881
|
+
* real `button`, so the mount's own click and Enter handling treat it as a
|
|
882
|
+
* control rather than as a row.
|
|
883
|
+
*
|
|
884
|
+
* DRAWN ONLY WHEN THE HOST SUPPLIED A WORD FOR IT, again as layer 1 does: a
|
|
885
|
+
* control nobody named is one nobody wired.
|
|
886
|
+
*
|
|
887
|
+
* STALENESS IS AN ATTRIBUTE, NOT A WORD. See {@link WorkspaceWords.asOf}.
|
|
888
|
+
*/
|
|
889
|
+
function freshnessSpec(freshness, words) {
|
|
890
|
+
if (freshness === undefined)
|
|
891
|
+
return null;
|
|
892
|
+
return element('span', {
|
|
893
|
+
class: 'ig-workspace-freshness',
|
|
894
|
+
'data-stale': freshness.stale === true ? 'true' : 'false',
|
|
895
|
+
}, [
|
|
896
|
+
`${words.asOf} `,
|
|
897
|
+
element('span', { class: 'ig-id' }, [freshness.asOf]),
|
|
898
|
+
freshness.age === undefined || freshness.age === '' ? null : ` · ${freshness.age}`,
|
|
899
|
+
freshness.refresh === undefined || freshness.refresh === ''
|
|
900
|
+
? null
|
|
901
|
+
: element('button', { type: 'button', class: 'ig-workspace-refresh', 'data-ig-command': 'refresh' }, [freshness.refresh]),
|
|
902
|
+
]);
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* The host facts the RAIL is given: everything except the two §17a's header now
|
|
906
|
+
* draws.
|
|
907
|
+
*
|
|
908
|
+
* THIS IS THE WHOLE MECHANISM OF #135's MOVE, and it is a subtraction rather
|
|
909
|
+
* than a switch. Layer 1 draws a fact when it is given one and omits it when it
|
|
910
|
+
* is not — `hostHeader` returns `null` outright once nothing is left for it —
|
|
911
|
+
* so the zone that receives a fact is the zone that states it, and no option
|
|
912
|
+
* has to be invented to say so. `SceneOptions.chrome` is the switch that could
|
|
913
|
+
* not do this: it would also take the condition notice, the adoption NOTE and
|
|
914
|
+
* `data-ig-condition`, none of which §17a's header replaces.
|
|
915
|
+
*
|
|
916
|
+
* `adoption.note` SURVIVES WHILE `adoption.counts` DOES NOT, which is why this
|
|
917
|
+
* cannot drop the field whole. They are two independent members of one optional
|
|
918
|
+
* object — §16h's own note says the design draws the count where adoption is
|
|
919
|
+
* partial and the line where it is absent — and the line is the rail's panel
|
|
920
|
+
* footer, a place §17a's header is not.
|
|
921
|
+
*
|
|
922
|
+
* `running` SURVIVES BY BEING LEFT ALONE. The NOW row is not part of layer 1's
|
|
923
|
+
* panel header and never was gated with it; see {@link headerMarkup} for the
|
|
924
|
+
* premise that said otherwise.
|
|
925
|
+
*/
|
|
926
|
+
function railHostFacts(host) {
|
|
927
|
+
const { freshness, adoption, ...rest } = host;
|
|
928
|
+
return adoption?.note === undefined ? rest : { ...rest, adoption: { note: adoption.note } };
|
|
929
|
+
}
|
|
583
930
|
/**
|
|
584
931
|
* §17a's workspace header: what backlog this is, how much of it is encoded,
|
|
585
932
|
* what is wrong with it, how fresh the read is, and the way into a first pass.
|
|
@@ -590,18 +937,37 @@ function whyRankSpec(why, words, holds) {
|
|
|
590
937
|
* input had no header at all. §17a's header carries five facts and the audit
|
|
591
938
|
* count is one of them.
|
|
592
939
|
*
|
|
593
|
-
* WHAT IT DRAWS IS WHAT THE RAIL DOES NOT
|
|
594
|
-
* counts and the freshness stamp into a
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
* is,
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
940
|
+
* WHAT IT DRAWS IS WHAT THE RAIL DOES NOT, and #135 settled the split. §17a
|
|
941
|
+
* hoists the identity, the adoption counts and the freshness stamp into a
|
|
942
|
+
* header spanning all three zones, so this carries five facts: what backlog
|
|
943
|
+
* this is, how much of it is encoded, what the audit found, how fresh the read
|
|
944
|
+
* is, and the way into a first pass.
|
|
945
|
+
*
|
|
946
|
+
* THE RULING, RECORDED HERE BECAUSE THIS IS THE SURFACE IT IS ABOUT:
|
|
947
|
+
*
|
|
948
|
+
* - `Adoption.counts` and the freshness stamp (with its refresh control) are
|
|
949
|
+
* drawn HERE and nowhere else. §17a draws both, and layer 1's panel header
|
|
950
|
+
* drew both until this change.
|
|
951
|
+
* - `OrderCounts` and `concurrencyCap` stay in the RAIL. They are a different
|
|
952
|
+
* pair from the adoption one — how the order splits, not how much of the
|
|
953
|
+
* backlog is encoded — and §17a's header does not draw them. The rail is
|
|
954
|
+
* the order, so the order's own tally belongs to its panel header.
|
|
955
|
+
* - The running-job NOW row stays in the rail, unchanged.
|
|
956
|
+
*
|
|
957
|
+
* ⚠️ #135's PREMISE WAS WRONG ABOUT THE NOW ROW, and correcting it is what made
|
|
958
|
+
* the ruling small. The issue records that `SceneOptions.chrome` takes the
|
|
959
|
+
* stamp, the refresh control, the count chips AND the NOW row together, so that
|
|
960
|
+
* flipping it would drop a row §17a's rail does not replace. It does not:
|
|
961
|
+
* `nowRows` is called unconditionally by all three projection roots, beside the
|
|
962
|
+
* `chrome`-gated header, so no setting of that switch has ever reached it.
|
|
963
|
+
*
|
|
964
|
+
* SO THE SWITCH IS NOT THE LEVER, and it is deliberately left alone. `chrome:
|
|
965
|
+
* false` also takes the condition notice, the adoption NOTE and the panel's
|
|
966
|
+
* `data-ig-condition` — three things §17a's header does not replace — so the
|
|
967
|
+
* all-or-nothing flip could only be made by dropping them. What moves a fact
|
|
968
|
+
* instead is WHICH ZONE IS GIVEN IT: {@link railHostFacts} hands the rail the
|
|
969
|
+
* host facts minus the two this header now draws, and layer 1 then omits them
|
|
970
|
+
* on its own existing "omitted when absent" rule rather than on a new option.
|
|
605
971
|
*
|
|
606
972
|
* EVERY FACT COMES FROM THE PORT THAT ALREADY CARRIES IT. `ViewerDocument.host`
|
|
607
973
|
* is commented "THE HOST-FACTS PORT", and #127 rejected deriving its numbers in
|
|
@@ -613,7 +979,7 @@ function whyRankSpec(why, words, holds) {
|
|
|
613
979
|
* gives: a zero the reader can trust and a zero nobody computed are different
|
|
614
980
|
* facts, and a header that invents either is worse than one that says less.
|
|
615
981
|
*/
|
|
616
|
-
function headerMarkup(host, auditHeader) {
|
|
982
|
+
function headerMarkup(host, auditHeader, words) {
|
|
617
983
|
// A STRING, not a spec, because one member of this zone already is one: the
|
|
618
984
|
// audit header comes from its own leaf rendered, and it owns the filter
|
|
619
985
|
// toggle's `aria-pressed` and the count's own omitted-when-absent rule. The
|
|
@@ -623,7 +989,9 @@ function headerMarkup(host, auditHeader) {
|
|
|
623
989
|
host?.identity === undefined || host.identity === ''
|
|
624
990
|
? ''
|
|
625
991
|
: renderMarkup(element('span', { class: 'ig-workspace-identity' }, [host.identity])),
|
|
992
|
+
specMarkup(adoptionCountsSpec(host?.adoption, words)),
|
|
626
993
|
auditHeader,
|
|
994
|
+
specMarkup(freshnessSpec(host?.freshness, words)),
|
|
627
995
|
host?.firstPass === undefined || host.firstPass === ''
|
|
628
996
|
? ''
|
|
629
997
|
: renderMarkup(element('button', { type: 'button', class: 'ig-workspace-firstpass', 'data-ig-command': 'first-pass' }, [host.firstPass])),
|
|
@@ -947,6 +1315,11 @@ function inspectorSpec(view, context) {
|
|
|
947
1315
|
// recomputed per region: two calls could not disagree today, but a panel that
|
|
948
1316
|
// decides its own reach twice is the shape `carrier` exists to have ended.
|
|
949
1317
|
const scope = panelScope(view);
|
|
1318
|
+
// ASKED ONCE, FOR THE SAME REASON `scope` IS. §17a draws the create path in
|
|
1319
|
+
// two zones — `+ add` on the relationships header, the numbered kinds under
|
|
1320
|
+
// their own heading — and a step derived separately per zone is two answers
|
|
1321
|
+
// to one question, free to disagree the day either guard moves.
|
|
1322
|
+
const step = createStep(context, key);
|
|
950
1323
|
return element('div', { class: 'ig-inspector', 'data-subject': subject.kind }, [
|
|
951
1324
|
element('div', { class: 'ig-inspector-head' }, [
|
|
952
1325
|
element('h2', { class: 'ig-inspector-name' }, [words.inspector]),
|
|
@@ -974,8 +1347,24 @@ function inspectorSpec(view, context) {
|
|
|
974
1347
|
: whyRankSpec(subject.whyRank, words, subject.whyRank.holds.map((hold) => holdRow(hold, context.known, (subjectKey) => context.leadOf.get(subjectKey) === context.leadOf.get(subject.issue.key)))),
|
|
975
1348
|
])
|
|
976
1349
|
: null,
|
|
1350
|
+
// ONE DERIVATION, READ TWICE. The step is asked once here and both
|
|
1351
|
+
// placements below read the answer, so the header and the body cannot
|
|
1352
|
+
// disagree about WHICH step is live. What each draws for a given arm is
|
|
1353
|
+
// still a discipline this file keeps rather than one the type checker
|
|
1354
|
+
// keeps — the single-return shape this replaces made drawing both
|
|
1355
|
+
// unrepresentable, and a value read at two sites does not. The pin that
|
|
1356
|
+
// stands where the return ordering used to is the one asserting no `add`
|
|
1357
|
+
// control exists anywhere in the panel at the kind step.
|
|
977
1358
|
element('div', { class: 'ig-inspector-relationships', 'data-filtered': view.filtered ? 'true' : 'false' }, [
|
|
978
|
-
|
|
1359
|
+
// §17a PUTS `+ add` ON THE HEADING'S OWN ROW, and the reason is the
|
|
1360
|
+
// list between them: it is unbounded, so a control after it drifts
|
|
1361
|
+
// down the panel as a subject gains relationships and is below the
|
|
1362
|
+
// fold on the busy issues a groomer most needs it for. On the header
|
|
1363
|
+
// row its position does not depend on the content.
|
|
1364
|
+
element('div', { class: 'ig-inspector-relationships-head' }, [
|
|
1365
|
+
element('h3', { class: 'ig-inspector-heading' }, [words.relationships]),
|
|
1366
|
+
step.kind === 'add' ? addControlSpec(words, step.subject) : null,
|
|
1367
|
+
]),
|
|
979
1368
|
// A STATED EMPTY, AND ONLY WHERE THERE IS A SUBJECT TO STATE IT ABOUT.
|
|
980
1369
|
// With nothing selected the panel already says so once, in
|
|
981
1370
|
// `nothingSelected`; adding "no relationships" under it would be the
|
|
@@ -991,13 +1380,7 @@ function inspectorSpec(view, context) {
|
|
|
991
1380
|
// reader mid-draft a control that RESETS the draft they are in —
|
|
992
1381
|
// `create/draft.ts` makes `begin` clear the kind and the target
|
|
993
1382
|
// deliberately.
|
|
994
|
-
|
|
995
|
-
// `+ add` belongs to the subject. `subject.kind !== 'issue' ? null : …`
|
|
996
|
-
// was one guard doing both jobs, and it dropped a live draft's controls
|
|
997
|
-
// on a panel with no issue subject — which `reconcileHost` reaches by
|
|
998
|
-
// clearing a selection whose issue a write removed while leaving a
|
|
999
|
-
// draft begun from another issue standing.
|
|
1000
|
-
addStepSpec(context, subject.kind === 'issue' ? subject.issue.key : null),
|
|
1383
|
+
step.kind === 'kinds' ? kindListSpec(words, step.source) : null,
|
|
1001
1384
|
]),
|
|
1002
1385
|
// STATED WHERE THE READER MADE THE EDIT, by the same rule the refusals use
|
|
1003
1386
|
// — one `statedHere`, one carrier, no second derivation of whose panel this
|
|
@@ -1019,85 +1402,83 @@ function inspectorSpec(view, context) {
|
|
|
1019
1402
|
recoveryListSpec(context.recoveries.filter((recovery) => recovery.carrier === null && !statedHere(scope, recovery)), words.recovery, context.diffOpen, null, words.recovery.unplaced),
|
|
1020
1403
|
]);
|
|
1021
1404
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
*
|
|
1025
|
-
* THE STEP FOLLOWS THE DRAFT, NOT THE SELECTION, and the draft's source is
|
|
1026
|
-
* NAMED when it is not this panel's subject. The two can diverge: `pointed`
|
|
1027
|
-
* diverts a click to the draft only once a kind has been chosen, so at the kind
|
|
1028
|
-
* step a click anywhere else moves the selection and leaves the draft's source
|
|
1029
|
-
* behind — and `R` on a together unit's non-lead member begins a draft from
|
|
1030
|
-
* that member while `inspectorView` canonicalizes the panel onto the slot's
|
|
1031
|
-
* LEAD, so the two disagree from the first keystroke. Unnamed, the panel showed
|
|
1032
|
-
* "choose a kind" beneath the heading of one issue while the write would go out
|
|
1033
|
-
* from another. Named, it says whose draft it is.
|
|
1034
|
-
*
|
|
1035
|
-
* WITHHOLDING THE LIST WAS THE FIRST ANSWER AND IT WAS WORSE. It left the
|
|
1036
|
-
* reader a LIVE draft with no pointer route to it at all: the numbered choices
|
|
1037
|
-
* were gone, the cancel that sits with them was gone, and the shell draws no
|
|
1038
|
-
* chooser of its own at this step — `mount.ts`'s floating one is drawn only
|
|
1039
|
-
* under a canvas drop, and its target search only once a kind is chosen. So the
|
|
1040
|
-
* one control still on screen was `+ add`, which does not cancel a draft, it
|
|
1041
|
-
* RESETS one, silently moving the source to whatever the reader happened to be
|
|
1042
|
-
* looking at. "The shell's cancel still reaches it" was the reasoning, and the
|
|
1043
|
-
* shell has no cancel to reach it with. A draft the reader cannot abandon with
|
|
1044
|
-
* a pointer is the failure `create/keys.ts` names for the keyboard, and the
|
|
1045
|
-
* cost of the alternative — one phrase naming an issue that is not the panel's
|
|
1046
|
-
* subject — is the cost the shell's own chooser always paid, in the sentence it
|
|
1047
|
-
* printed above its kinds.
|
|
1048
|
-
*
|
|
1049
|
-
* `+ add` IS NOT DRAWN BESIDE IT, on either panel. Two entries into one draft is
|
|
1050
|
-
* the rule {@link WorkspaceOptions.drop} already states for the floating
|
|
1051
|
-
* chooser, and here the second entry is the destructive one: `create/draft.ts`
|
|
1052
|
-
* makes `begin` clear the kind and the target deliberately, so a reader
|
|
1053
|
-
* mid-draft offered `+ add` is offered a reset wearing the label of a start.
|
|
1054
|
-
* They restart by cancelling first, which is now a control they can see.
|
|
1055
|
-
*
|
|
1056
|
-
* IT IS ASKED FOR EVERY SUBJECT, INCLUDING NONE. `+ add` is the half that needs
|
|
1057
|
-
* one — `reduceHost`'s `add` arm answers `null` for an edge selection and for
|
|
1058
|
-
* none, so a control there could not complete — and the step is not. One guard
|
|
1059
|
-
* over both dropped a live draft's controls on a panel with no issue subject,
|
|
1060
|
-
* which `reconcileHost` reaches: it clears a selection whose issue a landed
|
|
1061
|
-
* write removed while leaving a draft begun from a DIFFERENT issue standing,
|
|
1062
|
-
* because that draft's own references are all still in the document.
|
|
1063
|
-
*
|
|
1064
|
-
* `null` AT THE TARGET STEP AND UNDER A LIVE DROP, which are two more different
|
|
1065
|
-
* silences and both deliberate. The target step belongs to the shell, which
|
|
1066
|
-
* owns the live input — and draws its own cancel beside it, naming the source
|
|
1067
|
-
* in the same sentence. A drop means a chooser is already open at the pointer,
|
|
1068
|
-
* with a cancel of its own, and a second copy of the same step in the column
|
|
1069
|
-
* beside it would be two controls writing to one draft.
|
|
1070
|
-
*/
|
|
1071
|
-
function addStepSpec(context, subject) {
|
|
1072
|
-
const { draft, drop, words } = context;
|
|
1073
|
-
// THE ONE LIVE KIND STEP, WHEREVER IT WAS BEGUN. Asked of the draft alone, so
|
|
1074
|
-
// there is exactly one panel state for it rather than one per subject — and
|
|
1075
|
-
// so a panel with no issue subject at all still carries the cancel.
|
|
1405
|
+
function createStep(context, subject) {
|
|
1406
|
+
const { draft, drop } = context;
|
|
1076
1407
|
if (draft.source !== null && draft.kind === null && drop === null) {
|
|
1077
|
-
return
|
|
1408
|
+
return { kind: 'kinds', source: draft.source === subject ? null : draft.source };
|
|
1078
1409
|
}
|
|
1079
1410
|
// `+ add` NEEDS A SUBJECT TO BEGIN FROM, and `reduceHost`'s `add` arm answers
|
|
1080
1411
|
// `null` for an edge selection and for none — so drawing it there would
|
|
1081
1412
|
// publish an act that cannot complete.
|
|
1082
|
-
if (subject !== null && draft.source !== subject)
|
|
1083
|
-
return
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1413
|
+
if (subject !== null && draft.source !== subject)
|
|
1414
|
+
return { kind: 'add', subject };
|
|
1415
|
+
return { kind: 'none' };
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* §17a's `+ add`, with the key that does the same thing beside it.
|
|
1419
|
+
*
|
|
1420
|
+
* THE HINT IS THE BINDING'S OWN LETTER, through {@link RELATE_KEY}. A literal
|
|
1421
|
+
* here would be a second spelling of a key the map already owns, free to go
|
|
1422
|
+
* stale the day the binding moves and with nothing failing to say so — the
|
|
1423
|
+
* drift `create/keys.ts`'s header rejects in terms, and the reason `KIND_KEYS`
|
|
1424
|
+
* exists for the digits one function below.
|
|
1425
|
+
*
|
|
1426
|
+
* IT IS `aria-hidden`, AND THE BUTTON KEEPS THE HOST'S WORD AS ITS NAME. An
|
|
1427
|
+
* accessible name is computed from descendant text, so an exposed hint appends
|
|
1428
|
+
* a bare letter to the act — "add relationship R" — which announces a shortcut
|
|
1429
|
+
* as though it were part of the label. Hidden, the name is the host's word
|
|
1430
|
+
* alone. This is the pairing rule `hiddenGlyph` already applies to every mark
|
|
1431
|
+
* beside a word in this package: the mark is seen, the word is heard.
|
|
1432
|
+
*
|
|
1433
|
+
* UPPERCASE IS THE STYLESHEET'S. The map stores the lowercase key a press
|
|
1434
|
+
* normalizes to; §17a draws `R`. `text-transform` renders it, as it already
|
|
1435
|
+
* does for every heading in this panel, so there is no second string.
|
|
1436
|
+
*
|
|
1437
|
+
* NOTE THE HINT AND THE CONTROL CAN BEGIN FROM DIFFERENT ISSUES. `keyIntent`'s
|
|
1438
|
+
* `relate` arm begins from `KeyboardContext.focused`, while this button
|
|
1439
|
+
* publishes the canonical `data-ig-target`; on a together unit's non-lead
|
|
1440
|
+
* member those are different issues, which {@link createStep} records. Both
|
|
1441
|
+
* begin a draft the reader can see and cancel, so the pairing is kept.
|
|
1442
|
+
*
|
|
1443
|
+
* AND THE KEY NOW WORKS WHILE THIS BUTTON ITSELF HOLDS FOCUS — it did not when
|
|
1444
|
+
* the hint was first drawn beside it, and `#173` is the repair. The hint had
|
|
1445
|
+
* been naming a key the reader genuinely has at the one focus position where it
|
|
1446
|
+
* was inert: `interaction()` answered `elsewhere` for a focused command control
|
|
1447
|
+
* and `create/keys.ts`'s `reaches()` refuses every binding there, while
|
|
1448
|
+
* `KeyboardContext.focused` was the rail's tab stop and so `null` then, leaving
|
|
1449
|
+
* the `relate` arm with nothing to relate FROM even had the interaction
|
|
1450
|
+
* reached. Both halves moved: `add-control` is a `CreateInteraction` admitting
|
|
1451
|
+
* `RELATE_KEY` alone, and `focusedAddSubject` supplies the source from the
|
|
1452
|
+
* attribute below.
|
|
1453
|
+
*
|
|
1454
|
+
* WHICH IS WHY THE ATTRIBUTE IS LOAD-BEARING FOR THE KEYBOARD TOO, and no
|
|
1455
|
+
* longer only for the pointer: `focusedAddSubject` reads exactly this
|
|
1456
|
+
* `data-ig-target`, narrowed to the `add` command, so the note above about the
|
|
1457
|
+
* hint and the control beginning from different issues is now the ONE case
|
|
1458
|
+
* where they still can — a together unit's non-lead member holding the rail's
|
|
1459
|
+
* tab stop, where `focusedKey` answers first and this fallback is not reached.
|
|
1460
|
+
*/
|
|
1461
|
+
function addControlSpec(words, subject) {
|
|
1462
|
+
return element('button', {
|
|
1463
|
+
type: 'button',
|
|
1464
|
+
class: 'ig-inspector-addbutton',
|
|
1465
|
+
'data-ig-command': 'add',
|
|
1466
|
+
// THE CANONICAL SUBJECT, PUBLISHED, for the reason the row's remove
|
|
1467
|
+
// control publishes its own edge. `reduceHost`'s `add` arm read
|
|
1468
|
+
// `selectedKey` — the RAW key — while everything around it is worded
|
|
1469
|
+
// against the slot LEAD that `inspectorView` canonicalizes to, so
|
|
1470
|
+
// selecting a together-unit PARTNER drew a panel titled with the lead
|
|
1471
|
+
// and began a relationship from the partner. The act names its
|
|
1472
|
+
// subject; the selection is the fallback for a control that names none.
|
|
1473
|
+
'data-ig-target': subject,
|
|
1474
|
+
}, [
|
|
1475
|
+
// THE WORD IS A BARE CHILD, NOT A WRAPPED ONE. A span around it would need
|
|
1476
|
+
// a class, and a class with no rule is what `styles.test.ts` refuses —
|
|
1477
|
+
// correctly, since an element this sheet does not style is one a host
|
|
1478
|
+
// cannot theme.
|
|
1479
|
+
words.addRelationship,
|
|
1480
|
+
element('span', { class: 'ig-inspector-addkey', 'aria-hidden': 'true' }, [RELATE_KEY]),
|
|
1481
|
+
]);
|
|
1101
1482
|
}
|
|
1102
1483
|
/**
|
|
1103
1484
|
* One hold in the inspector, with its cause and its subject on the markup.
|
|
@@ -1188,6 +1569,17 @@ export function renderWorkspace(input, options) {
|
|
|
1188
1569
|
const document = sound.document;
|
|
1189
1570
|
// The keys a hold's subject control may name — see `holdRow`.
|
|
1190
1571
|
const known = new Set(document.issues.map((issue) => issue.key));
|
|
1572
|
+
// THE EDGE IDS THIS SURFACE ACTUALLY DRAWS, for §17d's remedies to gate on.
|
|
1573
|
+
//
|
|
1574
|
+
// BUILT FROM THE DRAWN DOCUMENT, not from the audit's. The audit reads a
|
|
1575
|
+
// document that is not always the one on screen, and the two disagreeing is
|
|
1576
|
+
// the normal case rather than the exceptional one — a host audits everything
|
|
1577
|
+
// it holds and renders a page of it.
|
|
1578
|
+
//
|
|
1579
|
+
// `edgeIdentity` RATHER THAN A HAND-SPELLED KEY, because that function is
|
|
1580
|
+
// what mints the ids a finding carries and what the host looks one up by. A
|
|
1581
|
+
// second spelling here would agree with itself and disagree with both.
|
|
1582
|
+
const drawnEdges = new Set(document.edges.map((edge) => edgeIdentity(edge.field, edge.from, edge.to)));
|
|
1191
1583
|
// Which slot a key sits in, by lead — so a hold's subject that resolves to the
|
|
1192
1584
|
// inspected slot gets no control. The same canonicalization `inspectorView`
|
|
1193
1585
|
// applies, read off the same normalized slots.
|
|
@@ -1201,25 +1593,103 @@ export function renderWorkspace(input, options) {
|
|
|
1201
1593
|
// members rather than the lead, for the reason the bar is: a finding can name
|
|
1202
1594
|
// a member that does not lead its unit.
|
|
1203
1595
|
const filtered = overlay !== null && options.auditFiltered === true;
|
|
1596
|
+
// AN OVERLAY WITH NOTHING BEHIND IT IS NOT OPEN. `overlay === null` means the
|
|
1597
|
+
// host wired no audit at all, and a host that leaves `auditOpen` true across
|
|
1598
|
+
// that change would otherwise ask for a panel over an audit that does not
|
|
1599
|
+
// exist.
|
|
1600
|
+
const auditOpen = overlay !== null && options.auditOpen === true;
|
|
1601
|
+
// THE HEADER'S TWO FACTS ARE WITHHELD FROM THE RAIL HERE, and this is the one
|
|
1602
|
+
// place they are: see `railHostFacts` for why a subtraction rather than
|
|
1603
|
+
// `SceneOptions.chrome`.
|
|
1604
|
+
const railHost = railHostFacts(document.host);
|
|
1605
|
+
const narrowedSlots = filtered
|
|
1606
|
+
? document.order.slots.filter((slot) => slot.members.some((member) => auditFilterKeeps(overlay, member)))
|
|
1607
|
+
: document.order.slots;
|
|
1608
|
+
const narrowedExcluded = filtered
|
|
1609
|
+
? document.order.excluded.filter((exclusion) => auditFilterKeeps(overlay, exclusion.key))
|
|
1610
|
+
: document.order.excluded;
|
|
1611
|
+
// A FILTER THAT MATCHES NOTHING IS NOT A ZERO, AND THE PROJECTION CANNOT KNOW
|
|
1612
|
+
// THAT FROM WHAT IT IS HANDED. It receives a document with no slots and says
|
|
1613
|
+
// the true thing about it — "Nothing is in the order right now" — which is a
|
|
1614
|
+
// FALSE thing about the backlog, because every rank the filter hid is still
|
|
1615
|
+
// there. `RULINGS.md` §2 names it: *"a false statement about the data, and it
|
|
1616
|
+
// is the one thing this panel exists not to make."*
|
|
1617
|
+
//
|
|
1618
|
+
// It is the defect `statesItsOwnCause` already describes one input over —
|
|
1619
|
+
// *"its own derived sentence is not a fallback explanation but a false one"* —
|
|
1620
|
+
// arrived at by narrowing instead of by a condition, so it takes the same
|
|
1621
|
+
// remedy: the host RECORDS what it did and the projection stops deriving.
|
|
1622
|
+
// Recording it here rather than teaching the viewer about filters keeps the
|
|
1623
|
+
// seam where it is: narrowing is the editor's act, and only the editor knows
|
|
1624
|
+
// the denominator, because only it still holds the unnarrowed order.
|
|
1625
|
+
const emptiedByFilter = filtered && narrowedSlots.length === 0 && narrowedExcluded.length === 0;
|
|
1626
|
+
// STATED WHENEVER THE FILTER IS ON, not only when it empties the rail — and
|
|
1627
|
+
// that is a consequence of §17d's entry point moving into the overlay.
|
|
1628
|
+
//
|
|
1629
|
+
// While the filter lived on the header count it was always on screen and
|
|
1630
|
+
// always clearable. `RULINGS.md` §3 makes that count the overlay's anchor and
|
|
1631
|
+
// puts the filter in the panel head, so the ONLY control for it now closes
|
|
1632
|
+
// with the panel — and a reader who narrowed the rail, shut the panel and
|
|
1633
|
+
// scrolled away would be left looking at a short list with nothing saying why
|
|
1634
|
+
// and nothing to press. §2 forbids exactly that: *"A filter that is visibly
|
|
1635
|
+
// on, states what it is hiding, and clears in one click is not a refusal."*
|
|
1636
|
+
// All three halves have to stay true with the panel shut.
|
|
1637
|
+
const railCondition = filtered
|
|
1638
|
+
? {
|
|
1639
|
+
kind: 'filtered',
|
|
1640
|
+
headline: emptiedByFilter ? options.words.filteredEmpty : options.words.filteredActive,
|
|
1641
|
+
// THE HOST'S OWN RECORDED COUNT, NOT A SECOND DERIVATION OF IT. The
|
|
1642
|
+
// rail header already prints `8 ranked` from `counts.ranked`, and the
|
|
1643
|
+
// first version of this line counted `order.slots.length` instead —
|
|
1644
|
+
// which is 11 on the demo, because it includes the footer slots the
|
|
1645
|
+
// header does not call ranked. Two numbers for one fact, on one screen,
|
|
1646
|
+
// eight pixels apart.
|
|
1647
|
+
//
|
|
1648
|
+
// That is the defect this repository has already ruled on twice: two
|
|
1649
|
+
// capsules drawing one fact must not each hold their own sentence, or
|
|
1650
|
+
// they describe one document differently in one view. A denominator
|
|
1651
|
+
// that disagrees with the header is worse than the lie it replaced,
|
|
1652
|
+
// because it is a lie the reader can SEE is a lie and cannot resolve.
|
|
1653
|
+
//
|
|
1654
|
+
// Falls back to the unnarrowed slot count only when the host recorded
|
|
1655
|
+
// no counts at all — a number that may over-count is still better than
|
|
1656
|
+
// an empty sentence here, and the field is optional in the port.
|
|
1657
|
+
denominator: options.words.filteredDenominator(document.host.counts?.ranked ?? document.order.slots.length),
|
|
1658
|
+
action: { label: options.words.filteredClear },
|
|
1659
|
+
}
|
|
1660
|
+
: undefined;
|
|
1661
|
+
const railHostWithCause = railCondition === undefined ? railHost : { ...railHost, condition: railCondition };
|
|
1204
1662
|
const railInput = filtered
|
|
1205
1663
|
? {
|
|
1206
1664
|
...document,
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
// ones on screen while the header said the filter was on — the toggle
|
|
1210
|
-
// narrowing part of the rail and claiming to have narrowed it.
|
|
1211
|
-
slots: document.order.slots.filter((slot) => slot.members.some((member) => auditFilterKeeps(overlay, member))),
|
|
1212
|
-
excluded: document.order.excluded.filter((exclusion) => auditFilterKeeps(overlay, exclusion.key)),
|
|
1213
|
-
},
|
|
1665
|
+
host: railHostWithCause,
|
|
1666
|
+
order: { slots: narrowedSlots, excluded: narrowedExcluded },
|
|
1214
1667
|
}
|
|
1215
|
-
: document;
|
|
1668
|
+
: { ...document, host: railHostWithCause };
|
|
1216
1669
|
const rail = railWindow(railInput, options.rail ?? {});
|
|
1217
1670
|
const railRender = renderViewer(rail.document, {
|
|
1218
1671
|
projection: 'linear',
|
|
1219
1672
|
theme,
|
|
1673
|
+
// THE WORKSPACE RULES ITS OWN ZONES OFF. The rail track already carries
|
|
1674
|
+
// `border-right`, so a frame here doubles that seam and the host's outer
|
|
1675
|
+
// border on the other edge. See `SceneOptions.frame`.
|
|
1676
|
+
frame: false,
|
|
1677
|
+
// §16b's KEY, KEPT IN VIEW while the order scrolls under it. See
|
|
1678
|
+
// `SceneOptions.dockLegend`; `dockRailLegend` in mount.ts moves it clear of
|
|
1679
|
+
// this zone's own footer.
|
|
1680
|
+
dockLegend: true,
|
|
1220
1681
|
// The rail is where a selected ISSUE reads as current. An edge selection
|
|
1221
1682
|
// resolves to no key, which is `selectedKey`'s whole job.
|
|
1222
1683
|
selected: selectedKey(selection),
|
|
1684
|
+
// §16f. Passed WHOLE rather than windowed: the window moves, and a reader
|
|
1685
|
+
// who opened a row, scrolled past it and came back should find it open.
|
|
1686
|
+
// A key the window does not draw costs one `includes` miss and nothing else.
|
|
1687
|
+
...(options.expanded === undefined ? {} : { expanded: options.expanded }),
|
|
1688
|
+
// §17j'S DENSITY ESCAPE HATCH, FORWARDED TO THE RAIL AND ONLY THE RAIL.
|
|
1689
|
+
// The canvas draws a second viewer in this surface, and the density block
|
|
1690
|
+
// is about the ORDER's rows — so handing both the same value would change a
|
|
1691
|
+
// projection the option says nothing about. See `ViewerOptions.density`.
|
|
1692
|
+
...(options.railDensity === undefined ? {} : { density: options.railDensity }),
|
|
1223
1693
|
});
|
|
1224
1694
|
// Built once, over the window's rows, so a rail of 312 costs one pass rather
|
|
1225
1695
|
// than one scan of `overlay.rows` per drawn row.
|
|
@@ -1243,9 +1713,31 @@ export function renderWorkspace(input, options) {
|
|
|
1243
1713
|
// scrolling position and says nothing about what surrounds the selected
|
|
1244
1714
|
// issue; handing the ladder a windowed document would make its budgets — and
|
|
1245
1715
|
// therefore its refusal — depend on where the reader had scrolled to.
|
|
1716
|
+
// THE LIST'S ID, DERIVED ONCE AND USED BY BOTH ZONES. Undefined unless the
|
|
1717
|
+
// caller supplied a surface value; see `WorkspaceOptions.surfaceId` for why
|
|
1718
|
+
// this renderer must not invent one.
|
|
1719
|
+
const isolatedListId = options.surfaceId === undefined ? undefined : `ig-isolated-list-${options.surfaceId}`;
|
|
1246
1720
|
const canvas = renderScaleLadder(document, {
|
|
1247
1721
|
state: options.scale ?? INITIAL_SCALE_STATE,
|
|
1248
1722
|
theme,
|
|
1723
|
+
isolatedListId,
|
|
1724
|
+
// ONE CONTROL, ONE ZONE — and the condition is WHETHER THIS SURFACE DRAWS
|
|
1725
|
+
// ONE, which is exactly what `words.rail` says. That is the contract
|
|
1726
|
+
// `ScaleLadderOptions.isolatedChip` states: suppress the ladder's chip only
|
|
1727
|
+
// if you draw the control yourself. §17a puts the count at the foot of the
|
|
1728
|
+
// rail and `railFooter` below draws it there. The LIST is unaffected and
|
|
1729
|
+
// stays in the canvas — see `railFooter` for why this zone cannot hold it.
|
|
1730
|
+
//
|
|
1731
|
+
// "THE WORDS EXIST" AND "THE FOOTER WAS DRAWN" ARE THE SAME CONDITION HERE,
|
|
1732
|
+
// and it is worth saying why rather than leaving a reader to check. The only
|
|
1733
|
+
// other reason `railFooter` returns null is an empty isolated set — and
|
|
1734
|
+
// `isolatedSpec` refuses that case too, so at count 0 neither draws and the
|
|
1735
|
+
// flag changes nothing. Gating on the words rather than on the returned spec
|
|
1736
|
+
// keeps this to ONE derivation of the ladder: the footer is built from
|
|
1737
|
+
// `canvas.ladder` below, which is the same value this call computes.
|
|
1738
|
+
// `rail-footer.test.ts` pins both halves — no configuration yields two
|
|
1739
|
+
// copies, and none yields none.
|
|
1740
|
+
isolatedChip: options.words.rail === undefined,
|
|
1249
1741
|
// THE SAME ONE VALUE THE RAIL READ. Without this the canvas drew the
|
|
1250
1742
|
// selected issue as ordinary while the rail marked it current, so the
|
|
1251
1743
|
// single selection this surface advertises disagreed with itself between
|
|
@@ -1259,18 +1751,302 @@ export function renderWorkspace(input, options) {
|
|
|
1259
1751
|
selectedEdge: selectedEdgeId(selection),
|
|
1260
1752
|
projected: options.projected,
|
|
1261
1753
|
});
|
|
1754
|
+
const toolbar = canvasToolbar(canvas.ladder, document, options.words.canvas);
|
|
1755
|
+
// §17k'S STRIP CONTROL — the way out of the collapsed canvas below `1120`.
|
|
1756
|
+
//
|
|
1757
|
+
// ALWAYS RENDERED, HIDDEN BY THE SHEET AT THE OTHER TWO WIDTHS, and that
|
|
1758
|
+
// order matters: the alternative is rendering it only when narrow, which
|
|
1759
|
+
// would mean this function knowing how wide it is. It does not and must not —
|
|
1760
|
+
// §17k's implementation note — so the control is emitted once and the
|
|
1761
|
+
// container query decides where it is drawn. `display: none` is also what
|
|
1762
|
+
// keeps it out of the tab order where it has nothing to do, so a reader at
|
|
1763
|
+
// the wide layout never meets a button for a strip that is not there.
|
|
1764
|
+
//
|
|
1765
|
+
// `aria-expanded` RATHER THAN `aria-pressed`, on §17d's own distinction: this
|
|
1766
|
+
// discloses a region that is already in the markup rather than toggling a
|
|
1767
|
+
// mode. The label flips with it — see `WorkspaceWords.hideCanvas`.
|
|
1768
|
+
const canvasOpen = options.canvasOpen === true;
|
|
1769
|
+
const strip = element('button', {
|
|
1770
|
+
type: 'button',
|
|
1771
|
+
class: 'ig-canvas-strip',
|
|
1772
|
+
'data-ig-command': 'canvas-strip',
|
|
1773
|
+
'aria-expanded': canvasOpen ? 'true' : 'false',
|
|
1774
|
+
}, [canvasOpen ? options.words.hideCanvas : options.words.showCanvas]);
|
|
1775
|
+
// §17k'S MIDDLE ROW IS TWO RULES AND ONLY ONE OF THEM IS A STATE. *"Opened by
|
|
1776
|
+
// selection, Escape to dismiss"*: the first is a question about `selection`,
|
|
1777
|
+
// which this function already has, so the panel is lifted when something is
|
|
1778
|
+
// selected and the reader has not dismissed it since. Storing the openness
|
|
1779
|
+
// instead would be a second copy of the selection, free to disagree with it.
|
|
1780
|
+
const lifted = selection.kind !== 'none' && options.inspectorDismissed !== true;
|
|
1781
|
+
// WHETHER THE CANVAS DREW A KEY, SO THE SURFACE DRAWS ONE AND NOT TWO. The
|
|
1782
|
+
// rail and the canvas are both viewers, so both draw the grammar legend, and
|
|
1783
|
+
// the workspace showed the same five line styles twice, side by side, at the
|
|
1784
|
+
// same moment. It is the same defect `chrome: false` already answers one line
|
|
1785
|
+
// higher — §16's panel has ONE header — and the answer is the same shape.
|
|
1786
|
+
//
|
|
1787
|
+
// THE CANVAS KEEPS IT AND THE RAIL YIELDS, which is the way round the content
|
|
1788
|
+
// decides: four of the five entries are LINE styles, and the rail draws no
|
|
1789
|
+
// lines at all — it draws badges. The key belongs beside the strokes it is
|
|
1790
|
+
// about. It is also the cheaper zone to spend it in, at one or two wrapped
|
|
1791
|
+
// rows of a wide column rather than four rows of a 390 rail the order needs.
|
|
1792
|
+
//
|
|
1793
|
+
// AN ATTRIBUTE RATHER THAN AN OPTION, because the rail must get the key BACK
|
|
1794
|
+
// in the two states where the canvas has none, and only one of those is
|
|
1795
|
+
// knowable here. The refusal is: above its node budget the canvas draws
|
|
1796
|
+
// capsules and no viewer, so there is no legend in that zone to defer to.
|
|
1797
|
+
// The other is §17k's collapsed canvas, which is a question about the box
|
|
1798
|
+
// this surface was given — the container query is the only party that can
|
|
1799
|
+
// answer it, so the restore is a rule in the sheet rather than a branch here.
|
|
1800
|
+
// Suppressing the markup with an option would put the key out of reach of
|
|
1801
|
+
// both.
|
|
1802
|
+
const canvasLegend = canvas.ladder.tier === 'direct' ? 'drawn' : 'absent';
|
|
1803
|
+
const narrowState = ` data-canvas="${canvasOpen ? 'open' : 'strip'}"` +
|
|
1804
|
+
` data-canvas-legend="${canvasLegend}"` +
|
|
1805
|
+
` data-inspector="${lifted ? 'lifted' : 'dismissed'}"`;
|
|
1806
|
+
// §17a'S RAIL FOOTER, FROM THE LADDER THE CANVAS ALREADY DERIVED. `isolated`
|
|
1807
|
+
// is a property of the whole document rather than of the tier the canvas
|
|
1808
|
+
// settled on, so the rail and the canvas cannot disagree about it: there is
|
|
1809
|
+
// one derivation and both zones read it.
|
|
1810
|
+
//
|
|
1811
|
+
// NAMED `railFooterSpec`, NOT `railFooter`, because `railFooter` is the
|
|
1812
|
+
// function and — more to the point — the bare identifier `rail` a few lines
|
|
1813
|
+
// below is the WINDOW (`options.rail`, a `RailWindowOptions`), which is a
|
|
1814
|
+
// different value from `options.words.rail`. Two things called rail in one
|
|
1815
|
+
// scope is a reading hazard worth one name, not one comment per use.
|
|
1816
|
+
const railFooterSpec = railFooter(canvas.ladder.isolated, options.words.rail, isolatedListId);
|
|
1262
1817
|
const inspector = inspectorView(document, selection);
|
|
1818
|
+
// §17c'S LOOP, OVER THE RAIL THE READER IS ACTUALLY LOOKING AT.
|
|
1819
|
+
// `reevaluateView` is handed the WINDOWED scene, so a chip whose row is
|
|
1820
|
+
// scrolled out of the window resolves to no row and draws nowhere — which is
|
|
1821
|
+
// right: a chip is a mark ON a row, and there is no row. The summary still
|
|
1822
|
+
// counts it, because the summary is about the ORDER and not about the window.
|
|
1823
|
+
//
|
|
1824
|
+
// NO WORDS, NO LOOP. `words.change` is optional and its absence is the whole
|
|
1825
|
+
// reason there is no default sentence here; see `ChangeWords`.
|
|
1826
|
+
const changeWords = options.words.change;
|
|
1827
|
+
const orderStatus = options.orderStatus ?? 'settled';
|
|
1828
|
+
// EVERY REF THE ORDER HAS, WINDOWED OR NOT — and taken from the UNFILTERED
|
|
1829
|
+
// document on purpose, so a row the audit filter is hiding counts as expected
|
|
1830
|
+
// for the same reason an off-window row does. The rail this render hands the
|
|
1831
|
+
// viewer is narrowed twice over; neither narrowing is a disagreement between
|
|
1832
|
+
// the projection and the change, and only this function holds the order both
|
|
1833
|
+
// narrowings started from.
|
|
1834
|
+
const inTheOrder = new Set();
|
|
1835
|
+
for (const slot of document.order.slots) {
|
|
1836
|
+
inTheOrder.add(slot.lead);
|
|
1837
|
+
for (const member of slot.members)
|
|
1838
|
+
inTheOrder.add(member);
|
|
1839
|
+
}
|
|
1840
|
+
for (const exclusion of document.order.excluded)
|
|
1841
|
+
inTheOrder.add(exclusion.key);
|
|
1842
|
+
const change = reevaluateView(changeWords === undefined ? null : options.change, railRender.scene, orderStatus, inTheOrder);
|
|
1843
|
+
const deltaByKey = new Map(changeWords === undefined
|
|
1844
|
+
? []
|
|
1845
|
+
: change.chips.map((chip) => [
|
|
1846
|
+
chip.key,
|
|
1847
|
+
{ chip, words: changeWords, kind: deltaKind(chip) },
|
|
1848
|
+
]));
|
|
1849
|
+
// §17d'S PANEL IS AN OVERLAY OVER THE CANVAS, NOT A FOURTH ZONE.
|
|
1850
|
+
//
|
|
1851
|
+
// `RULINGS.md` §3: *"It is a transient overlay anchored to the header count,
|
|
1852
|
+
// floating over the canvas, 520px wide, dismissed on Escape."* It used to be
|
|
1853
|
+
// concatenated into the INSPECTOR zone, above the selection — §17a fixes
|
|
1854
|
+
// three zones and none of them is the audit's, so it had nowhere to go and
|
|
1855
|
+
// took a share of the one next to it. At six findings the inspector went
|
|
1856
|
+
// below the fold.
|
|
1857
|
+
//
|
|
1858
|
+
// OVER THE CANVAS AND NEVER THE RAIL, and the ruling gives the reason rather
|
|
1859
|
+
// than a preference: *"judging a finding is largely a question of what it
|
|
1860
|
+
// would do to the order, so the order has to stay readable while you
|
|
1861
|
+
// judge."* The canvas is also the zone §17f already designed to refuse.
|
|
1862
|
+
//
|
|
1863
|
+
// BUILT ONLY WHEN OPEN. "Transient" is the word, and a panel rendered hidden
|
|
1864
|
+
// is still in the accessibility tree and still in the tab order — a reader
|
|
1865
|
+
// on the keyboard would walk a list of findings they never opened.
|
|
1866
|
+
const auditPanel = !auditOpen
|
|
1867
|
+
? ''
|
|
1868
|
+
: (() => {
|
|
1869
|
+
if (overlay === null)
|
|
1870
|
+
return '';
|
|
1871
|
+
const refused = renderEncodingRefusedBlock(options.audit?.encodingRefused ?? [], {
|
|
1872
|
+
words: options.words.audit,
|
|
1873
|
+
known,
|
|
1874
|
+
issueUrl: options.issueUrl,
|
|
1875
|
+
});
|
|
1876
|
+
// `filtered` REACHES THE PANEL, and the first draft forgot it. The chip drew
|
|
1877
|
+
// `aria-pressed="false"` while the rail beside it was visibly narrowed —
|
|
1878
|
+
// a control lying about its own state, which is the exact shape §2 wrote
|
|
1879
|
+
// its ruling against.
|
|
1880
|
+
const panel = renderAuditPanel(overlay, {
|
|
1881
|
+
words: options.words.audit,
|
|
1882
|
+
known,
|
|
1883
|
+
edges: drawnEdges,
|
|
1884
|
+
filtered,
|
|
1885
|
+
});
|
|
1886
|
+
// THE REFUSAL LEADS. It is the finding that says the other three cannot
|
|
1887
|
+
// be trusted for that issue — its edges were never read — so a reader
|
|
1888
|
+
// meeting the list first would be reading conclusions drawn from a
|
|
1889
|
+
// declaration nobody could parse.
|
|
1890
|
+
const members = [refused, panel].filter((spec) => spec !== null);
|
|
1891
|
+
// NO EMPTY REGION. A wrapper drawn around nothing is a heading-less box
|
|
1892
|
+
// taking a declared share of a column whose space belongs to the
|
|
1893
|
+
// selection — the same call `renderAuditPanel` makes about a list of
|
|
1894
|
+
// nothing, one level up.
|
|
1895
|
+
if (members.length === 0)
|
|
1896
|
+
return '';
|
|
1897
|
+
return renderMarkup(element('div', { class: 'ig-audit-overlay' }, members));
|
|
1898
|
+
})();
|
|
1263
1899
|
const markup = [
|
|
1264
|
-
|
|
1265
|
-
|
|
1900
|
+
// THE ORDER'S STATUS, ON THE SURFACE ROOT. `mount.ts` already publishes the
|
|
1901
|
+
// same value on the mounted element for a host to read; this puts it where
|
|
1902
|
+
// the stylesheet can reach it in the UNMOUNTED rendering too, so §17c's
|
|
1903
|
+
// "greyed one step, and labelled" is drawn by whoever renders rather than
|
|
1904
|
+
// only by whoever mounts. Same vocabulary, verbatim, so the two agree.
|
|
1905
|
+
// STAMPED ONLY WHERE THE LABEL CAN BE DRAWN, and the two really are one
|
|
1906
|
+
// treatment. §17c's held state is a greyed rail AND the word saying why —
|
|
1907
|
+
// "a stale-but-labelled order beats a half-computed one" — and half of that
|
|
1908
|
+
// is worse than neither: a dimmed order with no explanation is exactly the
|
|
1909
|
+
// defect the label exists to prevent. `mountWorkspace` passes the store's
|
|
1910
|
+
// status unconditionally, so a host that has not supplied `words.change`
|
|
1911
|
+
// would otherwise get the greying with the label suppressed one branch
|
|
1912
|
+
// below, on the very path the optional vocabulary exists to keep working.
|
|
1913
|
+
//
|
|
1914
|
+
// THE FACT IS NOT LOST TO A HOST. `mount.ts` publishes the same status on
|
|
1915
|
+
// the element the host holds, which is the reader this attribute was never
|
|
1916
|
+
// for: this one is the stylesheet's hook, so it is absent exactly when the
|
|
1917
|
+
// stylesheet must not act.
|
|
1918
|
+
//
|
|
1919
|
+
// §17k'S TWO READER DECISIONS RIDE THE SAME ROOT, and for the same reason
|
|
1920
|
+
// `data-order` does: the stylesheet is the only party that knows which of
|
|
1921
|
+
// the three layouts is drawn, so the surface publishes what the reader
|
|
1922
|
+
// asked for and each container block reads the one it has a use for.
|
|
1923
|
+
// Neither attribute means anything at the width where its zone is a zone.
|
|
1924
|
+
//
|
|
1925
|
+
// BOTH ARE ALWAYS STAMPED, unlike `data-order`. That one is withheld where
|
|
1926
|
+
// its label cannot be drawn — a greyed rail with no word is worse than
|
|
1927
|
+
// neither — and these two have no such pair: the value is the full state,
|
|
1928
|
+
// and an absent attribute would read as the strip being open and the
|
|
1929
|
+
// inspector being lifted, which is the opposite of both defaults.
|
|
1930
|
+
changeWords === undefined
|
|
1931
|
+
? `<div class="ig-workspace"${narrowState}>`
|
|
1932
|
+
: `<div class="ig-workspace" data-order="${orderStatus}"${narrowState}>`,
|
|
1933
|
+
zone('header', [
|
|
1934
|
+
headerMarkup(document.host, overlay === null ? '' : renderAuditHeader(overlay, { open: auditOpen }), options.words),
|
|
1935
|
+
// §17c'S SUMMARY LIVES IN THE HEADER, NOT AT THE TOP OF THE RAIL, and
|
|
1936
|
+
// the frame draws them adjacent so this is a real departure worth the
|
|
1937
|
+
// sentence. The rail zone is the SCROLLER — `mount.ts` preserves
|
|
1938
|
+
// `railBefore.scrollTop` across every redraw — so a summary placed
|
|
1939
|
+
// inside it scrolls away from the reader, and §17c's rule is that the
|
|
1940
|
+
// summary "persists until the next edit or an explicit dismiss". A
|
|
1941
|
+
// summary that survives a dismiss but not a scroll is not persistent.
|
|
1942
|
+
//
|
|
1943
|
+
// The header is where #135 settled that a workspace-wide fact is
|
|
1944
|
+
// stated, and it is stated in exactly one zone. The chips carry the
|
|
1945
|
+
// adjacency instead: cause in the header, effect on the row.
|
|
1946
|
+
// "A STALE-BUT-LABELLED ORDER BEATS A HALF-COMPUTED ONE." §17c greys the
|
|
1947
|
+
// held rail and says WHY beside it, and the greying without the label
|
|
1948
|
+
// is the half of that pair which communicates nothing.
|
|
1949
|
+
//
|
|
1950
|
+
// THE LABEL IS `summarySpec`'S, INSIDE ITS LIVE REGION, and it was a
|
|
1951
|
+
// sibling of that region until a reader pointed out this made it
|
|
1952
|
+
// silent. The store clears `lastChange` when a write goes PENDING, so
|
|
1953
|
+
// the region is empty in exactly the state the label exists for — a
|
|
1954
|
+
// sighted reader got the greying and the sentence, and a screen-reader
|
|
1955
|
+
// user was told nothing about the ranks having gone stale.
|
|
1956
|
+
changeWords === undefined
|
|
1957
|
+
? ''
|
|
1958
|
+
: renderMarkup(summarySpec(change.summary, changeWords, { held: change.held })),
|
|
1959
|
+
].join('')),
|
|
1266
1960
|
zone('rail', [
|
|
1267
1961
|
railSpacer(rail.before, 'before'),
|
|
1268
|
-
renderMarkup(markRail(railRender.scene.root, (key) => severityByKey.get(key))),
|
|
1962
|
+
renderMarkup(markRail(railRender.scene.root, (key) => severityByKey.get(key), (key) => deltaByKey.get(key))),
|
|
1269
1963
|
railSpacer(rail.after, 'after'),
|
|
1964
|
+
railFooterSpec === null ? '' : renderMarkup(railFooterSpec),
|
|
1270
1965
|
].join('')),
|
|
1271
|
-
zone('canvas',
|
|
1272
|
-
|
|
1966
|
+
zone('canvas',
|
|
1967
|
+
// THE CAPTION LEADS THE ZONE, above the graph, exactly as frame 17a
|
|
1968
|
+
// draws it — and it is assembled HERE rather than inside the ladder.
|
|
1969
|
+
// `renderScaleLadder` is also used standalone, where `chrome: false` is
|
|
1970
|
+
// the deliberate answer, and the words this row needs are the
|
|
1971
|
+
// workspace's. Both halves are already-rendered markup, which is the
|
|
1972
|
+
// rule `zone` exists under: it writes the only hand-authored tag in this
|
|
1973
|
+
// package and takes no caller value.
|
|
1974
|
+
//
|
|
1975
|
+
// THE STRIP CONTROL LEADS THE ZONE, above the caption, because below
|
|
1976
|
+
// `1120` it is the ONLY thing in the zone the reader can reach: the
|
|
1977
|
+
// sheet hides every sibling while the canvas is collapsed. Drawn last it
|
|
1978
|
+
// would be the last stop in the zone's tab order at the wide layout,
|
|
1979
|
+
// where it is `display: none` and therefore no stop at all — so the
|
|
1980
|
+
// position is chosen for the width where it is drawn.
|
|
1981
|
+
//
|
|
1982
|
+
// AND §17d'S PANEL IS LAST so it paints over the graph rather than under
|
|
1983
|
+
// it, and it is INSIDE this zone so `workspace/styles.ts` can position it
|
|
1984
|
+
// against the zone's box — which is what keeps it off the rail without a
|
|
1985
|
+
// magic number. The strip's hiding rule names it as an exception, or a
|
|
1986
|
+
// collapsed canvas would take the audit panel down with it.
|
|
1987
|
+
[strip, toolbar]
|
|
1988
|
+
.map((spec) => (spec === null ? '' : renderMarkup(spec)))
|
|
1989
|
+
.join('') +
|
|
1990
|
+
canvas.markup +
|
|
1991
|
+
auditPanel),
|
|
1992
|
+
zone('inspector',
|
|
1993
|
+
// §17d'S AUDIT IS THE SELECTION'S SIBLING, AND THEY SHARE ONE SCROLL
|
|
1994
|
+
// TRACK. Sibling rather than child so each surface keeps its own padding
|
|
1995
|
+
// and reads as a peer of the selection rather than part of it — their
|
|
1996
|
+
// headings are `h2`s beside the inspector's for the same reason.
|
|
1997
|
+
//
|
|
1998
|
+
// THE AUDIT REGION TAKES A SHARE; THE OTHER TWO SIBLINGS DO NOT. #177
|
|
1999
|
+
// settled this. The zone is still ONE scroll track, and inside it
|
|
2000
|
+
// `workspace/styles.ts` bounds the region alone at half the column,
|
|
2001
|
+
// scrolling itself past that. So a long audit can no longer push the
|
|
2002
|
+
// selection detail further than half the column down, whatever the
|
|
2003
|
+
// finding count — the detail's head is always above the fold, which is
|
|
2004
|
+
// the bound, not a promise that all of it fits. `.ig-inspector` and the
|
|
2005
|
+
// `.ig-chrome` the mount appends keep the track's own scrolling and
|
|
2006
|
+
// cannot be clipped at any height.
|
|
2007
|
+
//
|
|
2008
|
+
// THE SHARE IS `workspace/styles.ts`', NOT `audit/styles.ts`'. An earlier
|
|
2009
|
+
// revision of this comment said otherwise; that file states in as many
|
|
2010
|
+
// words that its leaves declare no size of their own, and the
|
|
2011
|
+
// misattribution is what nearly let §17d's second surface arrive with no
|
|
2012
|
+
// bound at all.
|
|
2013
|
+
//
|
|
2014
|
+
// WHY THIS ONE. Being drawn FIRST is half the reason — it is what puts
|
|
2015
|
+
// its length in front of the detail — and being the zone's only GLOBAL
|
|
2016
|
+
// member is the other. The two below it are about what the reader is
|
|
2017
|
+
// doing; this is about the document.
|
|
2018
|
+
//
|
|
2019
|
+
// A two-pane version was built and reverted before that: it clipped
|
|
2020
|
+
// `.ig-chrome`, a THIRD sibling. Bounding one member inside the single
|
|
2021
|
+
// track is what avoids needing panes at all.
|
|
2022
|
+
//
|
|
2023
|
+
// BOTH SIDES ARE ALREADY-RENDERED MARKUP, which is the rule `zone` exists
|
|
2024
|
+
// under: it writes the only hand-authored tag in this package and takes
|
|
2025
|
+
// no caller value, and everything with a dynamic value in it went through
|
|
2026
|
+
// `renderMarkup`.
|
|
2027
|
+
//
|
|
2028
|
+
// `known` IS THE DRAWN DOCUMENT'S KEYS, the same set `holdRow` withholds
|
|
2029
|
+
// its subject control on, and NOT the audit's: a host audits what it
|
|
2030
|
+
// holds and draws a page of it, so a ref can be audited and still have no
|
|
2031
|
+
// row here. The panel cannot see that difference and is told.
|
|
2032
|
+
// §17d IS ONE REGION HOLDING TWO SURFACES, AND THAT IS WHY THEY ARE
|
|
2033
|
+
// WRAPPED RATHER THAN CONCATENATED. `audit/refused.ts` draws the fourth
|
|
2034
|
+
// class outside the findings list — SPEC surfaces a refusal "on the issue
|
|
2035
|
+
// itself" — so the zone would otherwise gain a FOURTH sibling, and #177's
|
|
2036
|
+
// answer does not survive that arithmetic: two siblings each bounded at
|
|
2037
|
+
// half the column can take all of it between them and push the selection
|
|
2038
|
+
// detail entirely below the fold, which is worse than the state #177
|
|
2039
|
+
// fixed rather than a smaller version of it.
|
|
2040
|
+
//
|
|
2041
|
+
// SO THE BOUND MOVES UP ONE LEVEL, and #177's rule is kept rather than
|
|
2042
|
+
// restated: exactly one sibling of this zone is bounded, and it is now
|
|
2043
|
+
// the REGION. The block and the panel are unbounded inside it and share
|
|
2044
|
+
// its half between them, which is also what the artboard draws — §17d
|
|
2045
|
+
// puts the findings list and the refusal side by side as one section.
|
|
2046
|
+
// `workspace/styles.ts` carries the share; neither leaf declares a size.
|
|
2047
|
+
renderMarkup(inspectorSpec(inspector, {
|
|
1273
2048
|
words: options.words,
|
|
2049
|
+
audit: overlay,
|
|
1274
2050
|
known,
|
|
1275
2051
|
leadOf,
|
|
1276
2052
|
draft: options.draft ?? IDLE_CREATE_DRAFT,
|
|
@@ -1315,6 +2091,11 @@ export function renderWorkspace(input, options) {
|
|
|
1315
2091
|
// it.
|
|
1316
2092
|
edgeOverlayStylesheet,
|
|
1317
2093
|
...(overlay === null ? [] : [auditStylesheet]),
|
|
2094
|
+
// UNCONDITIONAL, for the reason `edgeOverlayStylesheet` above records:
|
|
2095
|
+
// the summary and the chips arrive with the NEXT snapshot after an edit,
|
|
2096
|
+
// so a sheet installed only once something has already changed is missing
|
|
2097
|
+
// on exactly the render that first draws one.
|
|
2098
|
+
reevaluateStylesheet,
|
|
1318
2099
|
workspaceStylesheet,
|
|
1319
2100
|
].join('\n'),
|
|
1320
2101
|
// CONCATENATED, NOT DEDUPED — and the dedupe that used to sit here is worth
|
|
@@ -1332,7 +2113,17 @@ export function renderWorkspace(input, options) {
|
|
|
1332
2113
|
// Nothing replaces it, because nothing needs to: the zones now receive an
|
|
1333
2114
|
// already-sound document, so they contribute nothing to this list at all.
|
|
1334
2115
|
// `reports every occurrence, and the zones add nothing` pins both halves.
|
|
1335
|
-
|
|
2116
|
+
// THE PLACEMENT DIAGNOSTICS ARE THE CHANGE'S, and they are only worth
|
|
2117
|
+
// reporting because `expected` above makes them mean what they say. Dropped
|
|
2118
|
+
// entirely, a host got no signal that its projection and its change
|
|
2119
|
+
// disagree; forwarded unfiltered, every scroll past a changed row would
|
|
2120
|
+
// have reported one.
|
|
2121
|
+
diagnostics: [
|
|
2122
|
+
...sound.diagnostics,
|
|
2123
|
+
...railRender.diagnostics,
|
|
2124
|
+
...canvas.diagnostics,
|
|
2125
|
+
...change.diagnostics,
|
|
2126
|
+
],
|
|
1336
2127
|
};
|
|
1337
2128
|
}
|
|
1338
2129
|
//# sourceMappingURL=render.js.map
|