shipseal 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +595 -521
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import { cac } from "cac";
|
|
5
5
|
import { appendFile, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
6
6
|
import { z } from "zod";
|
|
@@ -241,6 +241,8 @@ const DEFAULT_CONFIG = {
|
|
|
241
241
|
},
|
|
242
242
|
milestones: {
|
|
243
243
|
stars: [
|
|
244
|
+
10,
|
|
245
|
+
50,
|
|
244
246
|
100,
|
|
245
247
|
250,
|
|
246
248
|
500,
|
|
@@ -409,8 +411,22 @@ function firstSentence(text) {
|
|
|
409
411
|
* sentences, and the fitting pass comfortably handles more.
|
|
410
412
|
*/
|
|
411
413
|
const HEADLINE_MAX_CHARS = 72;
|
|
414
|
+
/**
|
|
415
|
+
* Emoji ranges, removed from anything that reaches a card.
|
|
416
|
+
*
|
|
417
|
+
* No font we register carries these glyphs, so `\u{1F310} Human-friendly ...` (got's real
|
|
418
|
+
* GitHub description) rendered a tofu box on shipseal.dev. Takumi ships an emoji helper, but it
|
|
419
|
+
* rewrites each emoji into an <img> pointing at jsDelivr, and `generate()` does no network.
|
|
420
|
+
*
|
|
421
|
+
* The ranges deliberately start at U+2600, which keeps the textual symbols people put in real
|
|
422
|
+
* product names: (c) U+00A9, (r) U+00AE and (tm) U+2122 all sit below it.
|
|
423
|
+
*/
|
|
424
|
+
const EMOJI = /[\u{1F000}-\u{1FAFF}]|[\u{2600}-\u{27BF}]|[\u{FE00}-\u{FE0F}]|\u200D/gu;
|
|
425
|
+
function stripEmoji(text) {
|
|
426
|
+
return text.replaceAll(EMOJI, "").replace(/\s+/g, " ").trim();
|
|
427
|
+
}
|
|
412
428
|
function cleanLine(text) {
|
|
413
|
-
const stripped = text.replace(/\s*\u2014\s*/g, ": ").replace(/\s+\u2013\s+/g, ", ").replaceAll("–", "-").replaceAll("!", ".").replace(/\s+/g, " ").trim();
|
|
429
|
+
const stripped = stripEmoji(text).replace(/\s*\u2014\s*/g, ": ").replace(/\s+\u2013\s+/g, ", ").replaceAll("–", "-").replaceAll("!", ".").replace(/\s+/g, " ").trim();
|
|
414
430
|
const noTrail = stripped.endsWith(".") ? stripped.slice(0, -1) : stripped;
|
|
415
431
|
const first = noTrail.at(0);
|
|
416
432
|
if (first === void 0) return noTrail;
|
|
@@ -1818,9 +1834,9 @@ const storyPage = {
|
|
|
1818
1834
|
box: { width: 220 }
|
|
1819
1835
|
},
|
|
1820
1836
|
title: {
|
|
1821
|
-
maxLines: tall ?
|
|
1837
|
+
maxLines: tall ? 4 : 2,
|
|
1822
1838
|
maxFontSize: tall ? 64 : 48,
|
|
1823
|
-
minFontSize:
|
|
1839
|
+
minFontSize: tall ? 36 : 30,
|
|
1824
1840
|
step: 2,
|
|
1825
1841
|
box: {
|
|
1826
1842
|
width: width - 32,
|
|
@@ -1877,7 +1893,7 @@ const storyPage = {
|
|
|
1877
1893
|
render(raw, ctx) {
|
|
1878
1894
|
const props = propsSchema.parse(raw);
|
|
1879
1895
|
const colors = themeColors(ctx.brand, ctx.theme);
|
|
1880
|
-
const { tall,
|
|
1896
|
+
const { tall, bodyHeight, width } = geometry(ctx.format);
|
|
1881
1897
|
const editorial = ctx.brand.style === "editorial";
|
|
1882
1898
|
const terminal = ctx.brand.style === "terminal";
|
|
1883
1899
|
const code = isCommand(props.page);
|
|
@@ -1911,6 +1927,7 @@ const storyPage = {
|
|
|
1911
1927
|
justifyContent: "space-between",
|
|
1912
1928
|
alignItems: "center",
|
|
1913
1929
|
height: HEADER_HEIGHT,
|
|
1930
|
+
flexShrink: 0,
|
|
1914
1931
|
borderBottom: terminal ? `1px solid ${colors.muted}` : NO_BORDER,
|
|
1915
1932
|
paddingBottom: terminal ? 12 : 0
|
|
1916
1933
|
},
|
|
@@ -1943,83 +1960,91 @@ const storyPage = {
|
|
|
1943
1960
|
children: ctx.fitted.version?.text ?? props.version
|
|
1944
1961
|
})]
|
|
1945
1962
|
}),
|
|
1946
|
-
/* @__PURE__ */
|
|
1963
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1947
1964
|
style: {
|
|
1948
1965
|
display: "flex",
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1966
|
+
flexDirection: "column",
|
|
1967
|
+
flexGrow: 1,
|
|
1968
|
+
justifyContent: "center",
|
|
1969
|
+
gap: 24,
|
|
1970
|
+
minHeight: 0
|
|
1954
1971
|
},
|
|
1955
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
1972
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1956
1973
|
style: {
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1974
|
+
display: "flex",
|
|
1975
|
+
alignItems: "center",
|
|
1976
|
+
flexShrink: 0,
|
|
1977
|
+
paddingLeft: editorial ? 24 : 0,
|
|
1978
|
+
borderLeft: editorial ? `8px solid ${colors.primary}` : NO_BORDER
|
|
1962
1979
|
},
|
|
1963
|
-
children:
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1980
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1981
|
+
style: {
|
|
1982
|
+
fontFamily: props.heading,
|
|
1983
|
+
fontWeight: props.weight,
|
|
1984
|
+
fontSize: title?.fontSize ?? 48,
|
|
1985
|
+
lineHeight: 1.2,
|
|
1986
|
+
maxWidth: width - (editorial ? 32 : 0)
|
|
1987
|
+
},
|
|
1988
|
+
children: title?.text ?? props.page.title.value
|
|
1989
|
+
})
|
|
1990
|
+
}), comparison ? /* @__PURE__ */ jsx("div", {
|
|
1974
1991
|
style: {
|
|
1975
1992
|
display: "flex",
|
|
1976
|
-
flexDirection: "column",
|
|
1977
|
-
gap:
|
|
1978
|
-
|
|
1979
|
-
height: paneHeight
|
|
1993
|
+
flexDirection: tall ? "column" : "row",
|
|
1994
|
+
gap: 16,
|
|
1995
|
+
height: bodyHeight
|
|
1980
1996
|
},
|
|
1981
|
-
children: [/* @__PURE__ */
|
|
1997
|
+
children: ["before", "after"].map((label) => /* @__PURE__ */ jsxs("div", {
|
|
1982
1998
|
style: {
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1999
|
+
display: "flex",
|
|
2000
|
+
flexDirection: "column",
|
|
2001
|
+
gap: 8,
|
|
2002
|
+
width: paneWidth,
|
|
2003
|
+
height: paneHeight
|
|
1986
2004
|
},
|
|
1987
|
-
children:
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
backgroundColor: terminal || code ? colors.card : "transparent",
|
|
2003
|
-
borderRadius: terminal ? 0 : ctx.brand.radius,
|
|
2004
|
-
borderLeft: terminal ? `3px solid ${colors.primary}` : NO_BORDER
|
|
2005
|
-
},
|
|
2006
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
2005
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2006
|
+
style: {
|
|
2007
|
+
fontFamily: props.body,
|
|
2008
|
+
fontSize: 20,
|
|
2009
|
+
color: colors.muted
|
|
2010
|
+
},
|
|
2011
|
+
children: label === "before" ? "Before" : "After"
|
|
2012
|
+
}), /* @__PURE__ */ jsx("img", {
|
|
2013
|
+
src: `story-${label}`,
|
|
2014
|
+
width: paneWidth,
|
|
2015
|
+
height: paneHeight - HEADER_HEIGHT,
|
|
2016
|
+
style: { objectFit: "contain" }
|
|
2017
|
+
})]
|
|
2018
|
+
}, label))
|
|
2019
|
+
}) : hasBody ? /* @__PURE__ */ jsx("div", {
|
|
2007
2020
|
style: {
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2021
|
+
display: "flex",
|
|
2022
|
+
flexDirection: "column",
|
|
2023
|
+
justifyContent: "flex-start",
|
|
2024
|
+
maxHeight: bodyHeight,
|
|
2025
|
+
padding: terminal || code ? BODY_PAD : 0,
|
|
2026
|
+
backgroundColor: terminal || code ? colors.card : "transparent",
|
|
2027
|
+
borderRadius: terminal ? 0 : ctx.brand.radius,
|
|
2028
|
+
borderLeft: terminal ? `3px solid ${colors.primary}` : NO_BORDER
|
|
2014
2029
|
},
|
|
2015
|
-
children:
|
|
2016
|
-
|
|
2017
|
-
|
|
2030
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
2031
|
+
style: {
|
|
2032
|
+
fontFamily: code ? props.mono : props.body,
|
|
2033
|
+
fontWeight: 400,
|
|
2034
|
+
fontSize: bodyFontSize,
|
|
2035
|
+
lineHeight: code ? CODE_LINE_HEIGHT : 1.2,
|
|
2036
|
+
whiteSpace: code ? "pre" : "normal",
|
|
2037
|
+
color: colors.foreground
|
|
2038
|
+
},
|
|
2039
|
+
children: bodyText
|
|
2040
|
+
})
|
|
2041
|
+
}) : void 0]
|
|
2042
|
+
}),
|
|
2018
2043
|
/* @__PURE__ */ jsxs("div", {
|
|
2019
2044
|
style: {
|
|
2020
2045
|
display: "flex",
|
|
2021
2046
|
justifyContent: "space-between",
|
|
2022
|
-
|
|
2047
|
+
flexShrink: 0,
|
|
2023
2048
|
fontFamily: props.mono,
|
|
2024
2049
|
fontWeight: 400,
|
|
2025
2050
|
fontSize: 16,
|
|
@@ -2408,7 +2433,7 @@ async function collectReadme(cwd) {
|
|
|
2408
2433
|
ref: "README.md H1",
|
|
2409
2434
|
fetchedAt
|
|
2410
2435
|
});
|
|
2411
|
-
if (tagline !== void 0) project.tagline = fact(tagline, {
|
|
2436
|
+
if (tagline !== void 0) project.tagline = fact(stripEmoji(tagline), {
|
|
2412
2437
|
source: "readme",
|
|
2413
2438
|
ref: "README.md first paragraph",
|
|
2414
2439
|
fetchedAt
|
|
@@ -2499,6 +2524,14 @@ function stripBadges(text) {
|
|
|
2499
2524
|
return text.replace(/\[!\[[^\]]*]\([^)]+\)]\([^)]+\)/g, "").replace(/!\[[^\]]*]\([^)]+\)/g, "").replace(/<img[^>]*>/gi, "").replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
2500
2525
|
}
|
|
2501
2526
|
|
|
2527
|
+
//#endregion
|
|
2528
|
+
//#region src/sources/release-line.ts
|
|
2529
|
+
/** Markdown escapes a maintainer never meant to see rendered: \[ruff\] becomes [ruff]. */
|
|
2530
|
+
const MARKDOWN_ESCAPE = /\\([[\]()*_`~#+\-.!])/g;
|
|
2531
|
+
function cleanReleaseLine(item) {
|
|
2532
|
+
return item.replace(/^\s*[-*]\s*/, "").replace(/\[`[a-f0-9]{7,40}`]\([^)]+\)/gi, "").replace(/^[a-f0-9]{7,40}:\s*/i, "").replace(/\(#\d+\)/g, "").replace(/\[#\d+]\([^)]+\)/g, "").replace(/#\d+\b/g, "").replace(/Thanks\s+\[@[\w-]+]\([^)]+\)!?\s*-?\s*/gi, "").replace(/\[@[\w-]+]\([^)]+\)/g, "").replace(/\(@[\w-]+\)/g, "").replace(/\s+by\s+@[\w-]+/gi, "").replace(/@[\w-]+/g, "").replace(/^Thanks\s*!?\s*-?\s*/i, "").replaceAll(/!\[[^\]]*\]\([^)]*\)/g, "").replaceAll(/\[([^\]]+)\]\([^)]*\)/g, "$1").replaceAll(/<[^>]*>/g, "").replaceAll(MARKDOWN_ESCAPE, "$1").replaceAll(/[*`]/g, "").replace(/^\s*[-*]\s*/, "").replace(/\s*\(\s*\)/g, "").replace(/\s+([,.;:])/g, "$1").replace(/\s+/g, " ").trim().replace(/[\s,;:-]+$/, "").replace(/\.$/, "");
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2502
2535
|
//#endregion
|
|
2503
2536
|
//#region src/sources/changelog.ts
|
|
2504
2537
|
async function collectChangelog(cwd, version, changelogPath = "CHANGELOG.md") {
|
|
@@ -2600,7 +2633,7 @@ function findVersionSection(markdown, version) {
|
|
|
2600
2633
|
}
|
|
2601
2634
|
}
|
|
2602
2635
|
function cleanChangelogItem(item) {
|
|
2603
|
-
return item
|
|
2636
|
+
return cleanReleaseLine(item);
|
|
2604
2637
|
}
|
|
2605
2638
|
function headingIncludesVersion(heading, version) {
|
|
2606
2639
|
const unwrapped = heading.replace(/[[\]]/g, " ");
|
|
@@ -2887,7 +2920,7 @@ async function collectGithub(options) {
|
|
|
2887
2920
|
};
|
|
2888
2921
|
if (repo.description !== void 0 && repo.description !== null && repo.description.length > 0) out.project = {
|
|
2889
2922
|
...out.project,
|
|
2890
|
-
tagline: fact(repo.description, {
|
|
2923
|
+
tagline: fact(stripEmoji(repo.description), {
|
|
2891
2924
|
source: "github-api",
|
|
2892
2925
|
ref: `GET /repos/${slug.owner}/${slug.repo} description`,
|
|
2893
2926
|
fetchedAt
|
|
@@ -3083,7 +3116,7 @@ async function collectPackageJson(cwd, packagePath = "package.json") {
|
|
|
3083
3116
|
ref: rawName === void 0 ? `${packagePath} directory name` : `${packagePath}#name`,
|
|
3084
3117
|
fetchedAt
|
|
3085
3118
|
}) };
|
|
3086
|
-
if (pkg.data.description !== void 0 && pkg.data.description.length > 0) project.tagline = fact(pkg.data.description, {
|
|
3119
|
+
if (pkg.data.description !== void 0 && pkg.data.description.length > 0) project.tagline = fact(stripEmoji(pkg.data.description), {
|
|
3087
3120
|
source: "package-json",
|
|
3088
3121
|
ref: `${packagePath}#description`,
|
|
3089
3122
|
fetchedAt
|
|
@@ -4530,152 +4563,400 @@ function isRecord(value) {
|
|
|
4530
4563
|
}
|
|
4531
4564
|
|
|
4532
4565
|
//#endregion
|
|
4533
|
-
//#region src/brand/
|
|
4534
|
-
const
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
"
|
|
4538
|
-
"
|
|
4539
|
-
"
|
|
4540
|
-
"
|
|
4541
|
-
"
|
|
4542
|
-
"
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
const
|
|
4546
|
-
const
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
const
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4566
|
+
//#region src/brand/tailwind.ts
|
|
4567
|
+
const COLOR_PROP = /--color-([a-zA-Z0-9-]+)\s*:\s*([^;]+);/g;
|
|
4568
|
+
const CUSTOM_PROP = /--([a-zA-Z0-9-]+)\s*:\s*([^;]+);/g;
|
|
4569
|
+
const SKIP_PREFIXES = /* @__PURE__ */ new Set([
|
|
4570
|
+
"chart",
|
|
4571
|
+
"sidebar",
|
|
4572
|
+
"ring",
|
|
4573
|
+
"border",
|
|
4574
|
+
"input",
|
|
4575
|
+
"destructive"
|
|
4576
|
+
]);
|
|
4577
|
+
function extractTailwindV4Colors(css) {
|
|
4578
|
+
const blocks = themeBlocks(css);
|
|
4579
|
+
const custom = collectCustomProperties(css);
|
|
4580
|
+
const fromTheme = {};
|
|
4581
|
+
for (const block of blocks) {
|
|
4582
|
+
COLOR_PROP.lastIndex = 0;
|
|
4583
|
+
let match;
|
|
4584
|
+
while ((match = COLOR_PROP.exec(block)) !== null) {
|
|
4585
|
+
const name = match[1];
|
|
4586
|
+
const raw = match[2];
|
|
4587
|
+
if (name === void 0 || raw === void 0 || shouldSkip(name)) continue;
|
|
4588
|
+
fromTheme[name] = raw.trim();
|
|
4589
|
+
}
|
|
4557
4590
|
}
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4591
|
+
const resolved = {};
|
|
4592
|
+
for (const [name, raw] of Object.entries(fromTheme)) {
|
|
4593
|
+
const hex = resolveToHex(raw, custom, 0);
|
|
4594
|
+
if (hex !== void 0) resolved[name] = hex;
|
|
4561
4595
|
}
|
|
4596
|
+
return mapBrandColors(resolved);
|
|
4562
4597
|
}
|
|
4563
|
-
function
|
|
4564
|
-
const
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
const
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4598
|
+
function extractTailwindV3Colors(source) {
|
|
4599
|
+
const found = {};
|
|
4600
|
+
const re = /\b(primary|brand|accent|background|foreground|muted|secondary)\b\s*:\s*["'`]([^"'`]+)["'`]/g;
|
|
4601
|
+
let match;
|
|
4602
|
+
while ((match = re.exec(source)) !== null) {
|
|
4603
|
+
const name = match[1];
|
|
4604
|
+
const raw = match[2];
|
|
4605
|
+
if (name === void 0 || raw === void 0) continue;
|
|
4606
|
+
const hex = parseCssColor(raw);
|
|
4607
|
+
if (hex !== void 0) found[name] = hex;
|
|
4573
4608
|
}
|
|
4574
|
-
return
|
|
4609
|
+
return mapBrandColors(found);
|
|
4575
4610
|
}
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
26,
|
|
4587
|
-
10
|
|
4588
|
-
]);
|
|
4589
|
-
/** Channels per pixel for each PNG color type. Index is the color type. */
|
|
4590
|
-
const CHANNELS = {
|
|
4591
|
-
0: 1,
|
|
4592
|
-
2: 3,
|
|
4593
|
-
3: 1,
|
|
4594
|
-
4: 2,
|
|
4595
|
-
6: 4
|
|
4596
|
-
};
|
|
4597
|
-
function decodePng(buffer) {
|
|
4598
|
-
if (buffer.length < 8 || !buffer.subarray(0, 8).equals(SIGNATURE)) return;
|
|
4599
|
-
let width = 0;
|
|
4600
|
-
let height = 0;
|
|
4601
|
-
let bitDepth = 0;
|
|
4602
|
-
let colorType = 0;
|
|
4603
|
-
let interlace = 0;
|
|
4604
|
-
let palette;
|
|
4605
|
-
let paletteAlpha;
|
|
4606
|
-
const idat = [];
|
|
4607
|
-
let offset = 8;
|
|
4608
|
-
while (offset + 8 <= buffer.length) {
|
|
4609
|
-
const length = buffer.readUInt32BE(offset);
|
|
4610
|
-
const type = buffer.toString("ascii", offset + 4, offset + 8);
|
|
4611
|
-
const start = offset + 8;
|
|
4612
|
-
const end = start + length;
|
|
4613
|
-
if (end > buffer.length) return;
|
|
4614
|
-
if (type === "IHDR") {
|
|
4615
|
-
width = buffer.readUInt32BE(start);
|
|
4616
|
-
height = buffer.readUInt32BE(start + 4);
|
|
4617
|
-
bitDepth = buffer[start + 8] ?? 0;
|
|
4618
|
-
colorType = buffer[start + 9] ?? 0;
|
|
4619
|
-
interlace = buffer[start + 12] ?? 0;
|
|
4620
|
-
} else if (type === "PLTE") palette = buffer.subarray(start, end);
|
|
4621
|
-
else if (type === "tRNS") paletteAlpha = buffer.subarray(start, end);
|
|
4622
|
-
else if (type === "IDAT") idat.push(buffer.subarray(start, end));
|
|
4623
|
-
else if (type === "IEND") break;
|
|
4624
|
-
offset = end + 4;
|
|
4625
|
-
}
|
|
4626
|
-
const channels = CHANNELS[colorType];
|
|
4627
|
-
if (width === 0 || height === 0 || channels === void 0 || interlace !== 0 || bitDepth !== 8 && bitDepth !== 16 || idat.length === 0 || colorType === 3 && palette === void 0) return;
|
|
4628
|
-
if (width * height > 64e6) return;
|
|
4629
|
-
let raw;
|
|
4630
|
-
try {
|
|
4631
|
-
raw = inflateSync(Buffer.concat(idat));
|
|
4632
|
-
} catch {
|
|
4633
|
-
return;
|
|
4611
|
+
function themeBlocks(css) {
|
|
4612
|
+
const blocks = [];
|
|
4613
|
+
const re = /@theme(?:\s+inline)?\s*\{/g;
|
|
4614
|
+
let match;
|
|
4615
|
+
while ((match = re.exec(css)) !== null) {
|
|
4616
|
+
const open = css.indexOf("{", match.index);
|
|
4617
|
+
if (open === -1) break;
|
|
4618
|
+
const close = matchingBrace(css, open);
|
|
4619
|
+
if (close === -1) break;
|
|
4620
|
+
blocks.push(css.slice(open + 1, close));
|
|
4634
4621
|
}
|
|
4635
|
-
|
|
4636
|
-
const bpp = channels * bytesPerSample;
|
|
4637
|
-
const stride = width * bpp;
|
|
4638
|
-
if (raw.length < height * (stride + 1)) return;
|
|
4639
|
-
const unfiltered = unfilter(raw, height, stride, bpp);
|
|
4640
|
-
if (unfiltered === void 0) return;
|
|
4641
|
-
return {
|
|
4642
|
-
width,
|
|
4643
|
-
height,
|
|
4644
|
-
pixels: toRgba(unfiltered, width, height, colorType, bytesPerSample, palette, paletteAlpha)
|
|
4645
|
-
};
|
|
4622
|
+
return blocks;
|
|
4646
4623
|
}
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4624
|
+
function matchingBrace(source, openIndex) {
|
|
4625
|
+
let depth = 0;
|
|
4626
|
+
for (let i = openIndex; i < source.length; i += 1) {
|
|
4627
|
+
const ch = source[i];
|
|
4628
|
+
if (ch === "{") depth += 1;
|
|
4629
|
+
else if (ch === "}") {
|
|
4630
|
+
depth -= 1;
|
|
4631
|
+
if (depth === 0) return i;
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
return -1;
|
|
4635
|
+
}
|
|
4636
|
+
function collectCustomProperties(css) {
|
|
4637
|
+
const props = /* @__PURE__ */ new Map();
|
|
4638
|
+
CUSTOM_PROP.lastIndex = 0;
|
|
4639
|
+
let match;
|
|
4640
|
+
while ((match = CUSTOM_PROP.exec(css)) !== null) {
|
|
4641
|
+
const name = match[1];
|
|
4642
|
+
const value = match[2];
|
|
4643
|
+
if (name !== void 0 && value !== void 0) props.set(`--${name}`, value.trim());
|
|
4644
|
+
}
|
|
4645
|
+
return props;
|
|
4646
|
+
}
|
|
4647
|
+
function resolveToHex(raw, custom, depth) {
|
|
4648
|
+
if (depth > 3) return;
|
|
4649
|
+
const direct = parseCssColor(raw);
|
|
4650
|
+
if (direct !== void 0) return direct;
|
|
4651
|
+
const varMatch = /^var\(\s*(--[a-zA-Z0-9-]+)\s*(?:,[^)]+)?\)$/.exec(raw);
|
|
4652
|
+
if (varMatch === null) return;
|
|
4653
|
+
const ref = varMatch[1];
|
|
4654
|
+
if (ref === void 0) return;
|
|
4655
|
+
const next = custom.get(ref);
|
|
4656
|
+
if (next === void 0) return;
|
|
4657
|
+
return resolveToHex(next, custom, depth + 1);
|
|
4658
|
+
}
|
|
4659
|
+
function shouldSkip(name) {
|
|
4660
|
+
const root = name.split("-")[0];
|
|
4661
|
+
return root !== void 0 && SKIP_PREFIXES.has(root);
|
|
4662
|
+
}
|
|
4663
|
+
function mapBrandColors(found) {
|
|
4664
|
+
const out = {};
|
|
4665
|
+
const background = found.background ?? found.bg;
|
|
4666
|
+
if (background !== void 0) out.background = background;
|
|
4667
|
+
const foreground = found.foreground ?? found.fg;
|
|
4668
|
+
if (foreground !== void 0) out.foreground = foreground;
|
|
4669
|
+
const muted = found["muted-foreground"] ?? found.muted;
|
|
4670
|
+
if (muted !== void 0) out.muted = muted;
|
|
4671
|
+
const primary = found.primary ?? found.brand;
|
|
4672
|
+
if (primary !== void 0) out.primary = primary;
|
|
4673
|
+
const accent = found.accent ?? found.secondary;
|
|
4674
|
+
if (accent !== void 0) out.accent = accent;
|
|
4675
|
+
return out;
|
|
4676
|
+
}
|
|
4677
|
+
|
|
4678
|
+
//#endregion
|
|
4679
|
+
//#region src/brand/detect-colors.ts
|
|
4680
|
+
/** Path helpers, kept local so this module never imports `node:path`. */
|
|
4681
|
+
function basename$1(path) {
|
|
4682
|
+
return path.split("/").pop() ?? path;
|
|
4683
|
+
}
|
|
4684
|
+
function relative(from, path) {
|
|
4685
|
+
const prefix = from.endsWith("/") ? from : `${from}/`;
|
|
4686
|
+
return path.startsWith(prefix) ? path.slice(prefix.length) : path;
|
|
4687
|
+
}
|
|
4688
|
+
async function detectColorsFrom(files, cwd, logoPath, sources, notes, logoColor = () => Promise.resolve(void 0)) {
|
|
4689
|
+
const merged = {};
|
|
4690
|
+
const paths = await files.list();
|
|
4691
|
+
const cssFiles = paths.filter((file) => file.endsWith(".css"));
|
|
4692
|
+
const cssContents = await Promise.all(cssFiles.map(async (file) => ({
|
|
4693
|
+
file,
|
|
4694
|
+
css: await files.read(file)
|
|
4695
|
+
})));
|
|
4696
|
+
for (const { file, css } of cssContents) {
|
|
4697
|
+
if (css === void 0) continue;
|
|
4698
|
+
const fromTheme = extractTailwindV4Colors(css);
|
|
4699
|
+
const fromRoot = extractCssRootColors(css);
|
|
4700
|
+
const rel = relative(cwd, file);
|
|
4701
|
+
applyColors(merged, fromTheme, sources, `Tailwind v4 @theme in ${rel}`);
|
|
4702
|
+
applyColors(merged, fromRoot, sources, `:root variables in ${rel}`);
|
|
4703
|
+
}
|
|
4704
|
+
const configFiles = paths.filter((file) => basename$1(file).startsWith("tailwind.config."));
|
|
4705
|
+
const configContents = await Promise.all(configFiles.map(async (file) => ({
|
|
4706
|
+
file,
|
|
4707
|
+
source: await files.read(file)
|
|
4708
|
+
})));
|
|
4709
|
+
for (const { file, source } of configContents) {
|
|
4710
|
+
if (source === void 0) continue;
|
|
4711
|
+
applyColors(merged, extractTailwindV3Colors(source), sources, `Tailwind config ${relative(cwd, file)}`);
|
|
4712
|
+
}
|
|
4713
|
+
const tokenFile = paths.find((file) => file.endsWith(".tokens.json"));
|
|
4714
|
+
if (tokenFile !== void 0) {
|
|
4715
|
+
const raw = await files.read(tokenFile);
|
|
4716
|
+
if (raw !== void 0) try {
|
|
4717
|
+
const parsed = JSON.parse(raw);
|
|
4718
|
+
applyColors(merged, extractDtcgColors(parsed), sources, relative(cwd, tokenFile));
|
|
4719
|
+
} catch (error) {
|
|
4720
|
+
if (error instanceof SyntaxError) notes.push(`Could not parse design tokens file ${relative(cwd, tokenFile)}.`);
|
|
4721
|
+
else throw error;
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
if (merged.primary === void 0 && logoPath !== void 0) {
|
|
4725
|
+
const fromPng = logoPath.endsWith(".png") ? await logoColor(files, logoPath) : void 0;
|
|
4726
|
+
if (fromPng !== void 0) {
|
|
4727
|
+
merged.primary = fromPng;
|
|
4728
|
+
sources.push({
|
|
4729
|
+
field: "colors.primary",
|
|
4730
|
+
source: `dominant color in ${logoPath}`
|
|
4731
|
+
});
|
|
4732
|
+
}
|
|
4733
|
+
const svg = logoPath.endsWith(".svg") ? await files.read(logoPath) : void 0;
|
|
4734
|
+
if (svg !== void 0) {
|
|
4735
|
+
const fromLogo = firstNonNeutralSvgColor(svg);
|
|
4736
|
+
if (fromLogo !== void 0) {
|
|
4737
|
+
merged.primary = fromLogo;
|
|
4738
|
+
sources.push({
|
|
4739
|
+
field: "colors.primary",
|
|
4740
|
+
source: `SVG fill in ${logoPath}`
|
|
4741
|
+
});
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
const fellBack = [
|
|
4746
|
+
"background",
|
|
4747
|
+
"foreground",
|
|
4748
|
+
"muted",
|
|
4749
|
+
"primary",
|
|
4750
|
+
"accent"
|
|
4751
|
+
].filter((field) => merged[field] === void 0);
|
|
4752
|
+
if (fellBack.length > 0) notes.push(`Using built-in defaults for ${fellBack.map((f) => `colors.${f}`).join(", ")}. Nothing in this project set them. Edit .shipseal/brand.json to use your own.`);
|
|
4753
|
+
return {
|
|
4754
|
+
background: merged.background ?? DEFAULT_BRAND_COLORS.background,
|
|
4755
|
+
foreground: merged.foreground ?? DEFAULT_BRAND_COLORS.foreground,
|
|
4756
|
+
muted: merged.muted ?? DEFAULT_BRAND_COLORS.muted,
|
|
4757
|
+
primary: merged.primary ?? DEFAULT_BRAND_COLORS.primary,
|
|
4758
|
+
accent: merged.accent ?? DEFAULT_BRAND_COLORS.accent
|
|
4759
|
+
};
|
|
4760
|
+
}
|
|
4761
|
+
function applyColors(target, incoming, sources, source) {
|
|
4762
|
+
for (const key of [
|
|
4763
|
+
"background",
|
|
4764
|
+
"foreground",
|
|
4765
|
+
"muted",
|
|
4766
|
+
"primary",
|
|
4767
|
+
"accent"
|
|
4768
|
+
]) {
|
|
4769
|
+
const value = incoming[key];
|
|
4770
|
+
if (value !== void 0 && target[key] === void 0) {
|
|
4771
|
+
target[key] = value;
|
|
4772
|
+
sources.push({
|
|
4773
|
+
field: `colors.${key}`,
|
|
4774
|
+
source
|
|
4775
|
+
});
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
function applyReadableMuted(colors, sources, notes) {
|
|
4780
|
+
if (colors.muted === void 0 || readableOnBackground(colors.background, colors.muted)) return;
|
|
4781
|
+
colors.muted = DEFAULT_BRAND_COLORS.muted;
|
|
4782
|
+
notes.push(`Muted text color failed WCAG AA large-text contrast (3:1) against background, so it was set to ${DEFAULT_BRAND_COLORS.muted}.`);
|
|
4783
|
+
upsertSource(sources, "colors.muted", "contrast adjustment");
|
|
4784
|
+
}
|
|
4785
|
+
function applyReadableAccent(colors, sources, notes) {
|
|
4786
|
+
if (colors.accent === void 0 || readableOnBackground(colors.background, colors.accent)) return;
|
|
4787
|
+
colors.accent = colors.primary ?? DEFAULT_BRAND_COLORS.primary;
|
|
4788
|
+
notes.push("Accent failed contrast against background, so it uses the primary color.");
|
|
4789
|
+
upsertSource(sources, "colors.accent", "contrast adjustment");
|
|
4790
|
+
}
|
|
4791
|
+
function upsertSource(sources, field, source) {
|
|
4792
|
+
const existing = sources.find((entry) => entry.field === field);
|
|
4793
|
+
if (existing !== void 0) {
|
|
4794
|
+
existing.source = source;
|
|
4795
|
+
return;
|
|
4796
|
+
}
|
|
4797
|
+
sources.push({
|
|
4798
|
+
field,
|
|
4799
|
+
source
|
|
4800
|
+
});
|
|
4801
|
+
}
|
|
4802
|
+
function firstNonNeutralSvgColor(svg) {
|
|
4803
|
+
const re = /(?:fill|stroke)="([^"]+)"/g;
|
|
4804
|
+
let match;
|
|
4805
|
+
while ((match = re.exec(svg)) !== null) {
|
|
4806
|
+
const raw = match[1];
|
|
4807
|
+
if (raw === void 0 || raw === "none" || raw === "currentColor") continue;
|
|
4808
|
+
const hex = parseCssColor(raw);
|
|
4809
|
+
if (hex !== void 0 && !isNeutralHex(hex)) return hex;
|
|
4810
|
+
}
|
|
4811
|
+
}
|
|
4812
|
+
|
|
4813
|
+
//#endregion
|
|
4814
|
+
//#region src/brand/logo.ts
|
|
4815
|
+
const DIRECTORIES = [
|
|
4816
|
+
"",
|
|
4817
|
+
"assets",
|
|
4818
|
+
"assets/brand",
|
|
4819
|
+
"public",
|
|
4820
|
+
"public/brand",
|
|
4821
|
+
".github",
|
|
4822
|
+
"docs",
|
|
4823
|
+
"static",
|
|
4824
|
+
"branding"
|
|
4825
|
+
];
|
|
4826
|
+
const ICON_NAMES = ["icon.svg", "icon.png"];
|
|
4827
|
+
const NAMES = [
|
|
4828
|
+
"logo.svg",
|
|
4829
|
+
"logo.png",
|
|
4830
|
+
"logo-light.svg",
|
|
4831
|
+
"logo-light.png"
|
|
4832
|
+
];
|
|
4833
|
+
const FALLBACKS = [...ICON_NAMES, "favicon.svg"];
|
|
4834
|
+
function findLogo(cwd) {
|
|
4835
|
+
for (const dir of DIRECTORIES) for (const name of NAMES) {
|
|
4836
|
+
const relative = dir === "" ? name : join(dir, name);
|
|
4837
|
+
if (existsSync(join(cwd, relative))) return relative;
|
|
4838
|
+
}
|
|
4839
|
+
for (const dir of DIRECTORIES) for (const name of FALLBACKS) {
|
|
4840
|
+
const relative = dir === "" ? name : join(dir, name);
|
|
4841
|
+
if (existsSync(join(cwd, relative))) return relative;
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
function findLogoPair(cwd) {
|
|
4845
|
+
const light = findLogo(cwd);
|
|
4846
|
+
if (light === void 0) return;
|
|
4847
|
+
const darkNames = ["logo-dark.svg", "logo-dark.png"];
|
|
4848
|
+
for (const dir of DIRECTORIES) for (const name of darkNames) {
|
|
4849
|
+
const relative = dir === "" ? name : join(dir, name);
|
|
4850
|
+
if (existsSync(join(cwd, relative))) return {
|
|
4851
|
+
light,
|
|
4852
|
+
dark: relative
|
|
4853
|
+
};
|
|
4854
|
+
}
|
|
4855
|
+
return { light };
|
|
4856
|
+
}
|
|
4857
|
+
|
|
4858
|
+
//#endregion
|
|
4859
|
+
//#region src/brand/png.ts
|
|
4860
|
+
const SIGNATURE = Buffer.from([
|
|
4861
|
+
137,
|
|
4862
|
+
80,
|
|
4863
|
+
78,
|
|
4864
|
+
71,
|
|
4865
|
+
13,
|
|
4866
|
+
10,
|
|
4867
|
+
26,
|
|
4868
|
+
10
|
|
4869
|
+
]);
|
|
4870
|
+
/** Channels per pixel for each PNG color type. Index is the color type. */
|
|
4871
|
+
const CHANNELS = {
|
|
4872
|
+
0: 1,
|
|
4873
|
+
2: 3,
|
|
4874
|
+
3: 1,
|
|
4875
|
+
4: 2,
|
|
4876
|
+
6: 4
|
|
4877
|
+
};
|
|
4878
|
+
function decodePng(buffer) {
|
|
4879
|
+
if (buffer.length < 8 || !buffer.subarray(0, 8).equals(SIGNATURE)) return;
|
|
4880
|
+
let width = 0;
|
|
4881
|
+
let height = 0;
|
|
4882
|
+
let bitDepth = 0;
|
|
4883
|
+
let colorType = 0;
|
|
4884
|
+
let interlace = 0;
|
|
4885
|
+
let palette;
|
|
4886
|
+
let paletteAlpha;
|
|
4887
|
+
const idat = [];
|
|
4888
|
+
let offset = 8;
|
|
4889
|
+
while (offset + 8 <= buffer.length) {
|
|
4890
|
+
const length = buffer.readUInt32BE(offset);
|
|
4891
|
+
const type = buffer.toString("ascii", offset + 4, offset + 8);
|
|
4892
|
+
const start = offset + 8;
|
|
4893
|
+
const end = start + length;
|
|
4894
|
+
if (end > buffer.length) return;
|
|
4895
|
+
if (type === "IHDR") {
|
|
4896
|
+
width = buffer.readUInt32BE(start);
|
|
4897
|
+
height = buffer.readUInt32BE(start + 4);
|
|
4898
|
+
bitDepth = buffer[start + 8] ?? 0;
|
|
4899
|
+
colorType = buffer[start + 9] ?? 0;
|
|
4900
|
+
interlace = buffer[start + 12] ?? 0;
|
|
4901
|
+
} else if (type === "PLTE") palette = buffer.subarray(start, end);
|
|
4902
|
+
else if (type === "tRNS") paletteAlpha = buffer.subarray(start, end);
|
|
4903
|
+
else if (type === "IDAT") idat.push(buffer.subarray(start, end));
|
|
4904
|
+
else if (type === "IEND") break;
|
|
4905
|
+
offset = end + 4;
|
|
4906
|
+
}
|
|
4907
|
+
const channels = CHANNELS[colorType];
|
|
4908
|
+
if (width === 0 || height === 0 || channels === void 0 || interlace !== 0 || bitDepth !== 8 && bitDepth !== 16 || idat.length === 0 || colorType === 3 && palette === void 0) return;
|
|
4909
|
+
if (width * height > 64e6) return;
|
|
4910
|
+
let raw;
|
|
4911
|
+
try {
|
|
4912
|
+
raw = inflateSync(Buffer.concat(idat));
|
|
4913
|
+
} catch {
|
|
4914
|
+
return;
|
|
4915
|
+
}
|
|
4916
|
+
const bytesPerSample = bitDepth / 8;
|
|
4917
|
+
const bpp = channels * bytesPerSample;
|
|
4918
|
+
const stride = width * bpp;
|
|
4919
|
+
if (raw.length < height * (stride + 1)) return;
|
|
4920
|
+
const unfiltered = unfilter(raw, height, stride, bpp);
|
|
4921
|
+
if (unfiltered === void 0) return;
|
|
4922
|
+
return {
|
|
4923
|
+
width,
|
|
4924
|
+
height,
|
|
4925
|
+
pixels: toRgba(unfiltered, width, height, colorType, bytesPerSample, palette, paletteAlpha)
|
|
4926
|
+
};
|
|
4927
|
+
}
|
|
4928
|
+
/** Reverse the per-scanline filters defined in the PNG spec. */
|
|
4929
|
+
function unfilter(raw, height, stride, bpp) {
|
|
4930
|
+
const out = Buffer.alloc(height * stride);
|
|
4931
|
+
let pos = 0;
|
|
4932
|
+
for (let y = 0; y < height; y++) {
|
|
4933
|
+
const filter = raw[pos];
|
|
4934
|
+
pos += 1;
|
|
4935
|
+
if (filter === void 0 || filter > 4) return;
|
|
4936
|
+
const rowStart = y * stride;
|
|
4937
|
+
const prevStart = rowStart - stride;
|
|
4938
|
+
for (let x = 0; x < stride; x++) {
|
|
4939
|
+
const value = raw[pos + x] ?? 0;
|
|
4940
|
+
const a = x >= bpp ? out[rowStart + x - bpp] ?? 0 : 0;
|
|
4941
|
+
const b = y > 0 ? out[prevStart + x] ?? 0 : 0;
|
|
4942
|
+
const c = x >= bpp && y > 0 ? out[prevStart + x - bpp] ?? 0 : 0;
|
|
4943
|
+
let restored;
|
|
4944
|
+
switch (filter) {
|
|
4945
|
+
case 1:
|
|
4946
|
+
restored = value + a;
|
|
4947
|
+
break;
|
|
4948
|
+
case 2:
|
|
4949
|
+
restored = value + b;
|
|
4950
|
+
break;
|
|
4951
|
+
case 3:
|
|
4952
|
+
restored = value + Math.floor((a + b) / 2);
|
|
4953
|
+
break;
|
|
4954
|
+
case 4:
|
|
4955
|
+
restored = value + paeth(a, b, c);
|
|
4956
|
+
break;
|
|
4957
|
+
default: restored = value;
|
|
4958
|
+
}
|
|
4959
|
+
out[rowStart + x] = restored & 255;
|
|
4679
4960
|
}
|
|
4680
4961
|
pos += stride;
|
|
4681
4962
|
}
|
|
@@ -4764,141 +5045,22 @@ function dominantNonNeutralColor(png) {
|
|
|
4764
5045
|
bucket.count += 1;
|
|
4765
5046
|
bucket.r += r;
|
|
4766
5047
|
bucket.g += g;
|
|
4767
|
-
bucket.b += b;
|
|
4768
|
-
}
|
|
4769
|
-
}
|
|
4770
|
-
let best;
|
|
4771
|
-
for (const bucket of buckets.values()) if (best === void 0 || bucket.count > best.count) best = bucket;
|
|
4772
|
-
if (best === void 0) return;
|
|
4773
|
-
const channel = (total) => Math.round(total / best.count).toString(16).padStart(2, "0");
|
|
4774
|
-
return `#${channel(best.r)}${channel(best.g)}${channel(best.b)}`;
|
|
4775
|
-
}
|
|
4776
|
-
|
|
4777
|
-
//#endregion
|
|
4778
|
-
//#region src/brand/tailwind.ts
|
|
4779
|
-
const COLOR_PROP = /--color-([a-zA-Z0-9-]+)\s*:\s*([^;]+);/g;
|
|
4780
|
-
const CUSTOM_PROP = /--([a-zA-Z0-9-]+)\s*:\s*([^;]+);/g;
|
|
4781
|
-
const SKIP_PREFIXES = /* @__PURE__ */ new Set([
|
|
4782
|
-
"chart",
|
|
4783
|
-
"sidebar",
|
|
4784
|
-
"ring",
|
|
4785
|
-
"border",
|
|
4786
|
-
"input",
|
|
4787
|
-
"destructive"
|
|
4788
|
-
]);
|
|
4789
|
-
function extractTailwindV4Colors(css) {
|
|
4790
|
-
const blocks = themeBlocks(css);
|
|
4791
|
-
const custom = collectCustomProperties(css);
|
|
4792
|
-
const fromTheme = {};
|
|
4793
|
-
for (const block of blocks) {
|
|
4794
|
-
COLOR_PROP.lastIndex = 0;
|
|
4795
|
-
let match;
|
|
4796
|
-
while ((match = COLOR_PROP.exec(block)) !== null) {
|
|
4797
|
-
const name = match[1];
|
|
4798
|
-
const raw = match[2];
|
|
4799
|
-
if (name === void 0 || raw === void 0 || shouldSkip(name)) continue;
|
|
4800
|
-
fromTheme[name] = raw.trim();
|
|
4801
|
-
}
|
|
4802
|
-
}
|
|
4803
|
-
const resolved = {};
|
|
4804
|
-
for (const [name, raw] of Object.entries(fromTheme)) {
|
|
4805
|
-
const hex = resolveToHex(raw, custom, 0);
|
|
4806
|
-
if (hex !== void 0) resolved[name] = hex;
|
|
4807
|
-
}
|
|
4808
|
-
return mapBrandColors(resolved);
|
|
4809
|
-
}
|
|
4810
|
-
function extractTailwindV3Colors(source) {
|
|
4811
|
-
const found = {};
|
|
4812
|
-
const re = /\b(primary|brand|accent|background|foreground|muted|secondary)\b\s*:\s*["'`]([^"'`]+)["'`]/g;
|
|
4813
|
-
let match;
|
|
4814
|
-
while ((match = re.exec(source)) !== null) {
|
|
4815
|
-
const name = match[1];
|
|
4816
|
-
const raw = match[2];
|
|
4817
|
-
if (name === void 0 || raw === void 0) continue;
|
|
4818
|
-
const hex = parseCssColor(raw);
|
|
4819
|
-
if (hex !== void 0) found[name] = hex;
|
|
4820
|
-
}
|
|
4821
|
-
return mapBrandColors(found);
|
|
4822
|
-
}
|
|
4823
|
-
function themeBlocks(css) {
|
|
4824
|
-
const blocks = [];
|
|
4825
|
-
const re = /@theme(?:\s+inline)?\s*\{/g;
|
|
4826
|
-
let match;
|
|
4827
|
-
while ((match = re.exec(css)) !== null) {
|
|
4828
|
-
const open = css.indexOf("{", match.index);
|
|
4829
|
-
if (open === -1) break;
|
|
4830
|
-
const close = matchingBrace(css, open);
|
|
4831
|
-
if (close === -1) break;
|
|
4832
|
-
blocks.push(css.slice(open + 1, close));
|
|
4833
|
-
}
|
|
4834
|
-
return blocks;
|
|
4835
|
-
}
|
|
4836
|
-
function matchingBrace(source, openIndex) {
|
|
4837
|
-
let depth = 0;
|
|
4838
|
-
for (let i = openIndex; i < source.length; i += 1) {
|
|
4839
|
-
const ch = source[i];
|
|
4840
|
-
if (ch === "{") depth += 1;
|
|
4841
|
-
else if (ch === "}") {
|
|
4842
|
-
depth -= 1;
|
|
4843
|
-
if (depth === 0) return i;
|
|
4844
|
-
}
|
|
4845
|
-
}
|
|
4846
|
-
return -1;
|
|
4847
|
-
}
|
|
4848
|
-
function collectCustomProperties(css) {
|
|
4849
|
-
const props = /* @__PURE__ */ new Map();
|
|
4850
|
-
CUSTOM_PROP.lastIndex = 0;
|
|
4851
|
-
let match;
|
|
4852
|
-
while ((match = CUSTOM_PROP.exec(css)) !== null) {
|
|
4853
|
-
const name = match[1];
|
|
4854
|
-
const value = match[2];
|
|
4855
|
-
if (name !== void 0 && value !== void 0) props.set(`--${name}`, value.trim());
|
|
5048
|
+
bucket.b += b;
|
|
5049
|
+
}
|
|
4856
5050
|
}
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
if (direct !== void 0) return direct;
|
|
4863
|
-
const varMatch = /^var\(\s*(--[a-zA-Z0-9-]+)\s*(?:,[^)]+)?\)$/.exec(raw);
|
|
4864
|
-
if (varMatch === null) return;
|
|
4865
|
-
const ref = varMatch[1];
|
|
4866
|
-
if (ref === void 0) return;
|
|
4867
|
-
const next = custom.get(ref);
|
|
4868
|
-
if (next === void 0) return;
|
|
4869
|
-
return resolveToHex(next, custom, depth + 1);
|
|
4870
|
-
}
|
|
4871
|
-
function shouldSkip(name) {
|
|
4872
|
-
const root = name.split("-")[0];
|
|
4873
|
-
return root !== void 0 && SKIP_PREFIXES.has(root);
|
|
4874
|
-
}
|
|
4875
|
-
function mapBrandColors(found) {
|
|
4876
|
-
const out = {};
|
|
4877
|
-
const background = found.background ?? found.bg;
|
|
4878
|
-
if (background !== void 0) out.background = background;
|
|
4879
|
-
const foreground = found.foreground ?? found.fg;
|
|
4880
|
-
if (foreground !== void 0) out.foreground = foreground;
|
|
4881
|
-
const muted = found["muted-foreground"] ?? found.muted;
|
|
4882
|
-
if (muted !== void 0) out.muted = muted;
|
|
4883
|
-
const primary = found.primary ?? found.brand;
|
|
4884
|
-
if (primary !== void 0) out.primary = primary;
|
|
4885
|
-
const accent = found.accent ?? found.secondary;
|
|
4886
|
-
if (accent !== void 0) out.accent = accent;
|
|
4887
|
-
return out;
|
|
5051
|
+
let best;
|
|
5052
|
+
for (const bucket of buckets.values()) if (best === void 0 || bucket.count > best.count) best = bucket;
|
|
5053
|
+
if (best === void 0) return;
|
|
5054
|
+
const channel = (total) => Math.round(total / best.count).toString(16).padStart(2, "0");
|
|
5055
|
+
return `#${channel(best.r)}${channel(best.g)}${channel(best.b)}`;
|
|
4888
5056
|
}
|
|
4889
5057
|
|
|
4890
5058
|
//#endregion
|
|
4891
5059
|
//#region src/brand/detect.ts
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
"
|
|
4895
|
-
|
|
4896
|
-
"dist",
|
|
4897
|
-
"coverage",
|
|
4898
|
-
".shipseal",
|
|
4899
|
-
".next",
|
|
4900
|
-
"build"
|
|
4901
|
-
]);
|
|
5060
|
+
/** Path helpers, kept local so this module stays free of `node:path` and reaches the browser. */
|
|
5061
|
+
function basename(path) {
|
|
5062
|
+
return path.split("/").pop() ?? path;
|
|
5063
|
+
}
|
|
4902
5064
|
const packageSchema = z.object({
|
|
4903
5065
|
private: z.boolean().optional(),
|
|
4904
5066
|
name: z.string().optional(),
|
|
@@ -4906,20 +5068,24 @@ const packageSchema = z.object({
|
|
|
4906
5068
|
homepage: z.string().optional(),
|
|
4907
5069
|
repository: z.union([z.string(), z.object({ url: z.string().optional() })]).optional()
|
|
4908
5070
|
});
|
|
4909
|
-
|
|
5071
|
+
/**
|
|
5072
|
+
* Detection over any source. `root` is only used to make reported paths readable, so the
|
|
5073
|
+
* GitHub implementation can pass the repository slug.
|
|
5074
|
+
*/
|
|
5075
|
+
async function detectBrandFrom(files, cwd = ".") {
|
|
4910
5076
|
const sources = [];
|
|
4911
5077
|
const notes = [];
|
|
4912
|
-
const pkg = await readPackage(
|
|
4913
|
-
const readme = await
|
|
5078
|
+
const pkg = await readPackage(files);
|
|
5079
|
+
const readme = await files.read("README.md");
|
|
4914
5080
|
const name = detectName(pkg, readme, cwd, sources);
|
|
4915
5081
|
const tagline = detectTagline(pkg, readme, sources);
|
|
4916
|
-
const url = await detectUrl(pkg, cwd, sources);
|
|
5082
|
+
const url = await detectUrl(files, pkg, cwd, sources);
|
|
4917
5083
|
const logo = findLogoPair(cwd);
|
|
4918
5084
|
if (logo !== void 0) sources.push({
|
|
4919
5085
|
field: "logo",
|
|
4920
5086
|
source: logo.light
|
|
4921
5087
|
});
|
|
4922
|
-
const colors = await
|
|
5088
|
+
const colors = await detectColorsFrom(files, cwd, logo?.light, sources, notes, dominantLogoColor);
|
|
4923
5089
|
const contrast = ensureForegroundContrast(colors.background, colors.foreground);
|
|
4924
5090
|
if (contrast.adjusted) {
|
|
4925
5091
|
colors.foreground = contrast.foreground;
|
|
@@ -5009,7 +5175,7 @@ function detectTagline(pkg, readme, sources) {
|
|
|
5009
5175
|
}
|
|
5010
5176
|
}
|
|
5011
5177
|
}
|
|
5012
|
-
async function detectUrl(pkg, cwd, sources) {
|
|
5178
|
+
async function detectUrl(files, pkg, cwd, sources) {
|
|
5013
5179
|
if (pkg?.homepage !== void 0 && pkg.homepage.length > 0) {
|
|
5014
5180
|
sources.push({
|
|
5015
5181
|
field: "url",
|
|
@@ -5025,7 +5191,7 @@ async function detectUrl(pkg, cwd, sources) {
|
|
|
5025
5191
|
});
|
|
5026
5192
|
return repo;
|
|
5027
5193
|
}
|
|
5028
|
-
const remote = await gitRemote(
|
|
5194
|
+
const remote = await files.gitRemote?.();
|
|
5029
5195
|
if (remote !== void 0) {
|
|
5030
5196
|
sources.push({
|
|
5031
5197
|
field: "url",
|
|
@@ -5034,122 +5200,8 @@ async function detectUrl(pkg, cwd, sources) {
|
|
|
5034
5200
|
return remote;
|
|
5035
5201
|
}
|
|
5036
5202
|
}
|
|
5037
|
-
async function
|
|
5038
|
-
const
|
|
5039
|
-
const files = await listFiles(cwd, 4);
|
|
5040
|
-
const cssFiles = files.filter((file) => file.endsWith(".css"));
|
|
5041
|
-
const cssContents = await Promise.all(cssFiles.map(async (file) => ({
|
|
5042
|
-
file,
|
|
5043
|
-
css: await readMaybe(file)
|
|
5044
|
-
})));
|
|
5045
|
-
for (const { file, css } of cssContents) {
|
|
5046
|
-
if (css === void 0) continue;
|
|
5047
|
-
const fromTheme = extractTailwindV4Colors(css);
|
|
5048
|
-
const fromRoot = extractCssRootColors(css);
|
|
5049
|
-
const rel = relative(cwd, file);
|
|
5050
|
-
applyColors(merged, fromTheme, sources, `Tailwind v4 @theme in ${rel}`);
|
|
5051
|
-
applyColors(merged, fromRoot, sources, `:root variables in ${rel}`);
|
|
5052
|
-
}
|
|
5053
|
-
const configFiles = files.filter((file) => basename(file).startsWith("tailwind.config."));
|
|
5054
|
-
const configContents = await Promise.all(configFiles.map(async (file) => ({
|
|
5055
|
-
file,
|
|
5056
|
-
source: await readMaybe(file)
|
|
5057
|
-
})));
|
|
5058
|
-
for (const { file, source } of configContents) {
|
|
5059
|
-
if (source === void 0) continue;
|
|
5060
|
-
applyColors(merged, extractTailwindV3Colors(source), sources, `Tailwind config ${relative(cwd, file)}`);
|
|
5061
|
-
}
|
|
5062
|
-
const tokenFile = files.find((file) => file.endsWith(".tokens.json"));
|
|
5063
|
-
if (tokenFile !== void 0) {
|
|
5064
|
-
const raw = await readMaybe(tokenFile);
|
|
5065
|
-
if (raw !== void 0) try {
|
|
5066
|
-
const parsed = JSON.parse(raw);
|
|
5067
|
-
applyColors(merged, extractDtcgColors(parsed), sources, relative(cwd, tokenFile));
|
|
5068
|
-
} catch (error) {
|
|
5069
|
-
if (error instanceof SyntaxError) notes.push(`Could not parse design tokens file ${relative(cwd, tokenFile)}.`);
|
|
5070
|
-
else throw error;
|
|
5071
|
-
}
|
|
5072
|
-
}
|
|
5073
|
-
if (merged.primary === void 0 && logoPath !== void 0) {
|
|
5074
|
-
const fromPng = logoPath.endsWith(".png") ? await dominantLogoColor(join(cwd, logoPath)) : void 0;
|
|
5075
|
-
if (fromPng !== void 0) {
|
|
5076
|
-
merged.primary = fromPng;
|
|
5077
|
-
sources.push({
|
|
5078
|
-
field: "colors.primary",
|
|
5079
|
-
source: `dominant color in ${logoPath}`
|
|
5080
|
-
});
|
|
5081
|
-
}
|
|
5082
|
-
const svg = logoPath.endsWith(".svg") ? await readMaybe(join(cwd, logoPath)) : void 0;
|
|
5083
|
-
if (svg !== void 0) {
|
|
5084
|
-
const fromLogo = firstNonNeutralSvgColor(svg);
|
|
5085
|
-
if (fromLogo !== void 0) {
|
|
5086
|
-
merged.primary = fromLogo;
|
|
5087
|
-
sources.push({
|
|
5088
|
-
field: "colors.primary",
|
|
5089
|
-
source: `SVG fill in ${logoPath}`
|
|
5090
|
-
});
|
|
5091
|
-
}
|
|
5092
|
-
}
|
|
5093
|
-
}
|
|
5094
|
-
const fellBack = [
|
|
5095
|
-
"background",
|
|
5096
|
-
"foreground",
|
|
5097
|
-
"muted",
|
|
5098
|
-
"primary",
|
|
5099
|
-
"accent"
|
|
5100
|
-
].filter((field) => merged[field] === void 0);
|
|
5101
|
-
if (fellBack.length > 0) notes.push(`Using built-in defaults for ${fellBack.map((f) => `colors.${f}`).join(", ")}. Nothing in this project set them. Edit .shipseal/brand.json to use your own.`);
|
|
5102
|
-
return {
|
|
5103
|
-
background: merged.background ?? DEFAULT_BRAND_COLORS.background,
|
|
5104
|
-
foreground: merged.foreground ?? DEFAULT_BRAND_COLORS.foreground,
|
|
5105
|
-
muted: merged.muted ?? DEFAULT_BRAND_COLORS.muted,
|
|
5106
|
-
primary: merged.primary ?? DEFAULT_BRAND_COLORS.primary,
|
|
5107
|
-
accent: merged.accent ?? DEFAULT_BRAND_COLORS.accent
|
|
5108
|
-
};
|
|
5109
|
-
}
|
|
5110
|
-
function applyReadableMuted(colors, sources, notes) {
|
|
5111
|
-
if (colors.muted === void 0 || readableOnBackground(colors.background, colors.muted)) return;
|
|
5112
|
-
colors.muted = DEFAULT_BRAND_COLORS.muted;
|
|
5113
|
-
notes.push(`Muted text color failed WCAG AA large-text contrast (3:1) against background, so it was set to ${DEFAULT_BRAND_COLORS.muted}.`);
|
|
5114
|
-
upsertSource(sources, "colors.muted", "contrast adjustment");
|
|
5115
|
-
}
|
|
5116
|
-
function applyReadableAccent(colors, sources, notes) {
|
|
5117
|
-
if (colors.accent === void 0 || readableOnBackground(colors.background, colors.accent)) return;
|
|
5118
|
-
colors.accent = colors.primary ?? DEFAULT_BRAND_COLORS.primary;
|
|
5119
|
-
notes.push("Accent failed contrast against background, so it uses the primary color.");
|
|
5120
|
-
upsertSource(sources, "colors.accent", "contrast adjustment");
|
|
5121
|
-
}
|
|
5122
|
-
function applyColors(target, incoming, sources, source) {
|
|
5123
|
-
for (const key of [
|
|
5124
|
-
"background",
|
|
5125
|
-
"foreground",
|
|
5126
|
-
"muted",
|
|
5127
|
-
"primary",
|
|
5128
|
-
"accent"
|
|
5129
|
-
]) {
|
|
5130
|
-
const value = incoming[key];
|
|
5131
|
-
if (value !== void 0 && target[key] === void 0) {
|
|
5132
|
-
target[key] = value;
|
|
5133
|
-
sources.push({
|
|
5134
|
-
field: `colors.${key}`,
|
|
5135
|
-
source
|
|
5136
|
-
});
|
|
5137
|
-
}
|
|
5138
|
-
}
|
|
5139
|
-
}
|
|
5140
|
-
function upsertSource(sources, field, source) {
|
|
5141
|
-
const existing = sources.find((entry) => entry.field === field);
|
|
5142
|
-
if (existing !== void 0) {
|
|
5143
|
-
existing.source = source;
|
|
5144
|
-
return;
|
|
5145
|
-
}
|
|
5146
|
-
sources.push({
|
|
5147
|
-
field,
|
|
5148
|
-
source
|
|
5149
|
-
});
|
|
5150
|
-
}
|
|
5151
|
-
async function readPackage(cwd) {
|
|
5152
|
-
const raw = await readMaybe(join(cwd, "package.json"));
|
|
5203
|
+
async function readPackage(files) {
|
|
5204
|
+
const raw = await files.read("package.json");
|
|
5153
5205
|
if (raw === void 0) return;
|
|
5154
5206
|
try {
|
|
5155
5207
|
const parsed = JSON.parse(raw);
|
|
@@ -5160,18 +5212,69 @@ async function readPackage(cwd) {
|
|
|
5160
5212
|
throw error;
|
|
5161
5213
|
}
|
|
5162
5214
|
}
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5215
|
+
function stripScope(name) {
|
|
5216
|
+
const parts = name.split("/");
|
|
5217
|
+
return parts[parts.length - 1] ?? name;
|
|
5218
|
+
}
|
|
5219
|
+
function repositoryUrl(repository) {
|
|
5220
|
+
if (typeof repository === "string") return normalizeGitUrl(repository);
|
|
5221
|
+
if (repository?.url !== void 0) return normalizeGitUrl(repository.url);
|
|
5222
|
+
}
|
|
5223
|
+
function normalizeGitUrl(url) {
|
|
5224
|
+
const ssh = /^git@([^:]+):(.+)$/.exec(url);
|
|
5225
|
+
if (ssh !== null && ssh[1] !== void 0 && ssh[2] !== void 0) return `https://${ssh[1]}/${ssh[2].replace(/\.git$/, "")}`;
|
|
5226
|
+
return url.replace(/^git\+/, "").replace(/\.git$/, "");
|
|
5227
|
+
}
|
|
5228
|
+
/**
|
|
5229
|
+
* Dominant non-neutral color of a PNG logo, or undefined when the file cannot be read or
|
|
5230
|
+
* carries no color. Detection reports the color as not found rather than falling back to a
|
|
5231
|
+
* built-in default that would be presented to the user as "your brand".
|
|
5232
|
+
*/
|
|
5233
|
+
async function dominantLogoColor(files, path) {
|
|
5234
|
+
const buffer = await files.readBinary(path);
|
|
5235
|
+
if (buffer === void 0) return;
|
|
5236
|
+
const png = decodePng(Buffer.from(buffer));
|
|
5237
|
+
if (png === void 0) return;
|
|
5238
|
+
return dominantNonNeutralColor(png);
|
|
5239
|
+
}
|
|
5240
|
+
|
|
5241
|
+
//#endregion
|
|
5242
|
+
//#region src/brand/detect-node.ts
|
|
5243
|
+
const execFileAsync = promisify(execFile);
|
|
5244
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
5245
|
+
"node_modules",
|
|
5246
|
+
".git",
|
|
5247
|
+
"dist",
|
|
5248
|
+
"build",
|
|
5249
|
+
"coverage",
|
|
5250
|
+
".next",
|
|
5251
|
+
".astro",
|
|
5252
|
+
"out",
|
|
5253
|
+
"vendor"
|
|
5254
|
+
]);
|
|
5255
|
+
async function detectBrand(cwd) {
|
|
5256
|
+
return detectBrandFrom(diskFiles(cwd), cwd);
|
|
5257
|
+
}
|
|
5258
|
+
/** The disk implementation of the port, used by the CLI. Paths stay absolute here. */
|
|
5259
|
+
function diskFiles(cwd, maxDepth = 4) {
|
|
5260
|
+
return {
|
|
5261
|
+
list: () => listFiles(cwd, maxDepth),
|
|
5262
|
+
read: async (path) => {
|
|
5263
|
+
try {
|
|
5264
|
+
return await readFile(isAbsolute(path) ? path : join(cwd, path), "utf8");
|
|
5265
|
+
} catch {
|
|
5266
|
+
return;
|
|
5267
|
+
}
|
|
5268
|
+
},
|
|
5269
|
+
gitRemote: () => gitRemote(cwd),
|
|
5270
|
+
readBinary: async (path) => {
|
|
5271
|
+
try {
|
|
5272
|
+
return await readFile(isAbsolute(path) ? path : join(cwd, path));
|
|
5273
|
+
} catch {
|
|
5274
|
+
return;
|
|
5275
|
+
}
|
|
5276
|
+
}
|
|
5277
|
+
};
|
|
5175
5278
|
}
|
|
5176
5279
|
async function listFiles(root, maxDepth) {
|
|
5177
5280
|
const out = [];
|
|
@@ -5197,50 +5300,17 @@ async function listFiles(root, maxDepth) {
|
|
|
5197
5300
|
await walk(root, maxDepth);
|
|
5198
5301
|
return out;
|
|
5199
5302
|
}
|
|
5200
|
-
async function
|
|
5201
|
-
try {
|
|
5202
|
-
return await readFile(path, "utf8");
|
|
5203
|
-
} catch {
|
|
5204
|
-
return;
|
|
5205
|
-
}
|
|
5206
|
-
}
|
|
5207
|
-
function stripScope(name) {
|
|
5208
|
-
const parts = name.split("/");
|
|
5209
|
-
return parts[parts.length - 1] ?? name;
|
|
5210
|
-
}
|
|
5211
|
-
function repositoryUrl(repository) {
|
|
5212
|
-
if (typeof repository === "string") return normalizeGitUrl(repository);
|
|
5213
|
-
if (repository?.url !== void 0) return normalizeGitUrl(repository.url);
|
|
5214
|
-
}
|
|
5215
|
-
function normalizeGitUrl(url) {
|
|
5216
|
-
const ssh = /^git@([^:]+):(.+)$/.exec(url);
|
|
5217
|
-
if (ssh !== null && ssh[1] !== void 0 && ssh[2] !== void 0) return `https://${ssh[1]}/${ssh[2].replace(/\.git$/, "")}`;
|
|
5218
|
-
return url.replace(/^git\+/, "").replace(/\.git$/, "");
|
|
5219
|
-
}
|
|
5220
|
-
/**
|
|
5221
|
-
* Dominant non-neutral color of a PNG logo, or undefined when the file cannot be read or
|
|
5222
|
-
* carries no color. Detection reports the color as not found rather than falling back to a
|
|
5223
|
-
* built-in default that would be presented to the user as "your brand".
|
|
5224
|
-
*/
|
|
5225
|
-
async function dominantLogoColor(path) {
|
|
5226
|
-
let buffer;
|
|
5303
|
+
async function gitRemote(cwd) {
|
|
5227
5304
|
try {
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
const re = /(?:fill|stroke)="([^"]+)"/g;
|
|
5238
|
-
let match;
|
|
5239
|
-
while ((match = re.exec(svg)) !== null) {
|
|
5240
|
-
const raw = match[1];
|
|
5241
|
-
if (raw === void 0 || raw === "none" || raw === "currentColor") continue;
|
|
5242
|
-
const hex = parseCssColor(raw);
|
|
5243
|
-
if (hex !== void 0 && !isNeutralHex(hex)) return hex;
|
|
5305
|
+
const { stdout } = await execFileAsync("git", [
|
|
5306
|
+
"remote",
|
|
5307
|
+
"get-url",
|
|
5308
|
+
"origin"
|
|
5309
|
+
], { cwd });
|
|
5310
|
+
return normalizeGitUrl(stdout.trim());
|
|
5311
|
+
} catch (error) {
|
|
5312
|
+
if (error instanceof Error) return;
|
|
5313
|
+
throw error;
|
|
5244
5314
|
}
|
|
5245
5315
|
}
|
|
5246
5316
|
|
|
@@ -5596,10 +5666,10 @@ function overlayReleaseConfig(config, flags) {
|
|
|
5596
5666
|
//#region src/core/story.ts
|
|
5597
5667
|
const DEFAULT_CHANGE_PAGES = 4;
|
|
5598
5668
|
const STORY_FORMATS$1 = "portrait, square, og, github-social, x, linkedin, or producthunt";
|
|
5599
|
-
function storyFacts(facts, config,
|
|
5669
|
+
function storyFacts(facts, config, brand, generatedAt) {
|
|
5600
5670
|
const release = facts.release;
|
|
5601
5671
|
if (release === void 0) throw new ShipsealError("story.no-release", "No release facts are available for a story.", "Choose a published release or pass --tag to a local repository.");
|
|
5602
|
-
const copy = deterministicCopy(facts, config.release?.maxHighlights, name);
|
|
5672
|
+
const copy = deterministicCopy(facts, config.release?.maxHighlights, brand.name);
|
|
5603
5673
|
const configured = (value, ref) => fact(value, {
|
|
5604
5674
|
source: "user-config",
|
|
5605
5675
|
ref,
|
|
@@ -5614,7 +5684,11 @@ function storyFacts(facts, config, name, generatedAt) {
|
|
|
5614
5684
|
const pages = [{
|
|
5615
5685
|
kind: "cover",
|
|
5616
5686
|
title: fact(copy.headline, headlineSource.provenance),
|
|
5617
|
-
body: release.subheadline ?? facts.project.tagline ?? fact("Release notes", release.tag.provenance)
|
|
5687
|
+
body: release.subheadline ?? facts.project.tagline ?? (brand.tagline === void 0 ? fact("Release notes", release.tag.provenance) : fact(brand.tagline, {
|
|
5688
|
+
source: "user-config",
|
|
5689
|
+
ref: "brand.json#tagline",
|
|
5690
|
+
fetchedAt: generatedAt
|
|
5691
|
+
}))
|
|
5618
5692
|
}];
|
|
5619
5693
|
for (const item of changes.slice(0, config.release?.maxHighlights ?? DEFAULT_CHANGE_PAGES)) {
|
|
5620
5694
|
const collapsed = item.value.replaceAll(/\s+/g, " ").trim();
|
|
@@ -5657,7 +5731,7 @@ function storyFacts(facts, config, name, generatedAt) {
|
|
|
5657
5731
|
};
|
|
5658
5732
|
}
|
|
5659
5733
|
async function generateStory(input) {
|
|
5660
|
-
const facts = storyFacts(input.facts, input.config, input.brand
|
|
5734
|
+
const facts = storyFacts(input.facts, input.config, input.brand, input.generatedAt);
|
|
5661
5735
|
const pages = facts.story ?? [];
|
|
5662
5736
|
const result = {
|
|
5663
5737
|
files: [],
|