@issuegraph/editor 0.1.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/LICENSE +201 -0
- package/README.md +238 -0
- package/dist/audit/findings.d.ts +225 -0
- package/dist/audit/findings.d.ts.map +1 -0
- package/dist/audit/findings.js +348 -0
- package/dist/audit/findings.js.map +1 -0
- package/dist/audit/styles.d.ts +37 -0
- package/dist/audit/styles.d.ts.map +1 -0
- package/dist/audit/styles.js +72 -0
- package/dist/audit/styles.js.map +1 -0
- package/dist/audit/surface.d.ts +155 -0
- package/dist/audit/surface.d.ts.map +1 -0
- package/dist/audit/surface.js +202 -0
- package/dist/audit/surface.js.map +1 -0
- package/dist/create/draft.d.ts +119 -0
- package/dist/create/draft.d.ts.map +1 -0
- package/dist/create/draft.js +114 -0
- package/dist/create/draft.js.map +1 -0
- package/dist/create/keys.d.ts +225 -0
- package/dist/create/keys.d.ts.map +1 -0
- package/dist/create/keys.js +228 -0
- package/dist/create/keys.js.map +1 -0
- package/dist/create/placement.d.ts +69 -0
- package/dist/create/placement.d.ts.map +1 -0
- package/dist/create/placement.js +76 -0
- package/dist/create/placement.js.map +1 -0
- package/dist/firstpass/batch.d.ts +190 -0
- package/dist/firstpass/batch.d.ts.map +1 -0
- package/dist/firstpass/batch.js +143 -0
- package/dist/firstpass/batch.js.map +1 -0
- package/dist/firstpass/candidates.d.ts +128 -0
- package/dist/firstpass/candidates.d.ts.map +1 -0
- package/dist/firstpass/candidates.js +39 -0
- package/dist/firstpass/candidates.js.map +1 -0
- package/dist/firstpass/keys.d.ts +90 -0
- package/dist/firstpass/keys.d.ts.map +1 -0
- package/dist/firstpass/keys.js +112 -0
- package/dist/firstpass/keys.js.map +1 -0
- package/dist/firstpass/queue.d.ts +183 -0
- package/dist/firstpass/queue.d.ts.map +1 -0
- package/dist/firstpass/queue.js +159 -0
- package/dist/firstpass/queue.js.map +1 -0
- package/dist/firstpass/render.d.ts +100 -0
- package/dist/firstpass/render.d.ts.map +1 -0
- package/dist/firstpass/render.js +216 -0
- package/dist/firstpass/render.js.map +1 -0
- package/dist/firstpass/styles.d.ts +36 -0
- package/dist/firstpass/styles.d.ts.map +1 -0
- package/dist/firstpass/styles.js +148 -0
- package/dist/firstpass/styles.js.map +1 -0
- package/dist/firstpass/view.d.ts +82 -0
- package/dist/firstpass/view.d.ts.map +1 -0
- package/dist/firstpass/view.js +60 -0
- package/dist/firstpass/view.js.map +1 -0
- package/dist/firstpass/words.d.ts +105 -0
- package/dist/firstpass/words.d.ts.map +1 -0
- package/dist/firstpass/words.js +28 -0
- package/dist/firstpass/words.js.map +1 -0
- package/dist/index.d.ts +150 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +147 -0
- package/dist/index.js.map +1 -0
- package/dist/overlay/grammar.d.ts +259 -0
- package/dist/overlay/grammar.d.ts.map +1 -0
- package/dist/overlay/grammar.js +223 -0
- package/dist/overlay/grammar.js.map +1 -0
- package/dist/overlay/render.d.ts +125 -0
- package/dist/overlay/render.d.ts.map +1 -0
- package/dist/overlay/render.js +439 -0
- package/dist/overlay/render.js.map +1 -0
- package/dist/overlay/styles.d.ts +41 -0
- package/dist/overlay/styles.d.ts.map +1 -0
- package/dist/overlay/styles.js +124 -0
- package/dist/overlay/styles.js.map +1 -0
- package/dist/picker/render.d.ts +80 -0
- package/dist/picker/render.d.ts.map +1 -0
- package/dist/picker/render.js +112 -0
- package/dist/picker/render.js.map +1 -0
- package/dist/picker/styles.d.ts +26 -0
- package/dist/picker/styles.d.ts.map +1 -0
- package/dist/picker/styles.js +116 -0
- package/dist/picker/styles.js.map +1 -0
- package/dist/picker/view.d.ts +135 -0
- package/dist/picker/view.d.ts.map +1 -0
- package/dist/picker/view.js +117 -0
- package/dist/picker/view.js.map +1 -0
- package/dist/picker/words.d.ts +53 -0
- package/dist/picker/words.d.ts.map +1 -0
- package/dist/picker/words.js +28 -0
- package/dist/picker/words.js.map +1 -0
- package/dist/reevaluate/render.d.ts +77 -0
- package/dist/reevaluate/render.d.ts.map +1 -0
- package/dist/reevaluate/render.js +211 -0
- package/dist/reevaluate/render.js.map +1 -0
- package/dist/reevaluate/styles.d.ts +24 -0
- package/dist/reevaluate/styles.d.ts.map +1 -0
- package/dist/reevaluate/styles.js +182 -0
- package/dist/reevaluate/styles.js.map +1 -0
- package/dist/reevaluate/view.d.ts +102 -0
- package/dist/reevaluate/view.d.ts.map +1 -0
- package/dist/reevaluate/view.js +111 -0
- package/dist/reevaluate/view.js.map +1 -0
- package/dist/reevaluate/words.d.ts +54 -0
- package/dist/reevaluate/words.d.ts.map +1 -0
- package/dist/reevaluate/words.js +29 -0
- package/dist/reevaluate/words.js.map +1 -0
- package/dist/scale/commands.d.ts +60 -0
- package/dist/scale/commands.d.ts.map +1 -0
- package/dist/scale/commands.js +49 -0
- package/dist/scale/commands.js.map +1 -0
- package/dist/scale/ladder.d.ts +141 -0
- package/dist/scale/ladder.d.ts.map +1 -0
- package/dist/scale/ladder.js +243 -0
- package/dist/scale/ladder.js.map +1 -0
- package/dist/scale/render.d.ts +85 -0
- package/dist/scale/render.d.ts.map +1 -0
- package/dist/scale/render.js +194 -0
- package/dist/scale/render.js.map +1 -0
- package/dist/scale/styles.d.ts +21 -0
- package/dist/scale/styles.d.ts.map +1 -0
- package/dist/scale/styles.js +105 -0
- package/dist/scale/styles.js.map +1 -0
- package/dist/workspace/inspector.d.ts +117 -0
- package/dist/workspace/inspector.d.ts.map +1 -0
- package/dist/workspace/inspector.js +140 -0
- package/dist/workspace/inspector.js.map +1 -0
- package/dist/workspace/rail.d.ts +122 -0
- package/dist/workspace/rail.d.ts.map +1 -0
- package/dist/workspace/rail.js +216 -0
- package/dist/workspace/rail.js.map +1 -0
- package/dist/workspace/render.d.ts +139 -0
- package/dist/workspace/render.d.ts.map +1 -0
- package/dist/workspace/render.js +366 -0
- package/dist/workspace/render.js.map +1 -0
- package/dist/workspace/selection.d.ts +92 -0
- package/dist/workspace/selection.d.ts.map +1 -0
- package/dist/workspace/selection.js +83 -0
- package/dist/workspace/selection.js.map +1 -0
- package/dist/workspace/styles.d.ts +39 -0
- package/dist/workspace/styles.d.ts.map +1 -0
- package/dist/workspace/styles.js +313 -0
- package/dist/workspace/styles.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The audit's structural stylesheet.
|
|
3
|
+
*
|
|
4
|
+
* Same contract as the viewer's and the ladder's, and for the same reason: it
|
|
5
|
+
* carries layout, weight and state, never a value. Every colour and length is a
|
|
6
|
+
* `var(--ig-…)` the host's theme already resolves, so a second theme keeps
|
|
7
|
+
* working here. `styles.test.ts` scans these bytes for a literal and for a
|
|
8
|
+
* token the theme does not define.
|
|
9
|
+
*
|
|
10
|
+
* NO TOKENS OF ITS OWN, WHICH IS A REVERSAL WORTH RECORDING. An earlier draft
|
|
11
|
+
* declared `--ig-audit-bar` and shipped an `auditThemeCss()` to default it. The
|
|
12
|
+
* failure mode that killed it is the one this package's other stylesheet test
|
|
13
|
+
* exists to catch: a host that installs the viewer's theme and forgets a second
|
|
14
|
+
* one gets a `var()` resolving to nothing, and the bar silently does not draw —
|
|
15
|
+
* "the failure that looks like a styling bug for weeks". Reading the palette's
|
|
16
|
+
* own gold has no such state, and a host who wants a different attention colour
|
|
17
|
+
* still has one: target the attribute in their own CSS, which needs no API from
|
|
18
|
+
* us. Fewer exports is also the safer direction for a package that can add one
|
|
19
|
+
* later and never take one back.
|
|
20
|
+
*
|
|
21
|
+
* THE BAR IS AN INSET BOX SHADOW rather than a border, because a border changes
|
|
22
|
+
* a row's box and every affected row would shift by its width the moment a
|
|
23
|
+
* finding appeared. §17d asks for a count that never moves; a rail that jumps
|
|
24
|
+
* would be the same broken promise one element over.
|
|
25
|
+
*
|
|
26
|
+
* SCOPING, THE ONE PLACE THIS DEPARTS FROM ITS SIBLINGS. They scope every
|
|
27
|
+
* selector under an `.ig-` class because they own the elements they draw. The
|
|
28
|
+
* bar does not: it lands on a row the VIEWER rendered, and adding a class there
|
|
29
|
+
* means rewriting a `class` attribute this layer has no business touching. So
|
|
30
|
+
* it is scoped by {@link AUDIT_SEVERITY_ATTRIBUTE} instead — this package's own
|
|
31
|
+
* namespaced name, which bounds it to elements a host stamped on purpose.
|
|
32
|
+
*
|
|
33
|
+
* There is no transition, no animation and no `@keyframes` here, and there is a
|
|
34
|
+
* test that says so.
|
|
35
|
+
*/
|
|
36
|
+
import { AUDIT_SEVERITY_ATTRIBUTE } from "./surface.js";
|
|
37
|
+
export const auditStylesheet = `
|
|
38
|
+
.ig-audit {
|
|
39
|
+
align-items: center;
|
|
40
|
+
display: inline-flex;
|
|
41
|
+
font-family: var(--ig-font-ui);
|
|
42
|
+
font-size: var(--ig-font-size-small);
|
|
43
|
+
gap: var(--ig-space-tight);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.ig-audit-toggle {
|
|
47
|
+
align-items: center;
|
|
48
|
+
background: none;
|
|
49
|
+
border: 0;
|
|
50
|
+
color: var(--ig-text-muted);
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
display: inline-flex;
|
|
53
|
+
font: inherit;
|
|
54
|
+
gap: var(--ig-space-tight);
|
|
55
|
+
padding: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.ig-audit-toggle[aria-pressed='true'] {
|
|
59
|
+
color: var(--ig-text);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.ig-audit-count {
|
|
63
|
+
color: var(--ig-text);
|
|
64
|
+
font-family: var(--ig-font-mono);
|
|
65
|
+
font-variant-numeric: tabular-nums;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
[${AUDIT_SEVERITY_ATTRIBUTE}] {
|
|
69
|
+
box-shadow: inset var(--ig-stroke) 0 0 0 var(--ig-edge-serialize-with);
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
//# sourceMappingURL=styles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.js","sourceRoot":"","sources":["../../src/audit/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAExD,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+B5B,wBAAwB;;;CAG1B,CAAC"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ambient half of the audit: attention without nagging.
|
|
3
|
+
*
|
|
4
|
+
* §17d fixes both what this is and what it may never be. It is a **persistent
|
|
5
|
+
* header count** that never moves and never animates, a **left-bar** on the
|
|
6
|
+
* rail rows a finding touches, and an audit **filter**. It is explicitly not a
|
|
7
|
+
* modal, a toast, a red banner, an animated badge, anything that blocks the
|
|
8
|
+
* edit loop, or an auto-fix — every finding is a judgment call, so the surface
|
|
9
|
+
* offers navigation and never a remedy.
|
|
10
|
+
*
|
|
11
|
+
* §17a settles the last of those: audit is *"a filter, not a mode — a mode you
|
|
12
|
+
* must enter is a mode you forget, and encoding errors need to be visible while
|
|
13
|
+
* you work."* So the count is always drawn, including at zero, and the toggle
|
|
14
|
+
* narrows what is listed rather than switching the workspace into something
|
|
15
|
+
* else.
|
|
16
|
+
*
|
|
17
|
+
* ## Why the bar is CSS on an attribute rather than a drawn element
|
|
18
|
+
*
|
|
19
|
+
* The rows belong to `@issuegraph/viewer`, which stamps `KEY_ATTRIBUTE` on
|
|
20
|
+
* them. Layer 2 composes layer 1 through its public surface, and the viewer's
|
|
21
|
+
* markup primitive is deliberately not on it — so an overlay drawn from out
|
|
22
|
+
* here would have to re-implement HTML escaping, which is duplication with an
|
|
23
|
+
* injection shape rather than a mirror that merely drifts.
|
|
24
|
+
*
|
|
25
|
+
* An attribute plus a stylesheet needs neither. {@link auditRowAttributes}
|
|
26
|
+
* answers what an affected row carries, `./styles.ts` draws the bar from it,
|
|
27
|
+
* and the whole exchange is data. It also settles nothing about HOW
|
|
28
|
+
* overlays attach, which is a separate decision with its own change.
|
|
29
|
+
*
|
|
30
|
+
* ## Nothing here renders host text
|
|
31
|
+
*
|
|
32
|
+
* The only value that reaches the markup is a COUNT, and a count is a number.
|
|
33
|
+
* That is a deliberate boundary rather than a happy accident: a finding's
|
|
34
|
+
* `detail` is prose about issues a host supplied, so drawing it here would put
|
|
35
|
+
* an escaper in a package whose seam says it may not have one. Findings travel
|
|
36
|
+
* as data; whatever lists them owns their escaping.
|
|
37
|
+
*
|
|
38
|
+
* @see https://github.com/autnmy/issuegraph/blob/main/SPEC.md
|
|
39
|
+
*/
|
|
40
|
+
import type { IssueRef } from '@issuegraph/store';
|
|
41
|
+
import type { AuditClass, AuditFinding, AuditInput, AuditSeverity } from './findings.ts';
|
|
42
|
+
/**
|
|
43
|
+
* The attribute an affected rail row carries. Its value is the severity that
|
|
44
|
+
* won the row, so a host can style the four differently without this package
|
|
45
|
+
* deciding that they should be.
|
|
46
|
+
*/
|
|
47
|
+
export declare const AUDIT_SEVERITY_ATTRIBUTE = "data-ig-audit";
|
|
48
|
+
/** The attribute carrying the persistent count, for a host that reads it back. */
|
|
49
|
+
export declare const AUDIT_COUNT_ATTRIBUTE = "data-ig-audit-count";
|
|
50
|
+
/**
|
|
51
|
+
* The attribute marking the filter toggle, so a host can bind to it without
|
|
52
|
+
* matching on a class name that is a styling hook rather than a contract.
|
|
53
|
+
*/
|
|
54
|
+
export declare const AUDIT_FILTER_ATTRIBUTE = "data-ig-audit-filter";
|
|
55
|
+
/** What one affected row is told about itself. */
|
|
56
|
+
export interface AuditRow {
|
|
57
|
+
readonly ref: IssueRef;
|
|
58
|
+
/**
|
|
59
|
+
* The severity of the heaviest finding on this row — `weight` in
|
|
60
|
+
* {@link AUDIT_CLASS_SPECS}, which exists for exactly this.
|
|
61
|
+
*/
|
|
62
|
+
readonly severity: AuditSeverity;
|
|
63
|
+
/** Every class present on the row, in {@link AUDIT_CLASSES} order. */
|
|
64
|
+
readonly kinds: readonly AuditClass[];
|
|
65
|
+
/** How many findings name this row. */
|
|
66
|
+
readonly count: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The two things the ambient surface draws, plus the index they are looked up
|
|
70
|
+
* through.
|
|
71
|
+
*
|
|
72
|
+
* AN IN-PROCESS VALUE, NOT A PAYLOAD — the same distinction
|
|
73
|
+
* `@issuegraph/viewer` draws between its normalized document and the plain
|
|
74
|
+
* shape a host sends over a wire. {@link AuditOverlay.rowFor} is a closure, so
|
|
75
|
+
* this does not serialize; what crosses a boundary is the DOCUMENT, and a
|
|
76
|
+
* receiver builds its own overlay from it.
|
|
77
|
+
*/
|
|
78
|
+
export interface AuditOverlay {
|
|
79
|
+
/**
|
|
80
|
+
* The header count: findings, not affected rows.
|
|
81
|
+
*
|
|
82
|
+
* FINDINGS, BECAUSE THAT IS WHAT THE OWNER WORKS THROUGH. One cycle across
|
|
83
|
+
* six issues is one judgment call, not six, and counting rows would report it
|
|
84
|
+
* as six pieces of work while the list has one entry.
|
|
85
|
+
*/
|
|
86
|
+
readonly count: number;
|
|
87
|
+
/** One entry per affected ref, sorted, so two renders agree. */
|
|
88
|
+
readonly rows: readonly AuditRow[];
|
|
89
|
+
/**
|
|
90
|
+
* The row for a ref, or `undefined` when it is clean.
|
|
91
|
+
*
|
|
92
|
+
* A FUNCTION RATHER THAN THE INDEX ITSELF, and the difference is not
|
|
93
|
+
* cosmetic: `ReadonlyMap` is a TypeScript restriction and nothing more, so
|
|
94
|
+
* handing out the live `Map` let a JavaScript consumer call `.clear()` on it
|
|
95
|
+
* — after which the lookups disagreed with `rows`, `findings` and `count`,
|
|
96
|
+
* which is the inconsistency this whole value exists to prevent. A closure
|
|
97
|
+
* over a private index cannot be reached at all, and still answers in
|
|
98
|
+
* constant time, which a scan per row would not.
|
|
99
|
+
*/
|
|
100
|
+
readonly rowFor: (ref: IssueRef) => AuditRow | undefined;
|
|
101
|
+
/** The findings themselves, carried through unchanged. */
|
|
102
|
+
readonly findings: readonly AuditFinding[];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Project findings onto the two things the ambient surface draws.
|
|
106
|
+
*
|
|
107
|
+
* Pure, and a function of the findings alone: it neither re-detects anything
|
|
108
|
+
* nor reads the document, so the count on screen and the list behind it can
|
|
109
|
+
* never disagree about what was found.
|
|
110
|
+
*/
|
|
111
|
+
export declare function auditOverlay(input: AuditInput): AuditOverlay;
|
|
112
|
+
/**
|
|
113
|
+
* What an affected row carries, or an empty record when the row is clean.
|
|
114
|
+
*
|
|
115
|
+
* An EMPTY RECORD rather than `undefined`, so a caller spreads the answer
|
|
116
|
+
* unconditionally and a clean row simply contributes nothing. A caller that
|
|
117
|
+
* branched would be one `if` away from stamping `undefined` into an attribute,
|
|
118
|
+
* which reads in the DOM as the string.
|
|
119
|
+
*/
|
|
120
|
+
export declare function auditRowAttributes(overlay: AuditOverlay, ref: IssueRef): Readonly<Record<string, string>>;
|
|
121
|
+
/** Whether the audit filter would keep this row. Clean rows are hidden by it. */
|
|
122
|
+
export declare function auditFilterKeeps(overlay: AuditOverlay, ref: IssueRef): boolean;
|
|
123
|
+
/**
|
|
124
|
+
* The heaviest row among several refs, or `undefined` when all of them are clean.
|
|
125
|
+
*
|
|
126
|
+
* FOR A ROW THAT SPEAKS FOR MORE THAN ONE ISSUE. A `together-with` unit is one
|
|
127
|
+
* rail row and several refs, so a caller drawing a bar for that row has to pick
|
|
128
|
+
* which member's severity it shows — and the answer is the heaviest, exactly as
|
|
129
|
+
* {@link AuditRow.severity} picks the heaviest CLASS within one ref.
|
|
130
|
+
*
|
|
131
|
+
* IT LIVES HERE BECAUSE THE WEIGHTS DO. The obvious alternative is for the
|
|
132
|
+
* caller to rank the rows itself, and the obvious shortcut — take the first
|
|
133
|
+
* matching entry in `rows` — is wrong in a way that looks right: `rows` is
|
|
134
|
+
* sorted by `ref`, lexicographically, so "first match" returns whichever member
|
|
135
|
+
* sorts earliest and a `stale-blocker` on `a` masks a `cycle` on `b`. Ranking
|
|
136
|
+
* belongs next to the table it ranks by, so no caller holds a second copy of it.
|
|
137
|
+
*/
|
|
138
|
+
export declare function heaviestRow(overlay: AuditOverlay, refs: readonly IssueRef[]): AuditRow | undefined;
|
|
139
|
+
export interface AuditHeaderOptions {
|
|
140
|
+
/** Whether the filter is currently narrowing the rail. Defaults to `false`. */
|
|
141
|
+
readonly filtered?: boolean | undefined;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The persistent header count, as markup.
|
|
145
|
+
*
|
|
146
|
+
* ONE CONTROL, ALWAYS PRESENT, ALWAYS THE SAME CLICK — including at zero, which
|
|
147
|
+
* is why it is neither hidden nor disabled when nothing is found. A control
|
|
148
|
+
* that appears when there is bad news is a control the eye has to re-find, and
|
|
149
|
+
* §17d's whole ask is that the audit never demands attention it has not earned.
|
|
150
|
+
*
|
|
151
|
+
* A `button` with `aria-pressed`, because a filter toggle is a toggle: it has
|
|
152
|
+
* two states and a screen reader has to be able to say which one is on.
|
|
153
|
+
*/
|
|
154
|
+
export declare function renderAuditHeader(overlay: AuditOverlay, options?: AuditHeaderOptions): string;
|
|
155
|
+
//# sourceMappingURL=surface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"surface.d.ts","sourceRoot":"","sources":["../../src/audit/surface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAGlD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEzF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,kBAAkB,CAAC;AAExD,kFAAkF;AAClF,eAAO,MAAM,qBAAqB,wBAAwB,CAAC;AAE3D;;;GAGG;AACH,eAAO,MAAM,sBAAsB,yBAAyB,CAAC;AAE7D,kDAAkD;AAClD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,sEAAsE;IACtE,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IACtC,uCAAuC;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,SAAS,QAAQ,EAAE,CAAC;IACnC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,QAAQ,GAAG,SAAS,CAAC;IACzD,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;CAC5C;AAMD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,YAAY,CAmE5D;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,QAAQ,GACZ,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAIlC;AAED,iFAAiF;AACjF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAE9E;AAUD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,YAAY,EACrB,IAAI,EAAE,SAAS,QAAQ,EAAE,GACxB,QAAQ,GAAG,SAAS,CAQtB;AAED,MAAM,WAAW,kBAAkB;IACjC,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACzC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,GAAE,kBAAuB,GAAG,MAAM,CAYjG"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ambient half of the audit: attention without nagging.
|
|
3
|
+
*
|
|
4
|
+
* §17d fixes both what this is and what it may never be. It is a **persistent
|
|
5
|
+
* header count** that never moves and never animates, a **left-bar** on the
|
|
6
|
+
* rail rows a finding touches, and an audit **filter**. It is explicitly not a
|
|
7
|
+
* modal, a toast, a red banner, an animated badge, anything that blocks the
|
|
8
|
+
* edit loop, or an auto-fix — every finding is a judgment call, so the surface
|
|
9
|
+
* offers navigation and never a remedy.
|
|
10
|
+
*
|
|
11
|
+
* §17a settles the last of those: audit is *"a filter, not a mode — a mode you
|
|
12
|
+
* must enter is a mode you forget, and encoding errors need to be visible while
|
|
13
|
+
* you work."* So the count is always drawn, including at zero, and the toggle
|
|
14
|
+
* narrows what is listed rather than switching the workspace into something
|
|
15
|
+
* else.
|
|
16
|
+
*
|
|
17
|
+
* ## Why the bar is CSS on an attribute rather than a drawn element
|
|
18
|
+
*
|
|
19
|
+
* The rows belong to `@issuegraph/viewer`, which stamps `KEY_ATTRIBUTE` on
|
|
20
|
+
* them. Layer 2 composes layer 1 through its public surface, and the viewer's
|
|
21
|
+
* markup primitive is deliberately not on it — so an overlay drawn from out
|
|
22
|
+
* here would have to re-implement HTML escaping, which is duplication with an
|
|
23
|
+
* injection shape rather than a mirror that merely drifts.
|
|
24
|
+
*
|
|
25
|
+
* An attribute plus a stylesheet needs neither. {@link auditRowAttributes}
|
|
26
|
+
* answers what an affected row carries, `./styles.ts` draws the bar from it,
|
|
27
|
+
* and the whole exchange is data. It also settles nothing about HOW
|
|
28
|
+
* overlays attach, which is a separate decision with its own change.
|
|
29
|
+
*
|
|
30
|
+
* ## Nothing here renders host text
|
|
31
|
+
*
|
|
32
|
+
* The only value that reaches the markup is a COUNT, and a count is a number.
|
|
33
|
+
* That is a deliberate boundary rather than a happy accident: a finding's
|
|
34
|
+
* `detail` is prose about issues a host supplied, so drawing it here would put
|
|
35
|
+
* an escaper in a package whose seam says it may not have one. Findings travel
|
|
36
|
+
* as data; whatever lists them owns their escaping.
|
|
37
|
+
*
|
|
38
|
+
* @see https://github.com/autnmy/issuegraph/blob/main/SPEC.md
|
|
39
|
+
*/
|
|
40
|
+
import { AUDIT_CLASSES, AUDIT_CLASS_SPECS, auditDocument } from "./findings.js";
|
|
41
|
+
/**
|
|
42
|
+
* The attribute an affected rail row carries. Its value is the severity that
|
|
43
|
+
* won the row, so a host can style the four differently without this package
|
|
44
|
+
* deciding that they should be.
|
|
45
|
+
*/
|
|
46
|
+
export const AUDIT_SEVERITY_ATTRIBUTE = 'data-ig-audit';
|
|
47
|
+
/** The attribute carrying the persistent count, for a host that reads it back. */
|
|
48
|
+
export const AUDIT_COUNT_ATTRIBUTE = 'data-ig-audit-count';
|
|
49
|
+
/**
|
|
50
|
+
* The attribute marking the filter toggle, so a host can bind to it without
|
|
51
|
+
* matching on a class name that is a styling hook rather than a contract.
|
|
52
|
+
*/
|
|
53
|
+
export const AUDIT_FILTER_ATTRIBUTE = 'data-ig-audit-filter';
|
|
54
|
+
const CLASS_ORDER = new Map(AUDIT_CLASSES.map((kind, index) => [kind, index]));
|
|
55
|
+
/**
|
|
56
|
+
* Project findings onto the two things the ambient surface draws.
|
|
57
|
+
*
|
|
58
|
+
* Pure, and a function of the findings alone: it neither re-detects anything
|
|
59
|
+
* nor reads the document, so the count on screen and the list behind it can
|
|
60
|
+
* never disagree about what was found.
|
|
61
|
+
*/
|
|
62
|
+
export function auditOverlay(input) {
|
|
63
|
+
// IT RUNS THE AUDIT RATHER THAN ACCEPTING ONE. Taking a finding list made
|
|
64
|
+
// this a public boundary for values TypeScript never checked, and six review
|
|
65
|
+
// rounds each found a different way for one to be wrong — a mutable array, a
|
|
66
|
+
// mutable `members`, a `severity` disagreeing with its `kind`, a prototype
|
|
67
|
+
// key, a ref named twice, a finding naming nobody. That surface is not
|
|
68
|
+
// enumerable, so it is gone instead of defended: every finding here was built
|
|
69
|
+
// by the detector, with its invariants established rather than checked.
|
|
70
|
+
//
|
|
71
|
+
// The cost is a host that persisted findings must re-audit to draw them. That
|
|
72
|
+
// is the right way round anyway — the audit is pure and cheap, and a
|
|
73
|
+
// persisted finding may not describe the document being drawn.
|
|
74
|
+
const findings = auditDocument(input);
|
|
75
|
+
// ROWS ARE RAIL ROWS, so only a ref the document carries can have one. A
|
|
76
|
+
// refusal may name an issue outside a paged document — the detector supports
|
|
77
|
+
// that on purpose, because filtering findings to the loaded set would drop
|
|
78
|
+
// them on exactly the issues paging has not reached — but a ROW for such a
|
|
79
|
+
// ref advertises a rail entry that does not exist, and a consumer iterating
|
|
80
|
+
// `rows` to filter or navigate would offer an unreachable issue. The finding
|
|
81
|
+
// and the header count keep it; the row index does not.
|
|
82
|
+
const carried = new Set(input.document.issues.map((issue) => issue.ref));
|
|
83
|
+
const byRef = new Map();
|
|
84
|
+
for (const found of findings) {
|
|
85
|
+
for (const ref of found.members) {
|
|
86
|
+
if (!carried.has(ref))
|
|
87
|
+
continue;
|
|
88
|
+
const existing = byRef.get(ref);
|
|
89
|
+
if (existing === undefined) {
|
|
90
|
+
byRef.set(ref, { kinds: new Set([found.kind]), count: 1 });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
existing.kinds.add(found.kind);
|
|
94
|
+
existing.count += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const rows = [];
|
|
98
|
+
for (const [ref, { kinds, count }] of byRef) {
|
|
99
|
+
const ordered = [...kinds].sort((a, b) => (CLASS_ORDER.get(a) ?? 0) - (CLASS_ORDER.get(b) ?? 0));
|
|
100
|
+
// The heaviest class speaks for the row. Written as a loop rather than a
|
|
101
|
+
// seedless `reduce`, which throws on an empty list: a ref only enters the
|
|
102
|
+
// map by carrying a finding, so the list cannot be empty today — and an
|
|
103
|
+
// expression whose totality rests on an invariant stated three lines away
|
|
104
|
+
// is one refactor from a TypeError.
|
|
105
|
+
let heaviest;
|
|
106
|
+
for (const kind of ordered) {
|
|
107
|
+
if (heaviest === undefined || AUDIT_CLASS_SPECS[kind].weight > AUDIT_CLASS_SPECS[heaviest].weight) {
|
|
108
|
+
heaviest = kind;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (heaviest === undefined)
|
|
112
|
+
continue;
|
|
113
|
+
rows.push({
|
|
114
|
+
ref,
|
|
115
|
+
severity: AUDIT_CLASS_SPECS[heaviest].severity,
|
|
116
|
+
kinds: Object.freeze(ordered),
|
|
117
|
+
count,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
rows.sort((a, b) => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0));
|
|
121
|
+
const index = new Map(rows.map((row) => [row.ref, row]));
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
count: findings.length,
|
|
124
|
+
rows: Object.freeze(rows.map((row) => Object.freeze(row))),
|
|
125
|
+
// The index is captured, never exposed — see `rowFor`.
|
|
126
|
+
rowFor: (ref) => index.get(ref),
|
|
127
|
+
findings,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* What an affected row carries, or an empty record when the row is clean.
|
|
132
|
+
*
|
|
133
|
+
* An EMPTY RECORD rather than `undefined`, so a caller spreads the answer
|
|
134
|
+
* unconditionally and a clean row simply contributes nothing. A caller that
|
|
135
|
+
* branched would be one `if` away from stamping `undefined` into an attribute,
|
|
136
|
+
* which reads in the DOM as the string.
|
|
137
|
+
*/
|
|
138
|
+
export function auditRowAttributes(overlay, ref) {
|
|
139
|
+
const row = overlay.rowFor(ref);
|
|
140
|
+
if (row === undefined)
|
|
141
|
+
return Object.freeze({});
|
|
142
|
+
return Object.freeze({ [AUDIT_SEVERITY_ATTRIBUTE]: row.severity });
|
|
143
|
+
}
|
|
144
|
+
/** Whether the audit filter would keep this row. Clean rows are hidden by it. */
|
|
145
|
+
export function auditFilterKeeps(overlay, ref) {
|
|
146
|
+
return overlay.rowFor(ref) !== undefined;
|
|
147
|
+
}
|
|
148
|
+
/** How heavy a row's worst finding is, from the class table and nothing else. */
|
|
149
|
+
function rowWeight(row) {
|
|
150
|
+
return row.kinds.reduce((heaviest, kind) => Math.max(heaviest, AUDIT_CLASS_SPECS[kind].weight), Number.NEGATIVE_INFINITY);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The heaviest row among several refs, or `undefined` when all of them are clean.
|
|
154
|
+
*
|
|
155
|
+
* FOR A ROW THAT SPEAKS FOR MORE THAN ONE ISSUE. A `together-with` unit is one
|
|
156
|
+
* rail row and several refs, so a caller drawing a bar for that row has to pick
|
|
157
|
+
* which member's severity it shows — and the answer is the heaviest, exactly as
|
|
158
|
+
* {@link AuditRow.severity} picks the heaviest CLASS within one ref.
|
|
159
|
+
*
|
|
160
|
+
* IT LIVES HERE BECAUSE THE WEIGHTS DO. The obvious alternative is for the
|
|
161
|
+
* caller to rank the rows itself, and the obvious shortcut — take the first
|
|
162
|
+
* matching entry in `rows` — is wrong in a way that looks right: `rows` is
|
|
163
|
+
* sorted by `ref`, lexicographically, so "first match" returns whichever member
|
|
164
|
+
* sorts earliest and a `stale-blocker` on `a` masks a `cycle` on `b`. Ranking
|
|
165
|
+
* belongs next to the table it ranks by, so no caller holds a second copy of it.
|
|
166
|
+
*/
|
|
167
|
+
export function heaviestRow(overlay, refs) {
|
|
168
|
+
let heaviest;
|
|
169
|
+
for (const ref of refs) {
|
|
170
|
+
const row = overlay.rowFor(ref);
|
|
171
|
+
if (row === undefined)
|
|
172
|
+
continue;
|
|
173
|
+
if (heaviest === undefined || rowWeight(row) > rowWeight(heaviest))
|
|
174
|
+
heaviest = row;
|
|
175
|
+
}
|
|
176
|
+
return heaviest;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The persistent header count, as markup.
|
|
180
|
+
*
|
|
181
|
+
* ONE CONTROL, ALWAYS PRESENT, ALWAYS THE SAME CLICK — including at zero, which
|
|
182
|
+
* is why it is neither hidden nor disabled when nothing is found. A control
|
|
183
|
+
* that appears when there is bad news is a control the eye has to re-find, and
|
|
184
|
+
* §17d's whole ask is that the audit never demands attention it has not earned.
|
|
185
|
+
*
|
|
186
|
+
* A `button` with `aria-pressed`, because a filter toggle is a toggle: it has
|
|
187
|
+
* two states and a screen reader has to be able to say which one is on.
|
|
188
|
+
*/
|
|
189
|
+
export function renderAuditHeader(overlay, options = {}) {
|
|
190
|
+
const count = String(overlay.count);
|
|
191
|
+
const pressed = options.filtered === true;
|
|
192
|
+
return [
|
|
193
|
+
`<div class="ig-audit" ${AUDIT_COUNT_ATTRIBUTE}="${count}">`,
|
|
194
|
+
`<button type="button" class="ig-audit-toggle" aria-pressed="${String(pressed)}"`,
|
|
195
|
+
` ${AUDIT_FILTER_ATTRIBUTE}>`,
|
|
196
|
+
`<span class="ig-audit-count">${count}</span>`,
|
|
197
|
+
`<span class="ig-audit-label">audit</span>`,
|
|
198
|
+
`</button>`,
|
|
199
|
+
`</div>`,
|
|
200
|
+
].join('');
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=surface.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"surface.js","sourceRoot":"","sources":["../../src/audit/surface.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creating an edge, as the one thing all three paths do.
|
|
3
|
+
*
|
|
4
|
+
* §17b asks for three **equivalent** create paths — canvas, inspector and
|
|
5
|
+
* keyboard — and is explicit that they are equivalent rather than a primary
|
|
6
|
+
* path with two shortcuts. The inspector is "the only path when the target
|
|
7
|
+
* isn't on canvas", and at any real size most targets are off canvas (§17f:
|
|
8
|
+
* the canvas is a *local* instrument), so a design where drag is the real path
|
|
9
|
+
* and the other two are conveniences is a design that stops working at the
|
|
10
|
+
* size it was built for.
|
|
11
|
+
*
|
|
12
|
+
* ## Equivalence is a property of the SHAPE, not a promise a test keeps
|
|
13
|
+
*
|
|
14
|
+
* The three paths gather the same three facts in different orders:
|
|
15
|
+
*
|
|
16
|
+
* canvas source → target → kind (drag, then the picker at the drop)
|
|
17
|
+
* inspector source → kind → target ("+ add" → type → issue search)
|
|
18
|
+
* keyboard source → kind → target (`R` → `1`–`5` → search → `⏎`)
|
|
19
|
+
*
|
|
20
|
+
* So a draft modelled as a SEQUENCE would need three sequences, and "equivalent"
|
|
21
|
+
* would mean three implementations that agree today. This models it as a SET of
|
|
22
|
+
* gathered facts instead: three slots, each filled by its own command, in any
|
|
23
|
+
* order, and the proposal is emitted on whichever transition completes the set.
|
|
24
|
+
* Order-independence stops being a thing to verify and becomes a thing that
|
|
25
|
+
* cannot be otherwise — there is one emitter, and none of the three paths is
|
|
26
|
+
* spelled out here at all.
|
|
27
|
+
*
|
|
28
|
+
* That is also why the draft carries no path identity. A `source` filled by a
|
|
29
|
+
* drag and one filled by `R` are the same fact, and a field recording which
|
|
30
|
+
* arrived would be the difference between the paths growing back — every reader
|
|
31
|
+
* of it a place where one path could start behaving unlike the others. What
|
|
32
|
+
* genuinely differs between them is where the picker is DRAWN, which is
|
|
33
|
+
* geometry and lives in {@link ./placement.ts}, not state.
|
|
34
|
+
*
|
|
35
|
+
* ## One user act, one `Proposal`
|
|
36
|
+
*
|
|
37
|
+
* The same rule `picker/view.ts` keeps for retype and flip. `@issuegraph/store`
|
|
38
|
+
* closed the operation set at four so that one act is one round trip and one
|
|
39
|
+
* undo entry, and a create that emitted twice — or that emitted a delete plus a
|
|
40
|
+
* create — would undo the property the store went out of its way to model.
|
|
41
|
+
*
|
|
42
|
+
* ## Validity is the store's, and stays there
|
|
43
|
+
*
|
|
44
|
+
* A self-edge, a duplicate edge, an unknown issue: `structuralRefusal` owns all
|
|
45
|
+
* of them and answers with an `InvalidCode`. This module proposes; it does not
|
|
46
|
+
* adjudicate. `picker/view.ts` made the same call for `unchanged-kind` and
|
|
47
|
+
* recorded why — a second validity rule out here is a second place for the
|
|
48
|
+
* answer to drift, and the surface a refusal is drawn on already exists.
|
|
49
|
+
*/
|
|
50
|
+
import type { EdgeKind, IssueRef, Proposal } from '@issuegraph/store';
|
|
51
|
+
/**
|
|
52
|
+
* The facts gathered so far. Three slots, filled in any order.
|
|
53
|
+
*
|
|
54
|
+
* `null` means "not yet gathered", which is distinct from any legal value of
|
|
55
|
+
* either field — an issue reference is a non-empty string and a kind is one of
|
|
56
|
+
* five — so no sentinel collides with real data.
|
|
57
|
+
*/
|
|
58
|
+
export interface CreateDraft {
|
|
59
|
+
/** The issue the relationship is being created FROM. */
|
|
60
|
+
readonly source: IssueRef | null;
|
|
61
|
+
/** The issue it is being created TO. */
|
|
62
|
+
readonly target: IssueRef | null;
|
|
63
|
+
/** Which relationship. `null` until the type picker has answered. */
|
|
64
|
+
readonly kind: EdgeKind | null;
|
|
65
|
+
}
|
|
66
|
+
/** Nothing gathered. The state before a create starts and after one emits. */
|
|
67
|
+
export declare const IDLE_CREATE_DRAFT: CreateDraft;
|
|
68
|
+
/**
|
|
69
|
+
* One fact arriving, from whichever path gathered it.
|
|
70
|
+
*
|
|
71
|
+
* `begin` is deliberately not "set source": it starts a NEW relationship, so it
|
|
72
|
+
* clears whatever a previous, abandoned draft had gathered. Restarting from a
|
|
73
|
+
* different issue while a half-built draft is open is ordinary — the reader
|
|
74
|
+
* changes their mind about the subject — and carrying the old target into it
|
|
75
|
+
* would create an edge nobody asked for out of two halves of two intentions.
|
|
76
|
+
*/
|
|
77
|
+
export type CreateCommand = {
|
|
78
|
+
readonly kind: 'begin';
|
|
79
|
+
readonly source: IssueRef;
|
|
80
|
+
} | {
|
|
81
|
+
readonly kind: 'target';
|
|
82
|
+
readonly ref: IssueRef;
|
|
83
|
+
} | {
|
|
84
|
+
readonly kind: 'type';
|
|
85
|
+
readonly edgeKind: EdgeKind;
|
|
86
|
+
} | {
|
|
87
|
+
readonly kind: 'cancel';
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The next draft, and the proposal this transition emitted.
|
|
91
|
+
*
|
|
92
|
+
* `proposal` is `null` on every transition but the completing one. Same shape
|
|
93
|
+
* as the viewer's `NavigationResult` — next state plus the one thing the shell
|
|
94
|
+
* should do — because it is the same job, and a host already reducing one can
|
|
95
|
+
* reduce the other without learning a second protocol.
|
|
96
|
+
*/
|
|
97
|
+
export interface CreateResult {
|
|
98
|
+
readonly draft: CreateDraft;
|
|
99
|
+
readonly proposal: Proposal | null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Apply one command. Total, pure, and never mutates what it is given.
|
|
103
|
+
*
|
|
104
|
+
* AN EXHAUSTIVE SWITCH OVER A DISCRIMINATED UNION — the shape `scaleReducer`
|
|
105
|
+
* already uses here and the one branching form `AGENTS.md`'s boundary rule
|
|
106
|
+
* leaves open. Adding a command without a case fails the build rather than
|
|
107
|
+
* silently returning the draft unchanged.
|
|
108
|
+
*
|
|
109
|
+
* The direction a create lands with is the gather order: `from` is the source,
|
|
110
|
+
* `to` is the target. Nothing here infers it. §17b's rule is that direction is
|
|
111
|
+
* STATED rather than guessed, and it is — by the picker, after the edit lands,
|
|
112
|
+
* with a flip beside it, which is one act from correct. That is the same
|
|
113
|
+
* reasoning `picker/view.ts` records for retyping across the directed/symmetric
|
|
114
|
+
* split, and it is why this module does not ask about directedness at all: a
|
|
115
|
+
* symmetric kind keeps the pair too, precisely so an editor knows which issue
|
|
116
|
+
* carries the field.
|
|
117
|
+
*/
|
|
118
|
+
export declare function createReducer(draft: CreateDraft, command: CreateCommand): CreateResult;
|
|
119
|
+
//# sourceMappingURL=draft.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draft.d.ts","sourceRoot":"","sources":["../../src/create/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAEtE;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,wDAAwD;IACxD,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IACjC,wCAAwC;IACxC,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IACjC,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;CAChC;AAED,8EAA8E;AAC9E,eAAO,MAAM,iBAAiB,EAAE,WAI9B,CAAC;AAEH;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAA;CAAE,GACnD;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEhC;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;CACpC;AAqBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,GAAG,YAAY,CAmCtF"}
|