@issuegraph/viewer 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.
Files changed (67) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +156 -0
  3. package/dist/clusters.d.ts +34 -0
  4. package/dist/clusters.d.ts.map +1 -0
  5. package/dist/clusters.js +195 -0
  6. package/dist/clusters.js.map +1 -0
  7. package/dist/document.d.ts +190 -0
  8. package/dist/document.d.ts.map +1 -0
  9. package/dist/document.js +308 -0
  10. package/dist/document.js.map +1 -0
  11. package/dist/element.d.ts +95 -0
  12. package/dist/element.d.ts.map +1 -0
  13. package/dist/element.js +174 -0
  14. package/dist/element.js.map +1 -0
  15. package/dist/index.d.ts +40 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +33 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/layout.d.ts +122 -0
  20. package/dist/layout.d.ts.map +1 -0
  21. package/dist/layout.js +297 -0
  22. package/dist/layout.js.map +1 -0
  23. package/dist/mount.d.ts +64 -0
  24. package/dist/mount.d.ts.map +1 -0
  25. package/dist/mount.js +402 -0
  26. package/dist/mount.js.map +1 -0
  27. package/dist/navigation.d.ts +56 -0
  28. package/dist/navigation.d.ts.map +1 -0
  29. package/dist/navigation.js +121 -0
  30. package/dist/navigation.js.map +1 -0
  31. package/dist/parts.d.ts +89 -0
  32. package/dist/parts.d.ts.map +1 -0
  33. package/dist/parts.js +214 -0
  34. package/dist/parts.js.map +1 -0
  35. package/dist/projections/graph.d.ts +30 -0
  36. package/dist/projections/graph.d.ts.map +1 -0
  37. package/dist/projections/graph.js +644 -0
  38. package/dist/projections/graph.js.map +1 -0
  39. package/dist/projections/linear.d.ts +35 -0
  40. package/dist/projections/linear.d.ts.map +1 -0
  41. package/dist/projections/linear.js +157 -0
  42. package/dist/projections/linear.js.map +1 -0
  43. package/dist/projections/tree.d.ts +17 -0
  44. package/dist/projections/tree.d.ts.map +1 -0
  45. package/dist/projections/tree.js +209 -0
  46. package/dist/projections/tree.js.map +1 -0
  47. package/dist/render.d.ts +35 -0
  48. package/dist/render.d.ts.map +1 -0
  49. package/dist/render.js +45 -0
  50. package/dist/render.js.map +1 -0
  51. package/dist/scene.d.ts +94 -0
  52. package/dist/scene.d.ts.map +1 -0
  53. package/dist/scene.js +48 -0
  54. package/dist/scene.js.map +1 -0
  55. package/dist/styles.d.ts +17 -0
  56. package/dist/styles.d.ts.map +1 -0
  57. package/dist/styles.js +389 -0
  58. package/dist/styles.js.map +1 -0
  59. package/dist/theme.d.ts +71 -0
  60. package/dist/theme.d.ts.map +1 -0
  61. package/dist/theme.js +169 -0
  62. package/dist/theme.js.map +1 -0
  63. package/dist/vocabulary.d.ts +107 -0
  64. package/dist/vocabulary.d.ts.map +1 -0
  65. package/dist/vocabulary.js +98 -0
  66. package/dist/vocabulary.js.map +1 -0
  67. package/package.json +56 -0
package/dist/layout.js ADDED
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Graph geometry, computed — never eyeballed.
3
+ *
4
+ * Three columns, three jobs: the LEFT GUTTER holds open issues outside the
5
+ * order that explain a hold, the CENTRE SPINE is the work order itself, and the
6
+ * RIGHT GUTTER holds issues the order never works. Sequence is vertical
7
+ * position on the spine; dependency is everything off it.
8
+ *
9
+ * Two rules carry most of the weight, and both come from the design's
10
+ * implementation note rather than from taste:
11
+ *
12
+ * - **Every edge endpoint is derived from a node's computed bounds.** An edge
13
+ * terminates ON a bound, never at a remembered offset, so a node that
14
+ * changes size moves its edges with it.
15
+ * - **Arcs stay out of the occupied x-ranges** and out of the station band.
16
+ * The colour-blind-safety claim rests on four redundant channels, and the
17
+ * terminal marker is the one that separates `decomposed-from` from
18
+ * `duplicate-of` — a marker hidden under an opaque card silently drops the
19
+ * encoding to three.
20
+ *
21
+ * Deterministic by construction: no measurement, no randomness, no clock. The
22
+ * same document always produces the same coordinates, which is what lets a
23
+ * panel whose job is to be authoritative be trusted between refreshes.
24
+ */
25
+ /**
26
+ * Custom properties carrying LAYOUT OUTPUT rather than theme input.
27
+ *
28
+ * A theme token is a value a host chooses; these are values this module
29
+ * COMPUTES and writes onto individual elements, so the rail can sit on the node
30
+ * it names without either side hard-coding a coordinate. They are declared here
31
+ * so `styles.test.ts` can tell the two kinds apart — a `var()` naming neither a
32
+ * theme token nor one of these is still a defect, and still fails.
33
+ */
34
+ export const LAYOUT_PROPERTIES = Object.freeze([
35
+ '--ig-stage-w',
36
+ '--ig-stage-h',
37
+ '--ig-row-x',
38
+ '--ig-row-y',
39
+ '--ig-row-w',
40
+ '--ig-row-h',
41
+ ]);
42
+ function metric(theme, token) {
43
+ return theme.metrics[token];
44
+ }
45
+ /**
46
+ * A node's width follows its own contents rather than the column, so an edge
47
+ * terminating on its bound lands where the reader sees the box end. Clamped to
48
+ * the column so the channels stay free.
49
+ */
50
+ /**
51
+ * The label as it fits the box the layout gave it.
52
+ *
53
+ * `boxWidth` CLAMPS to the column, so a title longer than the column gets a box
54
+ * narrower than its text — and an SVG `<text>` neither wraps nor clips, so the
55
+ * overflow ran straight across the routing channel and the neighbouring nodes,
56
+ * hiding the edges and labels the graph exists to show. Long issue titles are
57
+ * ordinary, so this was the common case rather than an edge one.
58
+ *
59
+ * IT MEASURES WITH THE SAME METRIC `boxWidth` SIZES WITH, which is the whole
60
+ * reason this belongs beside it rather than in the projection. Both read
61
+ * `--ig-char-width`, so the renderer's idea of what fits and the layout's cannot
62
+ * disagree — a separate estimate in the drawing code would be a second opinion
63
+ * about the same question, and the two would drift.
64
+ *
65
+ * THE ELLIPSIS IS THIS PACKAGE'S OWN TREATMENT, not a new convention: the rail's
66
+ * HTML rows already resolve an over-long title with `text-overflow: ellipsis`,
67
+ * so a canvas label that simply stopped mid-word would make the two surfaces
68
+ * disagree about what a truncated title looks like.
69
+ *
70
+ * NOTHING IS LOST TO A READER — the full title stays in the node's accessible
71
+ * name and its `<title>`, so this shortens what is DRAWN and never what is
72
+ * available.
73
+ */
74
+ export function fitLabel(theme, label, width) {
75
+ const room = width - metric(theme, '--ig-space') * 2;
76
+ const fits = Math.floor(room / metric(theme, '--ig-char-width'));
77
+ if (fits >= label.length)
78
+ return label;
79
+ // Below two characters there is no room for a glyph AND the ellipsis, and an
80
+ // ellipsis alone names nothing — so the box has become too small to label at
81
+ // all, and drawing nothing is honester than drawing a lone dot.
82
+ if (fits < 2)
83
+ return '';
84
+ return `${label.slice(0, fits - 1)}\u2026`;
85
+ }
86
+ function boxWidth(theme, label, columnWidth) {
87
+ const padding = metric(theme, '--ig-space') * 2;
88
+ const measured = label.length * metric(theme, '--ig-char-width') + padding;
89
+ return Math.min(columnWidth, Math.max(columnWidth / 2, measured));
90
+ }
91
+ /**
92
+ * Assign every key that needs a box to a column.
93
+ *
94
+ * Totality is the requirement, not a nicety: every kept edge's endpoints must
95
+ * have bounds or its geometry is undefined, and `normalizeDocument` has already
96
+ * dropped the edges whose ends this document does not carry. Anything touched
97
+ * by an edge and not on the spine therefore lands in a gutter — the right one
98
+ * when the order deliberately never works it, the left one when it is an open
99
+ * issue standing in the way.
100
+ */
101
+ function assignColumns(document) {
102
+ const spine = [];
103
+ const slotMembers = new Map();
104
+ const placed = new Set();
105
+ for (const slot of document.order.slots) {
106
+ slotMembers.set(slot.lead, slot.members);
107
+ for (const member of slot.members) {
108
+ if (placed.has(member))
109
+ continue;
110
+ placed.add(member);
111
+ spine.push(member);
112
+ }
113
+ }
114
+ const neverWorked = new Set(document.order.excluded.map((exclusion) => exclusion.key));
115
+ for (const edge of document.edges) {
116
+ if (edge.field !== 'decomposed-from')
117
+ continue;
118
+ const origin = document.byKey.get(edge.to);
119
+ if (origin !== undefined && !origin.open && !placed.has(edge.to))
120
+ neverWorked.add(edge.to);
121
+ }
122
+ const left = [];
123
+ const right = [];
124
+ const seen = new Set(placed);
125
+ for (const edge of document.edges) {
126
+ for (const end of [edge.from, edge.to]) {
127
+ if (seen.has(end))
128
+ continue;
129
+ seen.add(end);
130
+ (neverWorked.has(end) ? right : left).push(end);
131
+ }
132
+ }
133
+ for (const key of neverWorked) {
134
+ if (seen.has(key))
135
+ continue;
136
+ seen.add(key);
137
+ right.push(key);
138
+ }
139
+ return { spine, left, right, slotMembers };
140
+ }
141
+ /** Lay a document out. Pure: coordinates depend only on the document and the theme. */
142
+ export function layoutGraph(document, theme) {
143
+ const rowHeight = metric(theme, '--ig-row-height');
144
+ const gap = metric(theme, '--ig-space');
145
+ const gutterWidth = metric(theme, '--ig-gutter-width');
146
+ const spineWidth = metric(theme, '--ig-spine-width');
147
+ const channelWidth = metric(theme, '--ig-gutter-width') / 2;
148
+ // THE CANVAS RESERVES WHAT THE ENCLOSURE NEEDS, AND WHAT THE FOCUS RING DOES.
149
+ // A `together-with` enclosure pads clear of its members' bounds, so a unit on
150
+ // the first or last row drew at a negative coordinate or past the bottom edge
151
+ // — outside the viewBox, and outside the stage, which hides its overflow.
152
+ // The focus ring reaches FURTHER than the enclosure and was clipped by the
153
+ // same edge: `:focus-visible` sits `--ig-space-tight` clear of the element and
154
+ // is `--ig-focus-ring` thick, so it extends their SUM outward, while a margin
155
+ // of `--ig-space-tight` alone left the top segment outside the stage — and on
156
+ // a one-row graph the bottom segment with it. A partial ring is exactly the
157
+ // indicator a keyboard reader depends on, so the margin covers the larger of
158
+ // the two demands rather than the one that happens to be named here.
159
+ // SUMMED FROM THE TOKENS, never written as 8: geometry is theme data in this
160
+ // package, so a retheme that changes either token moves the reservation with
161
+ // it instead of silently reintroducing the clip.
162
+ // The ENCLOSURE's own inset stays `--ig-space-tight` — this is the canvas
163
+ // margin, which must merely be big enough for both, not the same quantity.
164
+ const pad = metric(theme, '--ig-space-tight') + metric(theme, '--ig-focus-ring');
165
+ const { spine, left, right, slotMembers } = assignColumns(document);
166
+ const leftX = pad;
167
+ const spineX = pad + gutterWidth + channelWidth;
168
+ const rightX = spineX + spineWidth + channelWidth;
169
+ const width = rightX + gutterWidth + pad;
170
+ // The channels sit between the columns, so no arc routed through one can
171
+ // cross an occupied x-range.
172
+ const leftChannel = pad + gutterWidth + channelWidth / 2;
173
+ const rightChannel = spineX + spineWidth + channelWidth / 2;
174
+ const rankOf = new Map();
175
+ const heldOf = new Map();
176
+ for (const slot of document.order.slots) {
177
+ for (const member of slot.members) {
178
+ rankOf.set(member, slot.rank);
179
+ heldOf.set(member, !slot.ready);
180
+ }
181
+ }
182
+ const nodes = new Map();
183
+ const place = (keys, column, x, columnWidth) => {
184
+ keys.forEach((key, index) => {
185
+ const label = document.byKey.get(key)?.title ?? key;
186
+ nodes.set(key, {
187
+ key,
188
+ column,
189
+ x,
190
+ y: pad + index * (rowHeight + gap),
191
+ width: boxWidth(theme, label, columnWidth),
192
+ height: rowHeight,
193
+ rank: rankOf.get(key) ?? null,
194
+ held: heldOf.get(key) ?? false,
195
+ });
196
+ });
197
+ };
198
+ place(spine, 'spine', spineX, spineWidth);
199
+ place(left, 'left', leftX, gutterWidth);
200
+ place(right, 'right', rightX, gutterWidth);
201
+ const rows = Math.max(spine.length, left.length, right.length);
202
+ const height = rows === 0 ? 0 : rows * (rowHeight + gap) - gap + pad * 2;
203
+ return {
204
+ width,
205
+ height,
206
+ nodes,
207
+ spineOrder: spine,
208
+ slotMembers,
209
+ leftChannel,
210
+ rightChannel,
211
+ };
212
+ }
213
+ /**
214
+ * Where an edge leaves and enters, in the fraction of a box's height it uses.
215
+ *
216
+ * Departures leave low and arrivals enter high, so a path never runs along the
217
+ * centre line where the readiness station sits. That is the design's
218
+ * station-collision rule expressed as two numbers instead of a special case.
219
+ */
220
+ const DEPART_FRACTION = 0.75;
221
+ const ARRIVE_FRACTION = 0.25;
222
+ /**
223
+ * Which bound of `box` the edge should leave from or arrive at.
224
+ *
225
+ * PAIRWISE, NOT PER-COLUMN, and the difference is the whole correctness of the
226
+ * drawing. Deciding from a box's own column alone made every spine endpoint the
227
+ * LEFT bound — so an arc to the right gutter left the spine on its far side and
228
+ * had to cross the node to reach its own terminal, occluding exactly the marker
229
+ * the colour-blind-safety claim depends on.
230
+ *
231
+ * ON A TIE, FACE THE CHANNEL. Two nodes in the same column have the same x, so
232
+ * "face the other" says nothing — and defaulting LEFT was right for the spine
233
+ * only by coincidence: the left channel happens to sit left of it. For two
234
+ * left-gutter nodes the same channel is on their RIGHT, so the default sent
235
+ * both endpoints out of the canvas and dragged the path back across the boxes.
236
+ * Facing the channel is the rule the spine case was an instance of, so it keeps
237
+ * "arcs bow left of the spine" while fixing the gutter.
238
+ */
239
+ function facingSide(box, other, channelX) {
240
+ const centre = box.x + box.width / 2;
241
+ const otherCentre = other.x + other.width / 2;
242
+ if (otherCentre !== centre)
243
+ return otherCentre > centre ? 'right' : 'left';
244
+ return channelX > centre ? 'right' : 'left';
245
+ }
246
+ function anchor(box, fraction, side) {
247
+ return {
248
+ x: side === 'left' ? box.x : box.x + box.width,
249
+ y: box.y + box.height * fraction,
250
+ };
251
+ }
252
+ /**
253
+ * The geometry for one edge, derived entirely from the two boxes' bounds.
254
+ *
255
+ * Returns `null` when either end has no box — which `normalizeDocument` has
256
+ * already made impossible for a kept edge, so a `null` here means a caller
257
+ * built a layout from a different document than the edge came from. Refusing is
258
+ * better than drawing at coordinates nobody computed.
259
+ */
260
+ export function edgeGeometry(layout, edge) {
261
+ const from = layout.nodes.get(edge.from);
262
+ const to = layout.nodes.get(edge.to);
263
+ if (from === undefined || to === undefined)
264
+ return null;
265
+ // The channel is chosen FIRST, because on a same-column tie it is what
266
+ // decides which bound each endpoint uses. A pair of spine nodes bows LEFT, as
267
+ // the design fixes; anything touching the right gutter uses the right channel
268
+ // so the two families never tangle.
269
+ const usesRight = from.column === 'right' || to.column === 'right';
270
+ const controlX = usesRight ? layout.rightChannel : layout.leftChannel;
271
+ const start = anchor(from, DEPART_FRACTION, facingSide(from, to, controlX));
272
+ const end = anchor(to, ARRIVE_FRACTION, facingSide(to, from, controlX));
273
+ const controlY = (start.y + end.y) / 2;
274
+ const round = (value) => (Math.round(value * 100) / 100).toFixed(2);
275
+ const d = `M ${round(start.x)} ${round(start.y)} Q ${round(controlX)} ${round(controlY)} ${round(end.x)} ${round(end.y)}`;
276
+ return {
277
+ d,
278
+ start,
279
+ end,
280
+ endAngle: Math.atan2(end.y - controlY, end.x - controlX),
281
+ };
282
+ }
283
+ /** The box enclosing a together unit's members, padded clear of their bounds. */
284
+ export function enclosureBounds(layout, members, theme) {
285
+ const boxes = members
286
+ .map((member) => layout.nodes.get(member))
287
+ .filter((box) => box !== undefined);
288
+ if (boxes.length < 2)
289
+ return null;
290
+ const pad = metric(theme, '--ig-space-tight');
291
+ const x = Math.min(...boxes.map((box) => box.x)) - pad;
292
+ const y = Math.min(...boxes.map((box) => box.y)) - pad;
293
+ const right = Math.max(...boxes.map((box) => box.x + box.width)) + pad;
294
+ const bottom = Math.max(...boxes.map((box) => box.y + box.height)) + pad;
295
+ return { x, y, width: right - x, height: bottom - y };
296
+ }
297
+ //# sourceMappingURL=layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The DOM shell — the only module in this package that touches nodes.
3
+ *
4
+ * It is deliberately thin. Everything that could be wrong about WHAT is drawn
5
+ * lives in the projections, everything that could be wrong about WHERE
6
+ * navigation goes lives in `navigate`, and what is left here is three
7
+ * listeners, an index from key to element, and a teardown. A shell that grew
8
+ * decisions would be a second implementation of the parts above it, testable
9
+ * only through a DOM.
10
+ *
11
+ * IT REACHES NO GLOBAL. The document comes from the container the caller
12
+ * passed, which is what keeps this module importable on a runtime that has no
13
+ * DOM — the property the workspace smoke test measures against the published
14
+ * entry.
15
+ */
16
+ import { type ViewerDocument } from './document.ts';
17
+ import { type SpecDocument, type SpecElement } from './element.ts';
18
+ import { type NavigationState } from './navigation.ts';
19
+ import { type RenderOptions } from './render.ts';
20
+ import { type Projection } from './scene.ts';
21
+ /** The slice of an element `mount` needs beyond building. */
22
+ export interface MountElement extends SpecElement {
23
+ readonly ownerDocument: SpecDocument;
24
+ /** Upper-case, as the DOM reports it. Absent on an implementation that has none. */
25
+ readonly tagName?: string;
26
+ addEventListener(type: string, handler: (event: MountEvent) => void): void;
27
+ removeEventListener(type: string, handler: (event: MountEvent) => void): void;
28
+ removeChild(child: SpecElement): void;
29
+ getAttribute(name: string): string | null;
30
+ readonly parentElement: MountElement | null;
31
+ focus?(): void;
32
+ }
33
+ /** The slice of an event `mount` reads. */
34
+ export interface MountEvent {
35
+ readonly target?: MountElement | null;
36
+ /** Present on a keyboard event. */
37
+ readonly key?: string;
38
+ preventDefault?(): void;
39
+ }
40
+ export interface MountOptions extends RenderOptions {
41
+ /** Called when a key is selected — by click, `Enter`, or `select()`. */
42
+ readonly onSelect?: ((key: string | null) => void) | undefined;
43
+ /** Called with the hovered key, and with `null` when nothing is hovered. */
44
+ readonly onHover?: ((key: string | null) => void) | undefined;
45
+ }
46
+ export interface ViewerHandle {
47
+ /** Redraw. A still-present selection survives; focus follows it. */
48
+ update(input: ViewerDocument, options?: MountOptions): void;
49
+ /** Switch projection. The subject is kept; only the representation changes. */
50
+ setProjection(projection: Projection): void;
51
+ /** Select a key programmatically. Fires `onSelect` exactly as a click does. */
52
+ select(key: string | null): void;
53
+ readonly state: NavigationState;
54
+ /** Remove every listener this handle added and empty the container. */
55
+ destroy(): void;
56
+ }
57
+ /**
58
+ * Mount a viewer into a container.
59
+ *
60
+ * Returns a handle rather than nothing, because a host that cannot tear this
61
+ * down leaks a listener on every re-render — the shell's one genuine hazard.
62
+ */
63
+ export declare function mountViewer(container: MountElement, input: ViewerDocument, options?: MountOptions): ViewerHandle;
64
+ //# sourceMappingURL=mount.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount.d.ts","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAA2B,KAAK,cAAc,EAAqB,MAAM,eAAe,CAAC;AAChG,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAe,MAAM,cAAc,CAAC;AAChF,OAAO,EACL,KAAK,eAAe,EAIrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,KAAK,aAAa,EAAY,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAkC,KAAK,UAAU,EAAc,MAAM,YAAY,CAAC;AAGzF,6DAA6D;AAC7D,MAAM,WAAW,YAAa,SAAQ,WAAW;IAC/C,QAAQ,CAAC,aAAa,EAAE,YAAY,CAAC;IACrC,oFAAoF;IACpF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IAC3E,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IAC9E,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IACtC,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC1C,QAAQ,CAAC,aAAa,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,IAAI,IAAI,CAAC;CAChB;AAED,2CAA2C;AAC3C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IACtC,mCAAmC;IACnC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,IAAI,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,YAAa,SAAQ,aAAa;IACjD,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IAC/D,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CAC/D;AAED,MAAM,WAAW,YAAY;IAC3B,oEAAoE;IACpE,MAAM,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5D,+EAA+E;IAC/E,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC5C,+EAA+E;IAC/E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,uEAAuE;IACvE,OAAO,IAAI,IAAI,CAAC;CACjB;AAoED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,YAAY,EACvB,KAAK,EAAE,cAAc,EACrB,OAAO,GAAE,YAAiB,GACzB,YAAY,CAqTd"}