@principal-ai/subsystems-react 0.51.2 → 0.51.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/subsystem/C4Graph.d.ts +12 -1
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +115 -22
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/C4NodeCard.d.ts +22 -22
- package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
- package/dist/subsystem/C4NodeCard.js +56 -77
- package/dist/subsystem/C4NodeCard.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +32 -13
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
- package/dist/subsystem/TrailsPanel.js +8 -4
- package/dist/subsystem/TrailsPanel.js.map +1 -1
- package/dist/subsystem/c4.d.ts +24 -86
- package/dist/subsystem/c4.d.ts.map +1 -1
- package/dist/subsystem/c4.js +6 -285
- package/dist/subsystem/c4.js.map +1 -1
- package/dist/subsystem/c4Evidence.js +1 -1
- package/dist/subsystem/c4Evidence.js.map +1 -1
- package/dist/subsystem/model.d.ts +14 -0
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +4 -2
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/techIcons/bun.d.ts +11 -0
- package/dist/subsystem/techIcons/bun.d.ts.map +1 -0
- package/dist/subsystem/techIcons/bun.js +11 -0
- package/dist/subsystem/techIcons/bun.js.map +1 -0
- package/dist/subsystem/techIcons/index.d.ts +36 -0
- package/dist/subsystem/techIcons/index.d.ts.map +1 -0
- package/dist/subsystem/techIcons/index.js +55 -0
- package/dist/subsystem/techIcons/index.js.map +1 -0
- package/dist/subsystem/techIcons/node.d.ts +12 -0
- package/dist/subsystem/techIcons/node.d.ts.map +1 -0
- package/dist/subsystem/techIcons/node.js +39 -0
- package/dist/subsystem/techIcons/node.js.map +1 -0
- package/dist/subsystem/techIcons/react.d.ts +12 -0
- package/dist/subsystem/techIcons/react.d.ts.map +1 -0
- package/dist/subsystem/techIcons/react.js +19 -0
- package/dist/subsystem/techIcons/react.js.map +1 -0
- package/dist/utils/edgeLabel.d.ts +1 -1
- package/dist/utils/edgeLabel.d.ts.map +1 -1
- package/dist/utils/edgeLabel.js +1 -1
- package/dist/utils/edgeLabel.js.map +1 -1
- package/dist/utils/elkLayout.d.ts +18 -0
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +90 -9
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +4 -6
- package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +212 -0
- package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +20 -10
- package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +4 -2
- package/src/stories/Subsystem/ComponentGraph/ProposedTrails.stories.tsx +21 -1
- package/src/subsystem/C4Graph.tsx +171 -19
- package/src/subsystem/C4NodeCard.test.ts +71 -77
- package/src/subsystem/C4NodeCard.tsx +65 -95
- package/src/subsystem/SubsystemComponentGraph.tsx +41 -13
- package/src/subsystem/TrailsPanel.tsx +10 -5
- package/src/subsystem/c4.ts +23 -357
- package/src/subsystem/c4Evidence.test.ts +1 -1
- package/src/subsystem/c4Evidence.ts +1 -1
- package/src/subsystem/model.ts +14 -0
- package/src/subsystem/nodes.tsx +5 -2
- package/src/subsystem/techIcons/bun.ts +10 -0
- package/src/subsystem/techIcons/index.tsx +73 -0
- package/src/subsystem/techIcons/node.ts +38 -0
- package/src/subsystem/techIcons/react.ts +18 -0
- package/src/subsystem/techIcons/techIcons.test.ts +39 -0
- package/src/utils/edgeLabel.ts +1 -1
- package/src/utils/elkLayout.test.ts +34 -0
- package/src/utils/elkLayout.ts +88 -9
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.d.ts +0 -493
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.d.ts.map +0 -1
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.js +0 -329
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.js.map +0 -1
- package/src/stories/Subsystem/C4Graph/ProposedElements.stories.tsx +0 -293
- package/src/stories/Subsystem/C4Graph/c4ProposedElements.fixture.ts +0 -366
- package/src/subsystem/c4.test.ts +0 -573
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
reorderTrails,
|
|
21
21
|
trailStepGraphEdgeId,
|
|
22
22
|
type SubsystemTrail,
|
|
23
|
+
type SubsystemTrailStep,
|
|
23
24
|
} from './model';
|
|
24
25
|
import { buildStepBrief } from './trailBrief';
|
|
25
26
|
|
|
@@ -90,10 +91,14 @@ function TrailFlow({
|
|
|
90
91
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
91
92
|
const hoverBg = theme.colors.background;
|
|
92
93
|
const wholeFlowActive = active !== null && active.stepIndex === null;
|
|
93
|
-
// A step is "proposed" when
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
94
|
+
// A step is "proposed" when it says so itself OR either endpoint component
|
|
95
|
+
// is proposed — the step flag covers planned seams whose endpoints already
|
|
96
|
+
// exist as real components.
|
|
97
|
+
const stepProposed = (step: SubsystemTrailStep) =>
|
|
98
|
+
step.proposed === true ||
|
|
99
|
+
(proposedAliases != null &&
|
|
100
|
+
(proposedAliases.has(step.from) || proposedAliases.has(step.to)));
|
|
101
|
+
const touchesProposed = trail.steps.some((s) => stepProposed(s));
|
|
97
102
|
const [headerHover, setHeaderHover] = useState(false);
|
|
98
103
|
const [collapseHover, setCollapseHover] = useState(false);
|
|
99
104
|
const [playHover, setPlayHover] = useState(false);
|
|
@@ -366,7 +371,7 @@ function TrailFlow({
|
|
|
366
371
|
>
|
|
367
372
|
{trail.steps.map((step, i) => {
|
|
368
373
|
const stepActive = active !== null && active.stepIndex === i;
|
|
369
|
-
const proposed = stepProposed(step
|
|
374
|
+
const proposed = stepProposed(step);
|
|
370
375
|
const revealed =
|
|
371
376
|
stepActive || hoveredStep === i || focusedStep === i || copiedStep === i;
|
|
372
377
|
const copied = copiedStep === i;
|
package/src/subsystem/c4.ts
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* reading `process` strings will produce it — `process` answers "where does
|
|
8
8
|
* this run", which is a different question.
|
|
9
9
|
*
|
|
10
|
-
* So the element list is authored
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* So the element list is authored, and so is the `C4Model` projected from it.
|
|
11
|
+
* There is no builder here: how edges are determined is still open, so a caller
|
|
12
|
+
* supplies the drawable model directly rather than having trails converted into
|
|
13
|
+
* lines.
|
|
13
14
|
*
|
|
14
15
|
* ## Why a union and not one type with two axes
|
|
15
16
|
*
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
* [C4 notation]: https://c4model.com/diagrams/notation
|
|
32
33
|
*/
|
|
33
34
|
|
|
34
|
-
import type { SubsystemComponent
|
|
35
|
+
import type { SubsystemComponent } from './model';
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Has anyone signed off on this element yet?
|
|
@@ -55,12 +56,9 @@ interface C4ElementBase {
|
|
|
55
56
|
* The component aliases that live directly inside this element.
|
|
56
57
|
*
|
|
57
58
|
* Named explicitly rather than derived from a `process` value, because two
|
|
58
|
-
* runs of code being one container is a judgement, not a lookup.
|
|
59
|
-
* the dedup mechanism: two elements that should be one simply end up as one
|
|
60
|
-
* element holding both member sets.
|
|
59
|
+
* runs of code being one container is a judgement, not a lookup.
|
|
61
60
|
*
|
|
62
|
-
* Each alias belongs to exactly one element.
|
|
63
|
-
* the aliases its components already claim — `buildC4Model` rolls those up.
|
|
61
|
+
* Each alias belongs to exactly one element.
|
|
64
62
|
*/
|
|
65
63
|
members: string[];
|
|
66
64
|
/**
|
|
@@ -185,18 +183,29 @@ export interface C4Group {
|
|
|
185
183
|
memberIds: string[];
|
|
186
184
|
}
|
|
187
185
|
|
|
188
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* A line between two elements. Authored, like the boxes.
|
|
188
|
+
*
|
|
189
|
+
* C4 asks a container-diagram line to say the *technology/protocol* of the
|
|
190
|
+
* interaction (`protocol`), and the label is that plus the trail verbs where
|
|
191
|
+
* known. `label` is what the chip shows; `protocol` and `mechanisms` are kept
|
|
192
|
+
* separate so a caller can render either.
|
|
193
|
+
*/
|
|
189
194
|
export interface C4Edge {
|
|
190
195
|
id: string;
|
|
191
196
|
source: string;
|
|
192
197
|
target: string;
|
|
198
|
+
/** Text the edge chip shows, e.g. `RPC` or `calls (HTTP)`. */
|
|
193
199
|
label: string;
|
|
200
|
+
/** The interaction's technology/protocol, e.g. `RPC`, `HTTP`, `stdin`. */
|
|
201
|
+
protocol?: string;
|
|
202
|
+
/** Trail verbs that produced this line (`calls`, `writes`, …). */
|
|
194
203
|
mechanisms: string[];
|
|
195
|
-
/**
|
|
204
|
+
/** How many underlying steps this line rolled up. */
|
|
196
205
|
count: number;
|
|
197
206
|
}
|
|
198
207
|
|
|
199
|
-
/** A drawable C4 projection. */
|
|
208
|
+
/** A drawable C4 projection. Authored, not derived. */
|
|
200
209
|
export interface C4Model {
|
|
201
210
|
view: C4View;
|
|
202
211
|
system: { id: string; label: string; repoKey?: string };
|
|
@@ -205,355 +214,12 @@ export interface C4Model {
|
|
|
205
214
|
edges: C4Edge[];
|
|
206
215
|
}
|
|
207
216
|
|
|
208
|
-
/** Something an author should fix before this set is trusted. */
|
|
209
|
-
export type C4ProblemKind =
|
|
210
|
-
/** a member is claimed by two elements */
|
|
211
|
-
| 'conflicting_member'
|
|
212
|
-
/** a member is not in the supplied document */
|
|
213
|
-
| 'unknown_member'
|
|
214
|
-
/** two elements share an id */
|
|
215
|
-
| 'duplicate_id'
|
|
216
|
-
/** an element that should have contents has none */
|
|
217
|
-
| 'empty_element'
|
|
218
|
-
/** a component names a container that is missing, or is not a container */
|
|
219
|
-
| 'unknown_container';
|
|
220
|
-
|
|
221
|
-
export interface C4Problem {
|
|
222
|
-
kind: C4ProblemKind;
|
|
223
|
-
message: string;
|
|
224
|
-
/** Alias for member problems, element id for the rest. */
|
|
225
|
-
subject: string;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export interface C4BuildResult {
|
|
229
|
-
model: C4Model;
|
|
230
|
-
/** Authoring mistakes. An empty array means the set is self-consistent. */
|
|
231
|
-
problems: C4Problem[];
|
|
232
|
-
/** Component aliases in the document that no element claims. */
|
|
233
|
-
unmapped: string[];
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export interface C4BuildOptions {
|
|
237
|
-
/** The elements. The source of truth — nothing is invented. */
|
|
238
|
-
elements: readonly C4Element[];
|
|
239
|
-
/** Supply to resolve members and draw lines from trails. */
|
|
240
|
-
document?: SubsystemModelDocument;
|
|
241
|
-
view?: C4View;
|
|
242
|
-
/** Name of the system the elements sit in. */
|
|
243
|
-
systemLabel?: string;
|
|
244
|
-
/** Override the repo key (defaults to the first member's purl). */
|
|
245
|
-
repoKey?: string;
|
|
246
|
-
/** Model-id attribution per component alias. */
|
|
247
|
-
modelsByAlias?: Record<string, string[]>;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
217
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* A C4 container diagram puts external systems *inside* the boundary: they are
|
|
254
|
-
* things this system depends on, drawn as containers we do not own. People go
|
|
255
|
-
* outside it, because a person is never part of any system.
|
|
218
|
+
* `owner/name` from a purl, trimmed of scheme and fragment. Used to label the
|
|
219
|
+
* system frame when a repo key is known.
|
|
256
220
|
*/
|
|
257
|
-
const IN_BOUNDARY = new Set<C4Element['kind']>(['container', 'external-system']);
|
|
258
|
-
|
|
259
|
-
/** True when this element kind is drawn inside the system frame. */
|
|
260
|
-
export function isInBoundary(e: C4Element): boolean {
|
|
261
|
-
return IN_BOUNDARY.has(e.kind);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function purlRepoKey(purl: string | undefined): string | undefined {
|
|
265
|
-
const p = (purl ?? '').split('#')[0]?.trim();
|
|
266
|
-
if (!p || p === 'external' || p.startsWith('external:')) return undefined;
|
|
267
|
-
return p;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/** `owner/name` from a purl, trimmed of scheme and fragment. */
|
|
271
221
|
export function labelFromPurl(purl: string): string {
|
|
272
222
|
const base = purl.replace(/^external:/, '').split('#')[0]?.trim() ?? '';
|
|
273
223
|
const parts = base.split('/').filter(Boolean);
|
|
274
224
|
return parts.slice(-2).join('/') || base || purl;
|
|
275
225
|
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Build the drawable projection from authored elements.
|
|
279
|
-
*
|
|
280
|
-
* A plain join with no architectural judgement:
|
|
281
|
-
*
|
|
282
|
-
* boxes ← one per element, named and placed by the author
|
|
283
|
-
* lines ← trail steps, with each endpoint mapped to its owning element
|
|
284
|
-
*
|
|
285
|
-
* Trail steps whose endpoints are not claimed by any element are skipped:
|
|
286
|
-
* drawing them would assert a relationship the author did not describe.
|
|
287
|
-
*/
|
|
288
|
-
export function buildC4Model(options: C4BuildOptions): C4BuildResult {
|
|
289
|
-
const view = options.view ?? 'container';
|
|
290
|
-
const doc = options.document;
|
|
291
|
-
const problems: C4Problem[] = [];
|
|
292
|
-
|
|
293
|
-
const byAlias = new Map(doc?.components.map((c) => [c.alias, c]) ?? []);
|
|
294
|
-
/**
|
|
295
|
-
* Alias → owning element, innermost home wins. One owner per alias: this is
|
|
296
|
-
* what makes `conflicting_member` mean "two boxes disagree" rather than
|
|
297
|
-
* "a container and its components both mention the same code".
|
|
298
|
-
*/
|
|
299
|
-
const membersOf = new Map<string, string[]>();
|
|
300
|
-
|
|
301
|
-
// --- accept each element once, and resolve membership --------------------
|
|
302
|
-
const seenIds = new Set<string>();
|
|
303
|
-
/** alias → element id. One owner per alias; conflicts are reported. */
|
|
304
|
-
const ownerOf = new Map<string, string>();
|
|
305
|
-
/** Ids in authored order, duplicates dropped. */
|
|
306
|
-
const accepted: string[] = [];
|
|
307
|
-
const elementById = new Map<string, C4Element>();
|
|
308
|
-
|
|
309
|
-
for (const e of options.elements) {
|
|
310
|
-
if (seenIds.has(e.id)) {
|
|
311
|
-
problems.push({
|
|
312
|
-
kind: 'duplicate_id',
|
|
313
|
-
subject: e.id,
|
|
314
|
-
message: `two elements share the id ${e.id}`,
|
|
315
|
-
});
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
seenIds.add(e.id);
|
|
319
|
-
accepted.push(e.id);
|
|
320
|
-
elementById.set(e.id, e);
|
|
321
|
-
|
|
322
|
-
// A rejected element is not drawn, so its members are inside nothing.
|
|
323
|
-
// Leaving them claimed would hide them from `unmapped` and quietly hide
|
|
324
|
-
// the fact that rejecting an element orphans whatever was in it.
|
|
325
|
-
if (e.state === 'rejected') continue;
|
|
326
|
-
|
|
327
|
-
for (const alias of e.members) {
|
|
328
|
-
if (doc && !byAlias.has(alias)) {
|
|
329
|
-
problems.push({
|
|
330
|
-
kind: 'unknown_member',
|
|
331
|
-
subject: alias,
|
|
332
|
-
message: `element "${e.id}" claims "${alias}", which is not in the supplied document`,
|
|
333
|
-
});
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
const existing = ownerOf.get(alias);
|
|
337
|
-
if (existing && existing !== e.id) {
|
|
338
|
-
problems.push({
|
|
339
|
-
kind: 'conflicting_member',
|
|
340
|
-
subject: alias,
|
|
341
|
-
message: `"${alias}" is claimed by both ${existing} and ${e.id}; it is drawn inside ${existing}`,
|
|
342
|
-
});
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
ownerOf.set(alias, e.id);
|
|
346
|
-
const list = membersOf.get(e.id) ?? [];
|
|
347
|
-
list.push(alias);
|
|
348
|
-
membersOf.set(e.id, list);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// --- the system boundary -------------------------------------------------
|
|
353
|
-
const repoKey =
|
|
354
|
-
options.repoKey ??
|
|
355
|
-
options.elements
|
|
356
|
-
.flatMap((e) => e.members)
|
|
357
|
-
.map((alias) => purlRepoKey(byAlias.get(alias)?.purl))
|
|
358
|
-
.find(Boolean);
|
|
359
|
-
|
|
360
|
-
const systemId = `system:${repoKey ?? 'unspecified'}`;
|
|
361
|
-
const systemLabel = options.systemLabel ?? (repoKey ? labelFromPurl(repoKey) : 'Software System');
|
|
362
|
-
|
|
363
|
-
// --- draw the boxes ------------------------------------------------------
|
|
364
|
-
const nodes: C4Node[] = [];
|
|
365
|
-
/**
|
|
366
|
-
* The aliases an element claimed itself, before any rollup. A component's
|
|
367
|
-
* own members and its container's own members are disjoint by construction,
|
|
368
|
-
* which is what lets the two be added together below.
|
|
369
|
-
*/
|
|
370
|
-
const ownMembers = new Map<string, string[]>();
|
|
371
|
-
for (const id of accepted) {
|
|
372
|
-
const e = elementById.get(id)!;
|
|
373
|
-
if (e.state === 'rejected') continue;
|
|
374
|
-
|
|
375
|
-
const own = membersOf.get(id) ?? [];
|
|
376
|
-
|
|
377
|
-
// A component's parent is stated, not inferred. Check it resolves: a
|
|
378
|
-
// dangling parent would silently drop the component out of every frame.
|
|
379
|
-
let parentId: string | undefined;
|
|
380
|
-
if (e.kind === 'component') {
|
|
381
|
-
const parent = elementById.get(e.container);
|
|
382
|
-
if (!parent || parent.state === 'rejected') {
|
|
383
|
-
problems.push({
|
|
384
|
-
kind: 'unknown_container',
|
|
385
|
-
subject: id,
|
|
386
|
-
message: `component "${id}" names container "${e.container}", which is not in this set`,
|
|
387
|
-
});
|
|
388
|
-
} else if (parent.kind !== 'container') {
|
|
389
|
-
problems.push({
|
|
390
|
-
kind: 'unknown_container',
|
|
391
|
-
subject: id,
|
|
392
|
-
message: `component "${id}" names container "${e.container}", which is a ${parent.kind}, not a container`,
|
|
393
|
-
});
|
|
394
|
-
} else {
|
|
395
|
-
parentId = parent.id;
|
|
396
|
-
}
|
|
397
|
-
} else if (isInBoundary(e)) {
|
|
398
|
-
parentId = systemId;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
ownMembers.set(id, own);
|
|
402
|
-
nodes.push({
|
|
403
|
-
id: e.id,
|
|
404
|
-
kind: e.kind,
|
|
405
|
-
label: e.label,
|
|
406
|
-
...(parentId ? { parentId } : {}),
|
|
407
|
-
technology: 'technology' in e ? e.technology : undefined,
|
|
408
|
-
description: e.description,
|
|
409
|
-
state: e.state,
|
|
410
|
-
...(e.kind === 'container' ? { containerKind: e.containerKind } : {}),
|
|
411
|
-
...(e.derivedFrom && e.derivedFrom.length > 1 ? { derivedFrom: e.derivedFrom } : {}),
|
|
412
|
-
members: own,
|
|
413
|
-
constructs: [],
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// --- roll components up into their container ------------------------------
|
|
418
|
-
//
|
|
419
|
-
// A C4 container diagram draws the container as a whole, so a container
|
|
420
|
-
// holds everything inside its components. Membership is still authored
|
|
421
|
-
// exactly once: an alias belongs to one element, so a container must not
|
|
422
|
-
// repeat what its components already claim — that repetition would read as
|
|
423
|
-
// a `conflicting_member` and hide the real question, which is how many
|
|
424
|
-
// components a container has.
|
|
425
|
-
for (const n of nodes) {
|
|
426
|
-
if (n.kind !== 'component' || !n.parentId) continue;
|
|
427
|
-
const list = membersOf.get(n.parentId) ?? [];
|
|
428
|
-
list.push(...(ownMembers.get(n.id) ?? []));
|
|
429
|
-
membersOf.set(n.parentId, list);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
for (const n of nodes) {
|
|
433
|
-
n.members = membersOf.get(n.id) ?? [];
|
|
434
|
-
if (n.members.length === 0) {
|
|
435
|
-
problems.push({
|
|
436
|
-
kind: 'empty_element',
|
|
437
|
-
subject: n.id,
|
|
438
|
-
message: `element "${n.id}" (${n.kind}) claims no members, and nothing is inside it either, so there is nothing to draw`,
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
const resolved = n.members.map((m) => byAlias.get(m)).filter((c): c is SubsystemComponent => !!c);
|
|
442
|
-
const constructs: string[] = [];
|
|
443
|
-
for (const c of resolved) if (!constructs.includes(c.construct)) constructs.push(c.construct);
|
|
444
|
-
n.constructs = constructs;
|
|
445
|
-
if (n.kind === 'component' && resolved[0]) n.component = resolved[0];
|
|
446
|
-
|
|
447
|
-
const models = [...new Set(n.members.flatMap((m) => options.modelsByAlias?.[m] ?? []))];
|
|
448
|
-
if (models.length > 0) n.models = models;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// --- compound frames -----------------------------------------------------
|
|
452
|
-
const groups: C4Group[] = [];
|
|
453
|
-
|
|
454
|
-
if (view === 'container') {
|
|
455
|
-
const containerIds = nodes.filter((n) => n.kind === 'container').map((n) => n.id);
|
|
456
|
-
if (containerIds.length > 0) {
|
|
457
|
-
groups.push({ id: systemId, kind: 'system', label: systemLabel, memberIds: containerIds });
|
|
458
|
-
}
|
|
459
|
-
} else {
|
|
460
|
-
const byContainer = new Map<string, string[]>();
|
|
461
|
-
for (const n of nodes) {
|
|
462
|
-
if (n.kind !== 'component' || !n.parentId) continue;
|
|
463
|
-
const list = byContainer.get(n.parentId) ?? [];
|
|
464
|
-
list.push(n.id);
|
|
465
|
-
byContainer.set(n.parentId, list);
|
|
466
|
-
}
|
|
467
|
-
const framed: string[] = [];
|
|
468
|
-
for (const [id, memberIds] of byContainer) {
|
|
469
|
-
const label = nodes.find((n) => n.id === id)?.label ?? id;
|
|
470
|
-
groups.push({ id, kind: 'container', label, parentId: systemId, memberIds });
|
|
471
|
-
framed.push(id);
|
|
472
|
-
}
|
|
473
|
-
if (framed.length > 0) {
|
|
474
|
-
groups.push({ id: systemId, kind: 'system', label: systemLabel, memberIds: framed });
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// --- lines, from the trails ---------------------------------------------
|
|
479
|
-
const drawn = new Set(nodes.map((n) => n.id));
|
|
480
|
-
const edges = new Map<string, C4Edge>();
|
|
481
|
-
for (const trail of doc?.trails ?? []) {
|
|
482
|
-
for (const step of trail.steps ?? []) {
|
|
483
|
-
const source = ownerOf.get(step.from);
|
|
484
|
-
const target = ownerOf.get(step.to);
|
|
485
|
-
// An endpoint no element claims has no box, so there is no line to draw.
|
|
486
|
-
if (!source || !target || source === target) continue;
|
|
487
|
-
if (!drawn.has(source) || !drawn.has(target)) continue;
|
|
488
|
-
const key = `${source}\0${target}`;
|
|
489
|
-
const edge = edges.get(key);
|
|
490
|
-
if (edge) {
|
|
491
|
-
edge.count += 1;
|
|
492
|
-
if (!edge.mechanisms.includes(step.mechanism)) edge.mechanisms.push(step.mechanism);
|
|
493
|
-
} else {
|
|
494
|
-
edges.set(key, {
|
|
495
|
-
id: `flow:${key}`,
|
|
496
|
-
source,
|
|
497
|
-
target,
|
|
498
|
-
label: step.mechanism,
|
|
499
|
-
mechanisms: [step.mechanism],
|
|
500
|
-
count: 1,
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
for (const e of edges.values()) e.label = [...e.mechanisms].sort().join(' + ');
|
|
506
|
-
|
|
507
|
-
const unmapped = (doc?.components ?? []).filter((c) => !ownerOf.has(c.alias)).map((c) => c.alias);
|
|
508
|
-
|
|
509
|
-
return {
|
|
510
|
-
model: {
|
|
511
|
-
view,
|
|
512
|
-
system: { id: systemId, label: systemLabel, repoKey },
|
|
513
|
-
nodes,
|
|
514
|
-
groups,
|
|
515
|
-
edges: [...edges.values()],
|
|
516
|
-
},
|
|
517
|
-
problems,
|
|
518
|
-
unmapped,
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Tally for the confirm UI: how much is proposed vs accepted, and how much is
|
|
524
|
-
* still missing what the C4 notation requires.
|
|
525
|
-
*
|
|
526
|
-
* Counts the authored set, not the projection — a rejected element is still a
|
|
527
|
-
* decision that was made, so it is reported rather than forgotten.
|
|
528
|
-
*/
|
|
529
|
-
export function summarizeElements(elements: readonly C4Element[]): {
|
|
530
|
-
containers: number;
|
|
531
|
-
components: number;
|
|
532
|
-
externals: number;
|
|
533
|
-
people: number;
|
|
534
|
-
accepted: number;
|
|
535
|
-
proposed: number;
|
|
536
|
-
rejected: number;
|
|
537
|
-
/** Containers missing the technology C4 requires. */
|
|
538
|
-
missingTechnology: number;
|
|
539
|
-
/** Elements missing a description. */
|
|
540
|
-
missingDescription: number;
|
|
541
|
-
/** Elements whose state is not yet `accepted`. */
|
|
542
|
-
needsDecision: number;
|
|
543
|
-
} {
|
|
544
|
-
const live = elements.filter((e) => e.state !== 'rejected');
|
|
545
|
-
return {
|
|
546
|
-
containers: live.filter((e) => e.kind === 'container').length,
|
|
547
|
-
components: live.filter((e) => e.kind === 'component').length,
|
|
548
|
-
externals: live.filter((e) => e.kind === 'external-system').length,
|
|
549
|
-
people: live.filter((e) => e.kind === 'person').length,
|
|
550
|
-
accepted: live.filter((e) => e.state === 'accepted').length,
|
|
551
|
-
proposed: live.filter((e) => e.state === 'proposed').length,
|
|
552
|
-
rejected: elements.filter((e) => e.state === 'rejected').length,
|
|
553
|
-
// `technology` is required by the type on containers and components, so a
|
|
554
|
-
// missing one can only mean an empty string — still worth reporting.
|
|
555
|
-
missingTechnology: live.filter((e) => 'technology' in e && !e.technology).length,
|
|
556
|
-
missingDescription: live.filter((e) => !e.description).length,
|
|
557
|
-
needsDecision: live.filter((e) => e.state !== 'accepted').length,
|
|
558
|
-
};
|
|
559
|
-
}
|
|
@@ -261,7 +261,7 @@ describe('proposeElements', () => {
|
|
|
261
261
|
});
|
|
262
262
|
|
|
263
263
|
test('a key with no framework signal gets an empty technology, not a guess', () => {
|
|
264
|
-
// `technology` is required by the type, and
|
|
264
|
+
// `technology` is required by the type, and an empty one is reported.
|
|
265
265
|
// gap. Writing "TypeScript" here would be the derivation this module does
|
|
266
266
|
// not do.
|
|
267
267
|
const set = proposeElements({ repoKey: REPO, rollup });
|
|
@@ -288,7 +288,7 @@ export function proposeElements(
|
|
|
288
288
|
label: key,
|
|
289
289
|
containerKind: suggestContainerKind(members),
|
|
290
290
|
// C4 requires a technology on every container. With no framework signal
|
|
291
|
-
// the honest value is empty and
|
|
291
|
+
// the honest value is empty and the notation-gap check reports it —
|
|
292
292
|
// inventing "TypeScript" here would be exactly the derivation this
|
|
293
293
|
// module stopped doing.
|
|
294
294
|
technology: frameworks.length > 0 ? frameworks.join(' + ') : '',
|
package/src/subsystem/model.ts
CHANGED
|
@@ -406,6 +406,14 @@ export interface SubsystemTrailStep {
|
|
|
406
406
|
* mechanism + filename fallback.
|
|
407
407
|
*/
|
|
408
408
|
symbol: string;
|
|
409
|
+
/**
|
|
410
|
+
* Planned seam — the step describes intended behavior through code that is
|
|
411
|
+
* not written yet. Orthogonal to (and redundant with) the endpoints' own
|
|
412
|
+
* `proposed` flags: a step is proposed when it says so OR either endpoint
|
|
413
|
+
* is proposed. Verification skips source checks either way, and viewers
|
|
414
|
+
* tint it exactly like a proposed node.
|
|
415
|
+
*/
|
|
416
|
+
proposed?: boolean;
|
|
409
417
|
/**
|
|
410
418
|
* Free-text note anchored to this step's site line. Optional — informative
|
|
411
419
|
* only, never verified against source; the Pierre trail code view
|
|
@@ -1421,6 +1429,12 @@ export interface SubsystemGraphEdgeData extends Record<string, unknown> {
|
|
|
1421
1429
|
/** True while another edge is selected — render this edge (and its label)
|
|
1422
1430
|
* dimmed to focus the selected relationship. */
|
|
1423
1431
|
dimmed?: boolean;
|
|
1432
|
+
/**
|
|
1433
|
+
* A backing trail step declares itself `proposed` — a planned seam between
|
|
1434
|
+
* otherwise-real components. The SVG edge and its label draw dashed;
|
|
1435
|
+
* distinct from a proposed ENDPOINT, which dashes the node instead.
|
|
1436
|
+
*/
|
|
1437
|
+
proposedStep?: boolean;
|
|
1424
1438
|
/** ELK-computed label midpoint (from the actual edge path, not node centers). */
|
|
1425
1439
|
labelX?: number;
|
|
1426
1440
|
labelY?: number;
|
package/src/subsystem/nodes.tsx
CHANGED
|
@@ -833,8 +833,11 @@ export function SubsystemEdge({
|
|
|
833
833
|
const edgeRef = { mechanism, provenance: data?.provenance };
|
|
834
834
|
const color = edgeColor(edgeRef);
|
|
835
835
|
// Dash style encodes provenance (graphify) and relationship kind (dashed =
|
|
836
|
-
// inverted-control or observational: hierarchy, registration, watches).
|
|
837
|
-
|
|
836
|
+
// inverted-control or observational: hierarchy, registration, watches). A
|
|
837
|
+
// step-proposed edge also dashes — the seam itself is planned — matching
|
|
838
|
+
// the dashed edge label and a proposed node's dashed border.
|
|
839
|
+
const isDashed =
|
|
840
|
+
edgeStrokeStyle(edgeRef) === 'dashed' || data?.proposedStep === true;
|
|
838
841
|
const dimmed = data?.dimmed === true;
|
|
839
842
|
// Dim the stroke by color, never via path `opacity`. SVG markers are shared
|
|
840
843
|
// by id; opacity on the referencing path paints every arrowhead that uses
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun mark — the official logo, vendored verbatim.
|
|
3
|
+
*
|
|
4
|
+
* Source: https://bun.sh/logo.svg.
|
|
5
|
+
* Bun and the Bun logo are trademarks of Oven. Used here for identification
|
|
6
|
+
* only, with no endorsement implied.
|
|
7
|
+
*
|
|
8
|
+
* Root sized to 100% so the card's wrapper controls the box.
|
|
9
|
+
*/
|
|
10
|
+
export const bunSvg = `<svg id="Bun" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 80 70"><path id="Shadow" d="M71.09,20.74c-.16-.17-.33-.34-.5-.5s-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5A26.46,26.46,0,0,1,75.5,35.7c0,16.57-16.82,30.05-37.5,30.05-11.58,0-21.94-4.23-28.83-10.86l.5.5.5.5.5.5.5.5.5.5.5.5.5.5C19.55,65.3,30.14,69.75,42,69.75c20.68,0,37.5-13.48,37.5-30C79.5,32.69,76.46,26,71.09,20.74Z"/><g id="Body"><path id="Background" d="M73,35.7c0,15.21-15.67,27.54-35,27.54S3,50.91,3,35.7C3,26.27,9,17.94,18.22,13S33.18,3,38,3s8.94,4.13,19.78,10C67,17.94,73,26.27,73,35.7Z" style="fill:#fbf0df"/><path id="Bottom_Shadow" data-name="Bottom Shadow" d="M73,35.7a21.67,21.67,0,0,0-.8-5.78c-2.73,33.3-43.35,34.9-59.32,24.94A40,40,0,0,0,38,63.24C57.3,63.24,73,50.89,73,35.7Z" style="fill:#f6dece"/><path id="Light_Shine" data-name="Light Shine" d="M24.53,11.17C29,8.49,34.94,3.46,40.78,3.45A9.29,9.29,0,0,0,38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7c0,.4,0,.8,0,1.19C9.06,15.48,20.07,13.85,24.53,11.17Z" style="fill:#fffefc"/><path id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" style="fill:#ccbea7;fill-rule:evenodd"/><path id="Outline" d="M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75ZM38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7,3,50.89,18.7,63.25,38,63.25S73,50.89,73,35.7C73,26.62,67.31,18.13,57.78,13,54,11,51.05,9.12,48.66,7.64c-1.09-.67-2.09-1.29-3-1.84C42.63,4,40.42,3,38,3Z"/></g><g id="Mouth"><g id="Background-2" data-name="Background"><path d="M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z" style="fill:#b71422"/></g><g id="Tongue"><path id="Background-3" data-name="Background" d="M34,47.79a6.91,6.91,0,0,0,4.12,1.9,6.91,6.91,0,0,0,4.11-1.9,10.63,10.63,0,0,0,1-1.07,6.83,6.83,0,0,0-4.9-2.31,6.15,6.15,0,0,0-5,2.78C33.56,47.4,33.76,47.6,34,47.79Z" style="fill:#ff6164"/><path id="Outline-2" data-name="Outline" d="M34.16,47a5.36,5.36,0,0,1,4.19-2.08,6,6,0,0,1,4,1.69c.23-.25.45-.51.66-.77a7,7,0,0,0-4.71-1.93,6.36,6.36,0,0,0-4.89,2.36A9.53,9.53,0,0,0,34.16,47Z"/></g><path id="Outline-3" data-name="Outline" d="M38.09,50.19a7.42,7.42,0,0,1-4.45-2,9.52,9.52,0,0,1-3.11-5.05,1.2,1.2,0,0,1,.26-1,1.41,1.41,0,0,1,1.13-.51H44.26a1.44,1.44,0,0,1,1.13.51,1.19,1.19,0,0,1,.25,1h0a9.52,9.52,0,0,1-3.11,5.05A7.42,7.42,0,0,1,38.09,50.19Zm-6.17-7.4c-.16,0-.2.07-.21.09a8.29,8.29,0,0,0,2.73,4.37A6.23,6.23,0,0,0,38.09,49a6.28,6.28,0,0,0,3.65-1.73,8.3,8.3,0,0,0,2.72-4.37.21.21,0,0,0-.2-.09Z"/></g><g id="Face"><ellipse id="Right_Blush" data-name="Right Blush" cx="53.22" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><ellipse id="Left_Bluch" data-name="Left Bluch" cx="22.95" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><path id="Eyes" d="M25.7,38.8a5.51,5.51,0,1,0-5.5-5.51A5.51,5.51,0,0,0,25.7,38.8Zm24.77,0A5.51,5.51,0,1,0,45,33.29,5.5,5.5,0,0,0,50.47,38.8Z" style="fill-rule:evenodd"/><path id="Iris" d="M24,33.64a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,24,33.64Zm24.77,0a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,48.75,33.64Z" style="fill:#fff;fill-rule:evenodd"/></g></svg>`;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Technology marks — official brand SVGs, vendored one per file, matched
|
|
3
|
+
* against an element's free-text `technology`.
|
|
4
|
+
*
|
|
5
|
+
* The rule is deliberately strict: if we do not hold the official mark for a
|
|
6
|
+
* technology, the slot stays empty. We do not substitute a generic glyph or a
|
|
7
|
+
* lookalike — a wrong logo is worse than none. Adding a brand is dropping its
|
|
8
|
+
* SVG in this folder and adding one row to `TECH_BRANDS`.
|
|
9
|
+
*
|
|
10
|
+
* Brand logos remain the property of their owners and are used for
|
|
11
|
+
* identification only, under each owner's trademark policy.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { bunSvg } from './bun';
|
|
15
|
+
import { nodeSvg } from './node';
|
|
16
|
+
import { reactSvg } from './react';
|
|
17
|
+
|
|
18
|
+
export interface TechBrand {
|
|
19
|
+
/** Human name, used for the tooltip / accessible label. */
|
|
20
|
+
name: string;
|
|
21
|
+
/** Where the mark was vendored from. */
|
|
22
|
+
source: string;
|
|
23
|
+
/** The official SVG, with its root sized to 100%. */
|
|
24
|
+
svg: string;
|
|
25
|
+
/** Matched against the technology string. First match in `TECH_BRANDS` wins. */
|
|
26
|
+
match: RegExp;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Order matters: the first brand whose pattern matches is the one drawn, so
|
|
31
|
+
* put the more specific marks first if two could match the same string.
|
|
32
|
+
*/
|
|
33
|
+
export const TECH_BRANDS: readonly TechBrand[] = [
|
|
34
|
+
{
|
|
35
|
+
name: 'React',
|
|
36
|
+
source: 'https://react.dev',
|
|
37
|
+
svg: reactSvg,
|
|
38
|
+
match: /\breact\b/i,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'Bun',
|
|
42
|
+
source: 'https://bun.sh/logo.svg',
|
|
43
|
+
svg: bunSvg,
|
|
44
|
+
match: /\bbun\b/i,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'Node.js',
|
|
48
|
+
source: 'https://nodejs.org/static/logos/nodejsHex.svg',
|
|
49
|
+
svg: nodeSvg,
|
|
50
|
+
// Matches "Node" and "Node.js"; "Electrobun" has no word boundary before
|
|
51
|
+
// "bun" and never reaches here.
|
|
52
|
+
match: /\bnode(\.js)?\b/i,
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/** The official mark for a technology string, or undefined when we hold none. */
|
|
57
|
+
export function technologyBrand(technology: string | undefined): TechBrand | undefined {
|
|
58
|
+
if (!technology) return undefined;
|
|
59
|
+
return TECH_BRANDS.find((brand) => brand.match.test(technology));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A brand mark sized to the top row. Full-colour, not tinted by state. */
|
|
63
|
+
export function TechMark({ brand, size = 14 }: { brand: TechBrand; size?: number }) {
|
|
64
|
+
return (
|
|
65
|
+
<span
|
|
66
|
+
title={`${brand.name} — ${brand.source}`}
|
|
67
|
+
aria-label={brand.name}
|
|
68
|
+
style={{ display: 'inline-flex', width: size, height: size, flexShrink: 0 }}
|
|
69
|
+
// The SVG is a static, vendored string — never user input.
|
|
70
|
+
dangerouslySetInnerHTML={{ __html: brand.svg }}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js mark — the official hexagon logo, vendored verbatim.
|
|
3
|
+
*
|
|
4
|
+
* Source: https://nodejs.org/static/logos/nodejsHex.svg (via
|
|
5
|
+
* https://nodejs.org/en/about/branding). Node.js and the Node.js logo are
|
|
6
|
+
* trademarks of the OpenJS Foundation. Used here for identification only,
|
|
7
|
+
* subject to https://trademark-policy.openjsf.org/.
|
|
8
|
+
*
|
|
9
|
+
* Root sized to 100% so the card's wrapper controls the box.
|
|
10
|
+
*/
|
|
11
|
+
export const nodeSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 44 50">
|
|
12
|
+
<defs>
|
|
13
|
+
<clipPath id="hexClip">
|
|
14
|
+
<path d="M22.8725 0.4166C22.136 0 21.2616 0 20.5253 0.4166L1.1505 11.6694C0.4142 12.0862 0 12.8733 0 13.707V36.2584C0 37.0921 0.4602 37.8791 1.1505 38.296L20.5253 49.5487C21.2616 49.9653 22.136 49.9653 22.8725 49.5487L42.2471 38.296C42.9836 37.8791 43.3976 37.0921 43.3976 36.2584V13.707C43.3976 12.8733 42.9375 12.0862 42.2471 11.6694L22.8725 0.4166Z" />
|
|
15
|
+
</clipPath>
|
|
16
|
+
<linearGradient id="main" x1="30.33" y1="8.56" x2="14.9" y2="44.7" gradientUnits="userSpaceOnUse">
|
|
17
|
+
<stop stop-color="#3F8B3D"/>
|
|
18
|
+
<stop offset="0.64" stop-color="#3F873F"/>
|
|
19
|
+
<stop offset="0.93" stop-color="#3DA92E"/>
|
|
20
|
+
<stop offset="1" stop-color="#3DAE2B"/>
|
|
21
|
+
</linearGradient>
|
|
22
|
+
<linearGradient id="r1" x1="18.8" y1="26.8" x2="68" y2="0.4" gradientUnits="userSpaceOnUse">
|
|
23
|
+
<stop offset="0.14" stop-color="#3F873F"/>
|
|
24
|
+
<stop offset="0.4" stop-color="#52A044"/>
|
|
25
|
+
<stop offset="0.71" stop-color="#64B749"/>
|
|
26
|
+
<stop offset="0.91" stop-color="#6ABF4B"/>
|
|
27
|
+
</linearGradient>
|
|
28
|
+
<linearGradient id="r2" x1="0.25" y1="24.5" x2="44" y2="24.5" gradientUnits="userSpaceOnUse">
|
|
29
|
+
<stop offset="0.09" stop-color="#6ABF4B"/>
|
|
30
|
+
<stop offset="0.29" stop-color="#64B749"/>
|
|
31
|
+
<stop offset="0.6" stop-color="#52A044"/>
|
|
32
|
+
<stop offset="0.86" stop-color="#3F873F"/>
|
|
33
|
+
</linearGradient>
|
|
34
|
+
</defs>
|
|
35
|
+
<path fill="url(#main)" d="M22.8725 0.4166C22.136 0 21.2616 0 20.5253 0.4166L1.1505 11.6694C0.4142 12.0862 0 12.8733 0 13.707V36.2584C0 37.0921 0.4602 37.8791 1.1505 38.296L20.5253 49.5487C21.2616 49.9653 22.136 49.9653 22.8725 49.5487L42.2471 38.296C42.9836 37.8791 43.3976 37.0921 43.3976 36.2584V13.707C43.3976 12.8733 42.9375 12.0862 42.2471 11.6694L22.8725 0.4166Z" />
|
|
36
|
+
<polygon fill="url(#r1)" clip-path="url(#hexClip)" points="21.698901,-1.046618 43.20532,11.948247 21.698901,51.072715 0.152778,38.055107" />
|
|
37
|
+
<polygon fill="url(#r2)" clip-path="url(#hexClip)" points="21.698901,-1.046618 0.152778,11.948247 21.698901,51.072715 43.20532,38.055107" />
|
|
38
|
+
</svg>`;
|