fullstackgtm 0.30.0 → 0.31.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/CHANGELOG.md +12 -0
- package/dist/market.d.ts +8 -0
- package/dist/marketReport.js +16 -2
- package/package.json +1 -1
- package/src/market.ts +8 -0
- package/src/marketReport.ts +16 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
5
5
|
and the project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
The path to 1.0 is planned in [docs/roadmap-to-1.0.md](./docs/roadmap-to-1.0.md).
|
|
7
7
|
|
|
8
|
+
## [0.31.0] — 2026-06-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Vendor logos in the market-map report.** `MarketVendor` gains an optional
|
|
13
|
+
`logo` field; `marketMapToHtml` renders it beside the name in the legend and
|
|
14
|
+
above the matrix column headers, degrading to the numbered swatch / plain name
|
|
15
|
+
when absent. Only `data:image/…` URIs are honored — keeping the report
|
|
16
|
+
self-contained (no external requests, survives being saved or emailed) and safe
|
|
17
|
+
under a strict `img-src data:` CSP. The hosted service extracts a canonical logo
|
|
18
|
+
from each vendor's homepage; CLI users can set `logo` by hand.
|
|
19
|
+
|
|
8
20
|
## [0.30.0] — 2026-06-17
|
|
9
21
|
|
|
10
22
|
Connector fixes — the two concrete defects a real HubSpot/Salesforce shop hits.
|
package/dist/market.d.ts
CHANGED
|
@@ -81,6 +81,14 @@ export type MarketVendor = {
|
|
|
81
81
|
* obvious from the vendor's own pricing page (which the map captures).
|
|
82
82
|
*/
|
|
83
83
|
acvBand?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Optional brand logo for the report (legend + matrix headers). A `data:` URI
|
|
86
|
+
* keeps the rendered report self-contained — no external requests, survives
|
|
87
|
+
* being saved or emailed. The hosted service extracts it from the vendor's
|
|
88
|
+
* homepage; CLI users can set it by hand. Renderers degrade gracefully to the
|
|
89
|
+
* numbered swatch when absent.
|
|
90
|
+
*/
|
|
91
|
+
logo?: string;
|
|
84
92
|
notes?: string;
|
|
85
93
|
};
|
|
86
94
|
export type MarketAxis = {
|
package/dist/marketReport.js
CHANGED
|
@@ -28,6 +28,17 @@ function escapeHtml(value) {
|
|
|
28
28
|
.replace(/>/g, ">")
|
|
29
29
|
.replace(/"/g, """);
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* A small brand logo for legend rows / matrix headers. Accepts only `data:image/`
|
|
33
|
+
* URIs — self-contained (no external request, survives save/email) and safe under
|
|
34
|
+
* the report's `img-src data:` CSP (an SVG loaded via <img> can't execute script).
|
|
35
|
+
* Returns "" when absent so callers fall back to the numbered swatch / plain name.
|
|
36
|
+
*/
|
|
37
|
+
function logoImg(logo, cls) {
|
|
38
|
+
if (typeof logo !== "string" || !logo.startsWith("data:image/"))
|
|
39
|
+
return "";
|
|
40
|
+
return `<img class="${cls}" src="${escapeHtml(logo)}" alt="" loading="lazy">`;
|
|
41
|
+
}
|
|
31
42
|
/**
|
|
32
43
|
* Serialize JSON for embedding inside an inline <script> block. JSON.stringify
|
|
33
44
|
* does not escape `<`, `>`, `&`, or the U+2028/U+2029 line separators, so a
|
|
@@ -291,6 +302,7 @@ function axisSectionsHtml(config, set) {
|
|
|
291
302
|
// The number inside each bubble resolves dense clusters that name labels
|
|
292
303
|
// never could; color is Okabe–Ito (colorblind-safe) keyed in the legend.
|
|
293
304
|
const points = pointsFor(px, py);
|
|
305
|
+
const logoByVendor = new Map(config.vendors.map((vendor) => [vendor.id, vendor.logo]));
|
|
294
306
|
const legendOrder = [...points].sort((a, b) => b.size - a.size || a.name.localeCompare(b.name));
|
|
295
307
|
const numberByVendor = new Map(legendOrder.map((point, index) => [point.vendorId, index + 1]));
|
|
296
308
|
const colorByVendor = new Map(legendOrder.map((point, index) => [point.vendorId, VENDOR_COLORS[index % VENDOR_COLORS.length]]));
|
|
@@ -305,7 +317,7 @@ function axisSectionsHtml(config, set) {
|
|
|
305
317
|
? `${(share * 100).toFixed(1)}%`
|
|
306
318
|
: "—"
|
|
307
319
|
: `${loudCounts.get(point.vendorId) ?? 0} loud`;
|
|
308
|
-
return `<tr data-v="${e(point.vendorId)}"${isAnchor ? ' class="anchor-row"' : ""}><td><span class="swatch" style="background:${color};color:${numeralColor(color)}">${number}</span></td><td>${e(point.name)}${isAnchor ? " · anchor" : ""}</td><td class="num">${measure}</td></tr>`;
|
|
320
|
+
return `<tr data-v="${e(point.vendorId)}"${isAnchor ? ' class="anchor-row"' : ""}><td><span class="swatch" style="background:${color};color:${numeralColor(color)}">${number}</span></td><td>${logoImg(logoByVendor.get(point.vendorId), "v-logo")}${e(point.name)}${isAnchor ? " · anchor" : ""}</td><td class="num">${measure}</td></tr>`;
|
|
309
321
|
})
|
|
310
322
|
.join("");
|
|
311
323
|
const legendMeasureHead = useScale ? "est. share" : "loud";
|
|
@@ -426,7 +438,7 @@ export function marketMapToHtml(config, set) {
|
|
|
426
438
|
`<td class="front"><span class="chip chip-${state}">${state.toUpperCase()}</span></td></tr>`);
|
|
427
439
|
};
|
|
428
440
|
const vendorHeads = config.vendors
|
|
429
|
-
.map((vendor) => `<th class="vh${vendor.id === anchor ? " anchor-col" : ""}"
|
|
441
|
+
.map((vendor) => `<th class="vh${vendor.id === anchor ? " anchor-col" : ""}">${logoImg(vendor.logo, "vh-logo")}<span>${e(vendor.name)}</span></th>`)
|
|
430
442
|
.join("");
|
|
431
443
|
// Claims grouped by front state, each group a collapsed <details> whose
|
|
432
444
|
// summary carries the stats a skimmer needs; the full matrix is one click
|
|
@@ -562,6 +574,8 @@ tr.front-open th .claim-cap { color:var(--accent); font-weight:600; }
|
|
|
562
574
|
figure.map { margin-top:16px; border:1px solid var(--line); position:relative; }
|
|
563
575
|
g.bubble { cursor:pointer; }
|
|
564
576
|
g.bubble.dim { opacity:0.25; transition:opacity .12s; }
|
|
577
|
+
img.v-logo { width:15px; height:15px; border-radius:3px; object-fit:contain; vertical-align:-3px; margin-right:6px; background:#fff; }
|
|
578
|
+
th.vh img.vh-logo { display:block; width:18px; height:18px; border-radius:3px; object-fit:contain; margin:0 auto 4px; background:#fff; }
|
|
565
579
|
table.legend tbody tr { cursor:default; }
|
|
566
580
|
table.legend tbody tr.hl td { background:var(--faint); }
|
|
567
581
|
.map-tip { position:absolute; z-index:5; background:#1c1c1c; color:#fff; font-size:11.5px; line-height:1.45;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullstackgtm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Open-source agentic GTM ops framework: canonical GTM data model, pluggable deterministic audits, reviewable dry-run patch plans, approval-gated write-back with conflict detection, and cross-system entity resolution. HubSpot, Salesforce, and Stripe connectors included.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Full Stack GTM LLC <ryan@fullstackgtm.com> (https://fullstackgtm.com)",
|
package/src/market.ts
CHANGED
|
@@ -94,6 +94,14 @@ export type MarketVendor = {
|
|
|
94
94
|
* obvious from the vendor's own pricing page (which the map captures).
|
|
95
95
|
*/
|
|
96
96
|
acvBand?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Optional brand logo for the report (legend + matrix headers). A `data:` URI
|
|
99
|
+
* keeps the rendered report self-contained — no external requests, survives
|
|
100
|
+
* being saved or emailed. The hosted service extracts it from the vendor's
|
|
101
|
+
* homepage; CLI users can set it by hand. Renderers degrade gracefully to the
|
|
102
|
+
* numbered swatch when absent.
|
|
103
|
+
*/
|
|
104
|
+
logo?: string;
|
|
97
105
|
notes?: string;
|
|
98
106
|
};
|
|
99
107
|
|
package/src/marketReport.ts
CHANGED
|
@@ -40,6 +40,17 @@ function escapeHtml(value: string): string {
|
|
|
40
40
|
.replace(/"/g, """);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* A small brand logo for legend rows / matrix headers. Accepts only `data:image/`
|
|
45
|
+
* URIs — self-contained (no external request, survives save/email) and safe under
|
|
46
|
+
* the report's `img-src data:` CSP (an SVG loaded via <img> can't execute script).
|
|
47
|
+
* Returns "" when absent so callers fall back to the numbered swatch / plain name.
|
|
48
|
+
*/
|
|
49
|
+
function logoImg(logo: string | undefined, cls: string): string {
|
|
50
|
+
if (typeof logo !== "string" || !logo.startsWith("data:image/")) return "";
|
|
51
|
+
return `<img class="${cls}" src="${escapeHtml(logo)}" alt="" loading="lazy">`;
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
/**
|
|
44
55
|
* Serialize JSON for embedding inside an inline <script> block. JSON.stringify
|
|
45
56
|
* does not escape `<`, `>`, `&`, or the U+2028/U+2029 line separators, so a
|
|
@@ -342,6 +353,7 @@ function axisSectionsHtml(
|
|
|
342
353
|
// The number inside each bubble resolves dense clusters that name labels
|
|
343
354
|
// never could; color is Okabe–Ito (colorblind-safe) keyed in the legend.
|
|
344
355
|
const points = pointsFor(px, py);
|
|
356
|
+
const logoByVendor = new Map(config.vendors.map((vendor) => [vendor.id, vendor.logo]));
|
|
345
357
|
const legendOrder = [...points].sort((a, b) => b.size - a.size || a.name.localeCompare(b.name));
|
|
346
358
|
const numberByVendor = new Map(legendOrder.map((point, index) => [point.vendorId, index + 1]));
|
|
347
359
|
const colorByVendor = new Map(legendOrder.map((point, index) => [point.vendorId, VENDOR_COLORS[index % VENDOR_COLORS.length]]));
|
|
@@ -356,7 +368,7 @@ function axisSectionsHtml(
|
|
|
356
368
|
? `${(share * 100).toFixed(1)}%`
|
|
357
369
|
: "—"
|
|
358
370
|
: `${loudCounts.get(point.vendorId) ?? 0} loud`;
|
|
359
|
-
return `<tr data-v="${e(point.vendorId)}"${isAnchor ? ' class="anchor-row"' : ""}><td><span class="swatch" style="background:${color};color:${numeralColor(color)}">${number}</span></td><td>${e(point.name)}${isAnchor ? " · anchor" : ""}</td><td class="num">${measure}</td></tr>`;
|
|
371
|
+
return `<tr data-v="${e(point.vendorId)}"${isAnchor ? ' class="anchor-row"' : ""}><td><span class="swatch" style="background:${color};color:${numeralColor(color)}">${number}</span></td><td>${logoImg(logoByVendor.get(point.vendorId), "v-logo")}${e(point.name)}${isAnchor ? " · anchor" : ""}</td><td class="num">${measure}</td></tr>`;
|
|
360
372
|
})
|
|
361
373
|
.join("");
|
|
362
374
|
const legendMeasureHead = useScale ? "est. share" : "loud";
|
|
@@ -488,7 +500,7 @@ export function marketMapToHtml(config: MarketConfig, set: ObservationSet): stri
|
|
|
488
500
|
const vendorHeads = config.vendors
|
|
489
501
|
.map(
|
|
490
502
|
(vendor) =>
|
|
491
|
-
`<th class="vh${vendor.id === anchor ? " anchor-col" : ""}"
|
|
503
|
+
`<th class="vh${vendor.id === anchor ? " anchor-col" : ""}">${logoImg(vendor.logo, "vh-logo")}<span>${e(vendor.name)}</span></th>`,
|
|
492
504
|
)
|
|
493
505
|
.join("");
|
|
494
506
|
|
|
@@ -632,6 +644,8 @@ tr.front-open th .claim-cap { color:var(--accent); font-weight:600; }
|
|
|
632
644
|
figure.map { margin-top:16px; border:1px solid var(--line); position:relative; }
|
|
633
645
|
g.bubble { cursor:pointer; }
|
|
634
646
|
g.bubble.dim { opacity:0.25; transition:opacity .12s; }
|
|
647
|
+
img.v-logo { width:15px; height:15px; border-radius:3px; object-fit:contain; vertical-align:-3px; margin-right:6px; background:#fff; }
|
|
648
|
+
th.vh img.vh-logo { display:block; width:18px; height:18px; border-radius:3px; object-fit:contain; margin:0 auto 4px; background:#fff; }
|
|
635
649
|
table.legend tbody tr { cursor:default; }
|
|
636
650
|
table.legend tbody tr.hl td { background:var(--faint); }
|
|
637
651
|
.map-tip { position:absolute; z-index:5; background:#1c1c1c; color:#fff; font-size:11.5px; line-height:1.45;
|