design-pact 0.2.1 → 0.3.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 +17 -1
- package/SKILL.md +17 -5
- package/dist/cli.js +712 -33
- package/package.json +2 -3
- package/web/404.html +1 -1
- package/web/__next.__PAGE__.txt +4 -4
- package/web/__next._full.txt +12 -12
- package/web/__next._head.txt +4 -4
- package/web/__next._index.txt +4 -4
- package/web/__next._tree.txt +2 -2
- package/web/_next/static/chunks/06~f4cvc3b8e~.js +5 -0
- package/web/_next/static/chunks/0dbhjjzl8qfwv.js +1 -0
- package/web/_next/static/chunks/0ge~kyq7lzl~5.js +187 -0
- package/web/_next/static/chunks/0ht900cau6_ur.js +31 -0
- package/web/_next/static/chunks/0nn9jq1cov9ax.css +3 -0
- package/web/_next/static/chunks/0pe3f11zk1rcv.js +1 -0
- package/web/_next/static/chunks/{turbopack-0y3s-y83i3.06.js → turbopack-0jm8qs4dffmjl.js} +1 -1
- package/web/_not-found/__next._full.txt +10 -10
- package/web/_not-found/__next._head.txt +4 -4
- package/web/_not-found/__next._index.txt +4 -4
- package/web/_not-found/__next._not-found.__PAGE__.txt +2 -2
- package/web/_not-found/__next._not-found.txt +3 -3
- package/web/_not-found/__next._tree.txt +2 -2
- package/web/_not-found.html +1 -1
- package/web/_not-found.txt +10 -10
- package/web/index.html +1 -1
- package/web/index.txt +12 -12
- package/web/_next/static/chunks/0-l9p_pd2v836.js +0 -5
- package/web/_next/static/chunks/012lv2-viu5_..js +0 -187
- package/web/_next/static/chunks/0rp7qr3afex.u.js +0 -31
- package/web/_next/static/chunks/0uyeui9y_zv_0.css +0 -3
- package/web/_next/static/chunks/0x72peuimhbw-.js +0 -1
- package/web/_next/static/chunks/15356_01bt_3u.js +0 -1
- /package/web/_next/static/{PuB4TNuzsXn-CMEz1oKNJ → tIX_HvPOULd9j1yucL1Pb}/_buildManifest.js +0 -0
- /package/web/_next/static/{PuB4TNuzsXn-CMEz1oKNJ → tIX_HvPOULd9j1yucL1Pb}/_clientMiddlewareManifest.js +0 -0
- /package/web/_next/static/{PuB4TNuzsXn-CMEz1oKNJ → tIX_HvPOULd9j1yucL1Pb}/_ssgManifest.js +0 -0
package/dist/cli.js
CHANGED
|
@@ -890,21 +890,21 @@ var require_react_development = __commonJS({
|
|
|
890
890
|
);
|
|
891
891
|
actScopeDepth = prevActScopeDepth;
|
|
892
892
|
}
|
|
893
|
-
function recursivelyFlushAsyncActWork(returnValue,
|
|
893
|
+
function recursivelyFlushAsyncActWork(returnValue, resolve4, reject) {
|
|
894
894
|
var queue = ReactSharedInternals.actQueue;
|
|
895
895
|
if (null !== queue)
|
|
896
896
|
if (0 !== queue.length)
|
|
897
897
|
try {
|
|
898
898
|
flushActQueue(queue);
|
|
899
899
|
enqueueTask(function() {
|
|
900
|
-
return recursivelyFlushAsyncActWork(returnValue,
|
|
900
|
+
return recursivelyFlushAsyncActWork(returnValue, resolve4, reject);
|
|
901
901
|
});
|
|
902
902
|
return;
|
|
903
903
|
} catch (error) {
|
|
904
904
|
ReactSharedInternals.thrownErrors.push(error);
|
|
905
905
|
}
|
|
906
906
|
else ReactSharedInternals.actQueue = null;
|
|
907
|
-
0 < ReactSharedInternals.thrownErrors.length ? (queue = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(queue)) :
|
|
907
|
+
0 < ReactSharedInternals.thrownErrors.length ? (queue = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(queue)) : resolve4(returnValue);
|
|
908
908
|
}
|
|
909
909
|
function flushActQueue(queue) {
|
|
910
910
|
if (!isFlushing) {
|
|
@@ -1091,7 +1091,7 @@ var require_react_development = __commonJS({
|
|
|
1091
1091
|
));
|
|
1092
1092
|
});
|
|
1093
1093
|
return {
|
|
1094
|
-
then: function(
|
|
1094
|
+
then: function(resolve4, reject) {
|
|
1095
1095
|
didAwaitActCall = true;
|
|
1096
1096
|
thenable.then(
|
|
1097
1097
|
function(returnValue) {
|
|
@@ -1101,7 +1101,7 @@ var require_react_development = __commonJS({
|
|
|
1101
1101
|
flushActQueue(queue), enqueueTask(function() {
|
|
1102
1102
|
return recursivelyFlushAsyncActWork(
|
|
1103
1103
|
returnValue,
|
|
1104
|
-
|
|
1104
|
+
resolve4,
|
|
1105
1105
|
reject
|
|
1106
1106
|
);
|
|
1107
1107
|
});
|
|
@@ -1115,7 +1115,7 @@ var require_react_development = __commonJS({
|
|
|
1115
1115
|
ReactSharedInternals.thrownErrors.length = 0;
|
|
1116
1116
|
reject(_thrownError);
|
|
1117
1117
|
}
|
|
1118
|
-
} else
|
|
1118
|
+
} else resolve4(returnValue);
|
|
1119
1119
|
},
|
|
1120
1120
|
function(error) {
|
|
1121
1121
|
popActScope(prevActQueue, prevActScopeDepth);
|
|
@@ -1137,15 +1137,15 @@ var require_react_development = __commonJS({
|
|
|
1137
1137
|
if (0 < ReactSharedInternals.thrownErrors.length)
|
|
1138
1138
|
throw callback = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, callback;
|
|
1139
1139
|
return {
|
|
1140
|
-
then: function(
|
|
1140
|
+
then: function(resolve4, reject) {
|
|
1141
1141
|
didAwaitActCall = true;
|
|
1142
1142
|
0 === prevActScopeDepth ? (ReactSharedInternals.actQueue = queue, enqueueTask(function() {
|
|
1143
1143
|
return recursivelyFlushAsyncActWork(
|
|
1144
1144
|
returnValue$jscomp$0,
|
|
1145
|
-
|
|
1145
|
+
resolve4,
|
|
1146
1146
|
reject
|
|
1147
1147
|
);
|
|
1148
|
-
})) :
|
|
1148
|
+
})) : resolve4(returnValue$jscomp$0);
|
|
1149
1149
|
}
|
|
1150
1150
|
};
|
|
1151
1151
|
};
|
|
@@ -1451,8 +1451,8 @@ var require_react = __commonJS({
|
|
|
1451
1451
|
});
|
|
1452
1452
|
|
|
1453
1453
|
// src/cli.ts
|
|
1454
|
-
import { readFileSync as
|
|
1455
|
-
import { join as join3, resolve as
|
|
1454
|
+
import { readFileSync as readFileSync3, writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
|
|
1455
|
+
import { join as join3, resolve as resolve3 } from "path";
|
|
1456
1456
|
|
|
1457
1457
|
// src/locale.ts
|
|
1458
1458
|
var envLang = (process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || "").toLowerCase();
|
|
@@ -1468,11 +1468,11 @@ function fence(md, lang) {
|
|
|
1468
1468
|
return m ? m[1] : null;
|
|
1469
1469
|
}
|
|
1470
1470
|
function parseDesignSystem(md) {
|
|
1471
|
-
if (!/^---\s*\ndesign-
|
|
1471
|
+
if (!/^---\s*\ndesign-pact:/m.test(md) && !md.includes("# Design system")) {
|
|
1472
1472
|
throw new Error(
|
|
1473
1473
|
t(
|
|
1474
|
-
"This does not look like a design.md (missing the design-
|
|
1475
|
-
"\u8FD9\u770B\u8D77\u6765\u4E0D\u662F design.md\uFF08\u7F3A\u5C11 design-
|
|
1474
|
+
"This does not look like a design.md (missing the design-pact frontmatter / Design system heading).",
|
|
1475
|
+
"\u8FD9\u770B\u8D77\u6765\u4E0D\u662F design.md\uFF08\u7F3A\u5C11 design-pact frontmatter / Design system \u6807\u9898\uFF09\u3002"
|
|
1476
1476
|
)
|
|
1477
1477
|
);
|
|
1478
1478
|
}
|
|
@@ -1497,6 +1497,33 @@ function parseDesignSystem(md) {
|
|
|
1497
1497
|
import { converter, formatHex, parse, formatCss } from "culori";
|
|
1498
1498
|
var toOklch = converter("oklch");
|
|
1499
1499
|
var toRgb = converter("rgb");
|
|
1500
|
+
function hexToOklch(hex) {
|
|
1501
|
+
const c = toOklch(parse(hex));
|
|
1502
|
+
if (!c) return { mode: "oklch", l: 0, c: 0, h: 0 };
|
|
1503
|
+
return { ...c, h: c.h ?? 0 };
|
|
1504
|
+
}
|
|
1505
|
+
function oklchToHex(c) {
|
|
1506
|
+
const rgb = toRgb(c);
|
|
1507
|
+
if (!rgb) return "#000000";
|
|
1508
|
+
const clamp2 = (v) => Math.max(0, Math.min(1, v));
|
|
1509
|
+
return formatHex({ ...rgb, r: clamp2(rgb.r), g: clamp2(rgb.g), b: clamp2(rgb.b) }) || "#000000";
|
|
1510
|
+
}
|
|
1511
|
+
function relativeLuminance(hex) {
|
|
1512
|
+
const rgb = toRgb(parse(hex));
|
|
1513
|
+
if (!rgb) return 0;
|
|
1514
|
+
const lin = (v) => v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
1515
|
+
return 0.2126 * lin(rgb.r) + 0.7152 * lin(rgb.g) + 0.0722 * lin(rgb.b);
|
|
1516
|
+
}
|
|
1517
|
+
function contrastRatio(a, b) {
|
|
1518
|
+
const la = relativeLuminance(a);
|
|
1519
|
+
const lb = relativeLuminance(b);
|
|
1520
|
+
const [hi, lo] = la > lb ? [la, lb] : [lb, la];
|
|
1521
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
1522
|
+
}
|
|
1523
|
+
function oklchString(hex) {
|
|
1524
|
+
const c = hexToOklch(hex);
|
|
1525
|
+
return formatCss(c) || "";
|
|
1526
|
+
}
|
|
1500
1527
|
|
|
1501
1528
|
// ../../lib/scales.ts
|
|
1502
1529
|
var SPACING_STEPS = [
|
|
@@ -1596,6 +1623,7 @@ function buildOpacityScale(base) {
|
|
|
1596
1623
|
}
|
|
1597
1624
|
|
|
1598
1625
|
// ../../lib/tokens-core.ts
|
|
1626
|
+
var SEMANTIC_KINDS = ["success", "warning", "error", "info"];
|
|
1599
1627
|
var defaultTypography = {
|
|
1600
1628
|
base: 16,
|
|
1601
1629
|
ratio: 1.25,
|
|
@@ -1635,6 +1663,95 @@ function buildScale(t2) {
|
|
|
1635
1663
|
}
|
|
1636
1664
|
|
|
1637
1665
|
// ../../lib/export.ts
|
|
1666
|
+
var OPACITY_USAGE = {
|
|
1667
|
+
hover: "tint/overlay on hover (e.g. button or row background)",
|
|
1668
|
+
pressed: "active/pressed feedback",
|
|
1669
|
+
focus: "focus ring or focused-row background",
|
|
1670
|
+
disabled: "opacity of disabled controls",
|
|
1671
|
+
overlay: "modal/drawer backdrop scrim"
|
|
1672
|
+
};
|
|
1673
|
+
function w3cTokens(colors, typography, spacing, radius, shadow, motion, border, opacity, darkColors, semantic, darkSemantic) {
|
|
1674
|
+
const darkById = new Map((darkColors ?? []).map((c) => [c.id, c.displayHex]));
|
|
1675
|
+
const colorGroup = {};
|
|
1676
|
+
for (const c of colors) {
|
|
1677
|
+
const key = c.role === "unassigned" ? c.id : c.role;
|
|
1678
|
+
const ext = {
|
|
1679
|
+
proportion: c.proportion,
|
|
1680
|
+
oklch: oklchString(c.displayHex)
|
|
1681
|
+
};
|
|
1682
|
+
const dark = darkById.get(c.id);
|
|
1683
|
+
if (dark) ext.dark = dark;
|
|
1684
|
+
colorGroup[key] = {
|
|
1685
|
+
$value: c.displayHex,
|
|
1686
|
+
$type: "color",
|
|
1687
|
+
$extensions: { "design-pact": ext }
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
const fontSizes = {};
|
|
1691
|
+
for (const s of buildScale(typography)) {
|
|
1692
|
+
fontSizes[s.name] = { $value: `${s.rem}rem`, $type: "dimension" };
|
|
1693
|
+
}
|
|
1694
|
+
const spacingGroup = {};
|
|
1695
|
+
for (const s of buildSpacing(spacing.base)) {
|
|
1696
|
+
spacingGroup[s.name] = { $value: `${s.px}px`, $type: "dimension" };
|
|
1697
|
+
}
|
|
1698
|
+
const radiusGroup = {};
|
|
1699
|
+
for (const r of buildRadius(radius.base)) {
|
|
1700
|
+
radiusGroup[r.name] = { $value: `${r.px}px`, $type: "dimension" };
|
|
1701
|
+
}
|
|
1702
|
+
const shadowGroup = {};
|
|
1703
|
+
for (const level of ["sm", "md", "lg"]) {
|
|
1704
|
+
shadowGroup[level] = { $value: shadowToCss(shadow[level]), $type: "shadow" };
|
|
1705
|
+
}
|
|
1706
|
+
const motionGroup = {};
|
|
1707
|
+
for (const d of buildDurations(motion.base)) {
|
|
1708
|
+
motionGroup[`duration-${d.name}`] = { $value: `${d.ms}ms`, $type: "duration" };
|
|
1709
|
+
}
|
|
1710
|
+
motionGroup["easing"] = { $value: EASING_PRESETS[motion.easing], $type: "cubicBezier" };
|
|
1711
|
+
const borderGroup = {};
|
|
1712
|
+
for (const b of buildBorderScale(border.base)) {
|
|
1713
|
+
borderGroup[b.name] = { $value: `${b.px}px`, $type: "dimension" };
|
|
1714
|
+
}
|
|
1715
|
+
const opacityGroup = {};
|
|
1716
|
+
for (const o of buildOpacityScale(opacity.base)) {
|
|
1717
|
+
opacityGroup[o.name] = { $value: o.value, $type: "number" };
|
|
1718
|
+
}
|
|
1719
|
+
const semanticGroup = semantic ? Object.fromEntries(
|
|
1720
|
+
SEMANTIC_KINDS.map((k) => [
|
|
1721
|
+
k,
|
|
1722
|
+
{
|
|
1723
|
+
$value: semantic[k],
|
|
1724
|
+
$type: "color",
|
|
1725
|
+
...darkSemantic ? { $extensions: { "design-pact": { dark: darkSemantic[k] } } } : {}
|
|
1726
|
+
}
|
|
1727
|
+
])
|
|
1728
|
+
) : void 0;
|
|
1729
|
+
return {
|
|
1730
|
+
color: colorGroup,
|
|
1731
|
+
...semanticGroup ? { semantic: semanticGroup } : {},
|
|
1732
|
+
typography: {
|
|
1733
|
+
fontFamily: {
|
|
1734
|
+
body: { $value: typography.fontFamily, $type: "fontFamily" },
|
|
1735
|
+
heading: { $value: typography.headingFamily, $type: "fontFamily" }
|
|
1736
|
+
},
|
|
1737
|
+
fontSize: fontSizes,
|
|
1738
|
+
fontWeight: { $value: typography.fontWeight, $type: "fontWeight" },
|
|
1739
|
+
fontWeightHeading: { $value: headingWeight(typography.fontWeight), $type: "fontWeight" },
|
|
1740
|
+
fontWeightBold: { $value: boldWeight(typography.fontWeight), $type: "fontWeight" },
|
|
1741
|
+
lineHeight: { $value: typography.lineHeight, $type: "number" },
|
|
1742
|
+
letterSpacing: { $value: `${typography.letterSpacing}em`, $type: "dimension" },
|
|
1743
|
+
$extensions: {
|
|
1744
|
+
"design-pact": { base: typography.base, ratio: typography.ratio }
|
|
1745
|
+
}
|
|
1746
|
+
},
|
|
1747
|
+
spacing: spacingGroup,
|
|
1748
|
+
borderRadius: radiusGroup,
|
|
1749
|
+
borderWidth: borderGroup,
|
|
1750
|
+
shadow: shadowGroup,
|
|
1751
|
+
motion: motionGroup,
|
|
1752
|
+
opacity: opacityGroup
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1638
1755
|
function tailwindConfig(colors, typography, spacing, radius, shadow, motion, border, opacity) {
|
|
1639
1756
|
const colorEntries = colors.map((c) => ` ${c.role === "unassigned" ? c.id : c.role}: "${c.displayHex}",`).join("\n");
|
|
1640
1757
|
const sizeEntries = buildScale(typography).map((s) => ` "${s.name}": "${s.rem}rem",`).join("\n");
|
|
@@ -1694,6 +1811,205 @@ ${opacityEntries}
|
|
|
1694
1811
|
};
|
|
1695
1812
|
`;
|
|
1696
1813
|
}
|
|
1814
|
+
function cssVars(colors, typography, spacing, radius, shadow, motion, border, opacity, darkColors, semantic, darkSemantic) {
|
|
1815
|
+
const colorLines = colors.map((c) => ` --color-${c.role === "unassigned" ? c.id : c.role}: ${c.displayHex};`).join("\n");
|
|
1816
|
+
const semanticLines = semantic ? "\n" + SEMANTIC_KINDS.map((k) => ` --color-${k}: ${semantic[k]};`).join("\n") : "";
|
|
1817
|
+
const sizeLines = buildScale(typography).map((s) => ` --font-size-${s.name}: ${s.rem}rem;`).join("\n");
|
|
1818
|
+
const spacingLines = buildSpacing(spacing.base).map((s) => ` --spacing-${s.name}: ${s.px}px;`).join("\n");
|
|
1819
|
+
const radiusLines = buildRadius(radius.base).map((r) => ` --radius-${r.name}: ${r.px}px;`).join("\n");
|
|
1820
|
+
const shadowLines = ["sm", "md", "lg"].map((level) => ` --shadow-${level}: ${shadowToCss(shadow[level])};`).join("\n");
|
|
1821
|
+
const durationLines = buildDurations(motion.base).map((d) => ` --duration-${d.name}: ${d.ms}ms;`).join("\n");
|
|
1822
|
+
const borderLines = buildBorderScale(border.base).map((b) => ` --border-${b.name}: ${b.px}px;`).join("\n");
|
|
1823
|
+
const opacityLines = buildOpacityScale(opacity.base).map((o) => ` --opacity-${o.name}: ${o.value};`).join("\n");
|
|
1824
|
+
const darkSemanticLines = darkColors && darkColors.length > 0 && darkSemantic ? "\n" + SEMANTIC_KINDS.map((k) => ` --color-${k}: ${darkSemantic[k]};`).join("\n") : "";
|
|
1825
|
+
const darkBlock = darkColors && darkColors.length > 0 ? `
|
|
1826
|
+
@media (prefers-color-scheme: dark) {
|
|
1827
|
+
:root {
|
|
1828
|
+
${darkColors.map((c) => ` --color-${c.role === "unassigned" ? c.id : c.role}: ${c.displayHex};`).join("\n")}${darkSemanticLines}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
` : "";
|
|
1832
|
+
return `:root {
|
|
1833
|
+
${colorLines}${semanticLines}
|
|
1834
|
+
--font-family-body: ${typography.fontFamily};
|
|
1835
|
+
--font-family-heading: ${typography.headingFamily};
|
|
1836
|
+
--font-weight: ${typography.fontWeight};
|
|
1837
|
+
--font-weight-heading: ${headingWeight(typography.fontWeight)};
|
|
1838
|
+
--font-weight-bold: ${boldWeight(typography.fontWeight)};
|
|
1839
|
+
--line-height: ${typography.lineHeight};
|
|
1840
|
+
--letter-spacing: ${typography.letterSpacing}em;
|
|
1841
|
+
${sizeLines}
|
|
1842
|
+
${spacingLines}
|
|
1843
|
+
${radiusLines}
|
|
1844
|
+
${borderLines}
|
|
1845
|
+
${shadowLines}
|
|
1846
|
+
${durationLines}
|
|
1847
|
+
--easing-standard: ${EASING_PRESETS[motion.easing]};
|
|
1848
|
+
${opacityLines}
|
|
1849
|
+
}
|
|
1850
|
+
${darkBlock}`;
|
|
1851
|
+
}
|
|
1852
|
+
function aiPrompt(colors, typography, spacing, radius, shadow, motion, border, opacity, darkColors, semantic, darkSemantic) {
|
|
1853
|
+
const sorted = [...colors].sort((a, b) => b.proportion - a.proportion);
|
|
1854
|
+
const palette = sorted.map(
|
|
1855
|
+
(c) => `- ${c.role === "unassigned" ? c.id : c.role}: ${c.displayHex} (${(c.proportion * 100).toFixed(1)}% of source image)`
|
|
1856
|
+
).join("\n");
|
|
1857
|
+
const scale = buildScale(typography).map((s) => `- ${s.name}: ${s.rem}rem (${s.px}px)`).join("\n");
|
|
1858
|
+
const dominant = sorted[0];
|
|
1859
|
+
const accent = sorted.find((c) => c.role === "accent") ?? sorted[1];
|
|
1860
|
+
const primary = sorted.find((c) => c.role === "primary") ?? sorted[1];
|
|
1861
|
+
const rootBlock = cssVars(colors, typography, spacing, radius, shadow, motion, border, opacity, darkColors, semantic, darkSemantic);
|
|
1862
|
+
const semanticSection = semantic ? `
|
|
1863
|
+
## Status colors
|
|
1864
|
+
Use these for feedback only \u2014 never as brand/UI surface colors. \`--color-success\` ${semantic.success} (confirmations), \`--color-warning\` ${semantic.warning} (cautions), \`--color-error\` ${semantic.error} (errors/destructive), \`--color-info\` ${semantic.info} (informational). Reference via \`var(--color-\u2026)\`.${darkSemantic ? " In dark mode the `@media` block above overrides these with the dark-background variants \u2014 same convention as the brand colors." : ""}
|
|
1865
|
+
` : "";
|
|
1866
|
+
const darkSection = darkColors && darkColors.length > 0 ? `
|
|
1867
|
+
## Dark mode
|
|
1868
|
+
The \`@media (prefers-color-scheme: dark)\` block above overrides COLOR variables only \u2014 every other token (type, spacing, radius, shadow, motion) stays identical in dark mode. Do not hand-pick dark colors or adjust non-color tokens for dark mode; the block is authoritative.
|
|
1869
|
+
` : "";
|
|
1870
|
+
return `# Design system
|
|
1871
|
+
|
|
1872
|
+
## Machine-readable tokens \u2014 COPY THIS BLOCK VERBATIM
|
|
1873
|
+
Paste the \`:root\` below into your \`<style>\` exactly as written, then reference
|
|
1874
|
+
every value through \`var(--\u2026)\`. This block is the single source of truth.
|
|
1875
|
+
|
|
1876
|
+
Hard rules:
|
|
1877
|
+
- Do NOT redefine, round, rescale, or re-derive any value. Copy it character for character.
|
|
1878
|
+
- Do NOT introduce colors, fonts, font-sizes, spacing, radii, or shadows that are not declared here.
|
|
1879
|
+
- If you need a lighter/darker shade of a color, derive it in OKLCH by changing lightness only \u2014 keep hue and chroma fixed. Do not invent a new hue.
|
|
1880
|
+
- Every length in your CSS must be a \`var(--\u2026)\` reference. Do NOT convert a \`px\` token to \`rem\` (or any other unit) \u2014 reference it as-is. Spacing, radius, and border vars are in \`px\`; font sizes are in \`rem\`; keep each as declared.
|
|
1881
|
+
|
|
1882
|
+
\`\`\`css
|
|
1883
|
+
${rootBlock}\`\`\`
|
|
1884
|
+
|
|
1885
|
+
The prose below explains the intent behind these tokens. When prose and the \`:root\` block disagree, the block wins.
|
|
1886
|
+
|
|
1887
|
+
---
|
|
1888
|
+
|
|
1889
|
+
When generating UI from this token set, respect the following proportions and roles. The percentages reflect how much of the source design each color occupies \u2014 use them as a guide for surface area in the output.
|
|
1890
|
+
|
|
1891
|
+
## Color palette
|
|
1892
|
+
${palette}
|
|
1893
|
+
|
|
1894
|
+
The dominant background should occupy roughly ${(dominant.proportion * 100).toFixed(0)}% of the layout.
|
|
1895
|
+
|
|
1896
|
+
Bind colors to roles exactly as the design tool renders them \u2014 do NOT swap primary and accent:
|
|
1897
|
+
|
|
1898
|
+
- **primary (${primary.displayHex})** is the main interactive fill: primary buttons / CTAs, active nav item, key links, the logo mark. This is the button color.
|
|
1899
|
+
- **accent (${accent.displayHex})** is for SECONDARY emphasis only: badges/chips, chart & graphic accents, small highlights \u2014 typically ${Math.min(15, Math.round(accent.proportion * 100))}% or less of any screen. Do not use accent as the primary button fill.
|
|
1900
|
+
- **muted** for secondary/placeholder text, **border** for hairlines and dividers.
|
|
1901
|
+
|
|
1902
|
+
## Typography
|
|
1903
|
+
Base size ${typography.base}px, modular scale ratio ${typography.ratio}.
|
|
1904
|
+
|
|
1905
|
+
${scale}
|
|
1906
|
+
|
|
1907
|
+
Body family: ${typography.fontFamily}
|
|
1908
|
+
Heading family: ${typography.headingFamily}
|
|
1909
|
+
Line-height: ${typography.lineHeight}, letter-spacing: ${typography.letterSpacing}em. There are three explicit font weights \u2014 bind them by role, and never let the browser default an element's weight:
|
|
1910
|
+
|
|
1911
|
+
- **\`--font-weight\` (${typography.fontWeight})** \u2192 body text, labels, and any non-heading copy.
|
|
1912
|
+
- **\`--font-weight-heading\` (${headingWeight(typography.fontWeight)})** \u2192 all headings h1\u2013h5. Set this on every heading; do NOT leave headings at the body weight.
|
|
1913
|
+
- **\`--font-weight-bold\` (${boldWeight(typography.fontWeight)})** \u2192 emphasis only: primary CTA labels, \`<strong>\`, badges.
|
|
1914
|
+
|
|
1915
|
+
## Spacing
|
|
1916
|
+
Base unit ${spacing.base}px. Use this 8-step scale for padding/margin/gap:
|
|
1917
|
+
|
|
1918
|
+
${buildSpacing(spacing.base).map((s) => `- ${s.name}: ${s.px}px`).join("\n")}
|
|
1919
|
+
|
|
1920
|
+
Bind padding & gaps to these steps exactly as the design tool renders them \u2014
|
|
1921
|
+
padding is written as \`vertical horizontal\`:
|
|
1922
|
+
|
|
1923
|
+
- **Primary / prominent CTA button** \u2192 \`--spacing-sm\` \`--spacing-lg\` (e.g. \`padding: var(--spacing-sm) var(--spacing-lg)\`) \u2014 a taller, comfortable target.
|
|
1924
|
+
- **Compact buttons / inputs / chips** \u2192 \`--spacing-xxs\` \`--spacing-sm\`.
|
|
1925
|
+
- **List rows / nav items / card header & footer** \u2192 \`--spacing-xs\` \`--spacing-md\`.
|
|
1926
|
+
- **Card / panel / modal body** \u2192 \`--spacing-md\` on all sides.
|
|
1927
|
+
- **Gap between sibling controls, grid/flex gaps** \u2192 \`--spacing-sm\`.
|
|
1928
|
+
- **Vertical rhythm between page sections** \u2192 \`--spacing-xl\` \u2026 \`--spacing-section\`.
|
|
1929
|
+
|
|
1930
|
+
Only use listed values; never improvise an intermediate gap.
|
|
1931
|
+
|
|
1932
|
+
## Border radius
|
|
1933
|
+
Base ${radius.base}px. Use:
|
|
1934
|
+
|
|
1935
|
+
${buildRadius(radius.base).map((r) => `- ${r.name}: ${r.name === "full" ? "9999px (pill)" : `${r.px}px`}`).join("\n")}
|
|
1936
|
+
|
|
1937
|
+
Bind components to these steps exactly as the design tool renders them \u2014 do NOT
|
|
1938
|
+
pick a step freely (e.g. do not make buttons pill-shaped unless \`--radius-md\`
|
|
1939
|
+
resolves to a pill):
|
|
1940
|
+
|
|
1941
|
+
- **Buttons / controls / nav items** \u2192 \`--radius-md\`
|
|
1942
|
+
- **Inputs / text fields / selects** \u2192 \`--radius-sm\`
|
|
1943
|
+
- **Cards / panels / modals / larger surfaces** \u2192 \`--radius-lg\`
|
|
1944
|
+
- **Badges / chips / avatars / toggles (intentionally pill)** \u2192 \`--radius-full\`
|
|
1945
|
+
- Bigger containers may use \`--radius-xl\`; \`--radius-full\` is reserved for
|
|
1946
|
+
circles/pills, never for standard buttons or cards.
|
|
1947
|
+
|
|
1948
|
+
## Shadow / elevation
|
|
1949
|
+
${shadow.advanced ? "Per-level custom values" : `Intensity ${shadow.intensity.toFixed(2)}`}.
|
|
1950
|
+
|
|
1951
|
+
${["sm", "md", "lg"].map((level) => `- ${level}: ${shadowToCss(shadow[level])}`).join("\n")}
|
|
1952
|
+
|
|
1953
|
+
Bind each level to an elevation exactly as the design tool renders them \u2014 do NOT
|
|
1954
|
+
pick a level freely:
|
|
1955
|
+
|
|
1956
|
+
- **sm** \u2192 resting surfaces: cards, panels, subtle raise (the default for a raised element).
|
|
1957
|
+
- **md** \u2192 hovered / lifted state: a card or button on hover, raised popovers.
|
|
1958
|
+
- **lg** \u2192 floating overlays: modals, drawers, dropdowns, menus above the page.
|
|
1959
|
+
|
|
1960
|
+
Flat elements (nav bar, page background, inline controls) get NO shadow. On hover, step a card up one level (sm \u2192 md), don't jump straight to lg.
|
|
1961
|
+
|
|
1962
|
+
## Border width
|
|
1963
|
+
${buildBorderScale(border.base).map((b) => `- ${b.name}: ${b.px}px`).join("\n")}
|
|
1964
|
+
|
|
1965
|
+
Bind each width as the design tool renders them:
|
|
1966
|
+
|
|
1967
|
+
- **default** \u2192 all resting borders: card outlines, input borders, dividers.
|
|
1968
|
+
- **strong** \u2192 focused / active / selected emphasis (e.g. an input grows from \`default\` to \`strong\` on focus). Do not use \`strong\` for ordinary resting borders.
|
|
1969
|
+
|
|
1970
|
+
## Opacity / transparency
|
|
1971
|
+
These drive interactive states \u2014 wire them up, don't leave them unused. Apply each via \`var(--opacity-\u2026)\` (or an rgba/overlay at that alpha):
|
|
1972
|
+
|
|
1973
|
+
${buildOpacityScale(opacity.base).map((o) => `- ${o.name} (${(o.value * 100).toFixed(1)}%): ${OPACITY_USAGE[o.name] ?? "interactive state"}`).join("\n")}
|
|
1974
|
+
|
|
1975
|
+
Every clickable element (buttons, links, cards) should show a hover state and a disabled state using these values. Use \`overlay\` for scrims behind modals/drawers.
|
|
1976
|
+
|
|
1977
|
+
## Motion / Animation
|
|
1978
|
+
Base duration ${motion.base}ms, easing: ${motion.easing} (${EASING_PRESETS[motion.easing]}).
|
|
1979
|
+
|
|
1980
|
+
${buildDurations(motion.base).map((d) => `- ${d.name}: ${d.ms}ms`).join("\n")}
|
|
1981
|
+
|
|
1982
|
+
Use \`--duration-normal\` as the default transition. Prefer \`--duration-fast\` for hover states and micro-interactions. Reserve \`--duration-page\` for route/page-level transitions.
|
|
1983
|
+
${darkSection}${semanticSection}
|
|
1984
|
+
## Output guidance
|
|
1985
|
+
- Maintain the proportional relationships above when laying out a page.
|
|
1986
|
+
- Do not introduce new hues. If you need additional shades, derive them by adjusting OKLCH lightness only, keeping hue and chroma fixed.
|
|
1987
|
+
- Respect the modular scale \u2014 pick from the listed sizes rather than introducing new ones.
|
|
1988
|
+
- Use only the listed spacing values; do not improvise intermediate gaps.
|
|
1989
|
+
- ${SCALE_STEPS.length} type sizes is intentional. Use semantic mapping: h1-h5 for headings, body for paragraphs, small/caption for metadata.
|
|
1990
|
+
`;
|
|
1991
|
+
}
|
|
1992
|
+
function designSystemMarkdown(colors, typography, spacing, radius, shadow, motion, border, opacity, darkColors, semantic, darkSemantic) {
|
|
1993
|
+
const prompt = aiPrompt(colors, typography, spacing, radius, shadow, motion, border, opacity, darkColors, semantic, darkSemantic);
|
|
1994
|
+
const tokens = w3cTokens(colors, typography, spacing, radius, shadow, motion, border, opacity, darkColors, semantic, darkSemantic);
|
|
1995
|
+
const generated = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1996
|
+
return `---
|
|
1997
|
+
design-pact: 1
|
|
1998
|
+
generated: ${generated}
|
|
1999
|
+
---
|
|
2000
|
+
|
|
2001
|
+
${prompt}
|
|
2002
|
+
---
|
|
2003
|
+
|
|
2004
|
+
## Machine-readable tokens (W3C Design Tokens)
|
|
2005
|
+
|
|
2006
|
+
Humans and AI can read the prose above. The JSON below lets tools (the companion CLI) reconstruct the design system precisely and deterministically, and convert it to CSS / Tailwind / etc.
|
|
2007
|
+
|
|
2008
|
+
\`\`\`json
|
|
2009
|
+
${JSON.stringify(tokens, null, 2)}
|
|
2010
|
+
\`\`\`
|
|
2011
|
+
`;
|
|
2012
|
+
}
|
|
1697
2013
|
|
|
1698
2014
|
// ../../node_modules/zustand/esm/vanilla.mjs
|
|
1699
2015
|
var createStoreImpl = (createState) => {
|
|
@@ -1949,7 +2265,7 @@ var useLang = create()(
|
|
|
1949
2265
|
// skipHydration: the static export prerenders with the "en" default, then
|
|
1950
2266
|
// StoreHydration rehydrates from localStorage on the client (same pattern
|
|
1951
2267
|
// as the tokens store) so there is no hydration mismatch.
|
|
1952
|
-
{ name: "design-
|
|
2268
|
+
{ name: "design-pact-lang", skipHydration: true }
|
|
1953
2269
|
)
|
|
1954
2270
|
);
|
|
1955
2271
|
function trg(en, zh) {
|
|
@@ -2000,7 +2316,7 @@ function parseW3CTokens(jsonText) {
|
|
|
2000
2316
|
role: ROLES.has(key) ? key : "unassigned",
|
|
2001
2317
|
name: key
|
|
2002
2318
|
});
|
|
2003
|
-
const dark = asObj(asObj(asObj(node)?.["$extensions"])?.["design-
|
|
2319
|
+
const dark = asObj(asObj(asObj(node)?.["$extensions"])?.["design-pact"])?.dark;
|
|
2004
2320
|
darkHexes.push(typeof dark === "string" && HEX_RE.test(dark) ? dark.toLowerCase() : void 0);
|
|
2005
2321
|
}
|
|
2006
2322
|
if (colors.length === 0) throw new Error(trg("No recognizable colors in the color group", "color \u5206\u7EC4\u91CC\u6CA1\u6709\u53EF\u8BC6\u522B\u7684\u989C\u8272"));
|
|
@@ -2009,7 +2325,7 @@ function parseW3CTokens(jsonText) {
|
|
|
2009
2325
|
const typo = asObj(root.typography);
|
|
2010
2326
|
if (typo) {
|
|
2011
2327
|
const t2 = {};
|
|
2012
|
-
const ext = asObj(asObj(typo.$extensions)?.["design-
|
|
2328
|
+
const ext = asObj(asObj(typo.$extensions)?.["design-pact"]);
|
|
2013
2329
|
const base = num(ext?.base);
|
|
2014
2330
|
const ratio = num(ext?.ratio);
|
|
2015
2331
|
if (base) t2.base = base;
|
|
@@ -2117,18 +2433,18 @@ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
2117
2433
|
async function cmdInit(args) {
|
|
2118
2434
|
const global = args.includes("--global");
|
|
2119
2435
|
const base = global ? join(homedir(), ".claude") : join(process.cwd(), ".claude");
|
|
2120
|
-
const dir = join(base, "skills", "design-
|
|
2436
|
+
const dir = join(base, "skills", "design-pact");
|
|
2121
2437
|
if (!existsSync(SKILL_SRC)) {
|
|
2122
2438
|
console.error("\u2717 " + t("SKILL.md missing from the package (run bundle before publishing).", "\u5305\u5185\u7F3A\u5C11 SKILL.md\uFF08\u53D1\u5E03\u524D\u9700\u5148\u8FD0\u884C bundle\uFF09\u3002"));
|
|
2123
2439
|
process.exit(1);
|
|
2124
2440
|
}
|
|
2125
2441
|
await mkdir(dir, { recursive: true });
|
|
2126
2442
|
await copyFile(SKILL_SRC, join(dir, "SKILL.md"));
|
|
2127
|
-
console.log(t(`\u2713 Installed the design-
|
|
2443
|
+
console.log(t(`\u2713 Installed the design-pact skill \u2192 ${join(dir, "SKILL.md")}`, `\u2713 \u5DF2\u5B89\u88C5 design-pact skill \u2192 ${join(dir, "SKILL.md")}`));
|
|
2128
2444
|
console.log(
|
|
2129
2445
|
global ? t(" (global: available in every project)", " \uFF08\u5168\u5C40\uFF1A\u5BF9\u6240\u6709\u9879\u76EE\u53EF\u7528\uFF09") : t(" (project-level: this project only; add --global to install into ~/.claude)", " \uFF08\u9879\u76EE\u7EA7\uFF1A\u4EC5\u5F53\u524D\u9879\u76EE\u53EF\u7528\uFF0C\u52A0 --global \u53EF\u88C5\u5230 ~/.claude\uFF09")
|
|
2130
2446
|
);
|
|
2131
|
-
console.log("\n" + t('Next: in Claude Code / Cursor, say "use the design-
|
|
2447
|
+
console.log("\n" + t('Next: in Claude Code / Cursor, say "use the design-pact skill".', "\u4E0B\u4E00\u6B65\uFF1A\u5728 Claude Code / Cursor \u91CC\u8BF4\u300C\u7528 design-pact skill\u300D\u3002"));
|
|
2132
2448
|
console.log(t("It clarifies direction, proposes palettes, and opens the local studio via `npx design-pact open`.", "\u5B83\u4F1A\u95EE\u6E05\u65B9\u5411\u3001\u4EA7\u51FA\u914D\u8272\uFF0C\u5E76\u7528 `npx design-pact open` \u5728\u672C\u5730\u6253\u5F00\u914D\u8272\u5DE5\u5177\u3002"));
|
|
2133
2449
|
}
|
|
2134
2450
|
function fileForUrl(urlPath) {
|
|
@@ -2151,21 +2467,21 @@ function serveStatic(port) {
|
|
|
2151
2467
|
res.writeHead(200, { "content-type": MIME[extname(file)] || "application/octet-stream" });
|
|
2152
2468
|
createReadStream(file).pipe(res);
|
|
2153
2469
|
});
|
|
2154
|
-
return new Promise((
|
|
2470
|
+
return new Promise((resolve4, reject) => {
|
|
2155
2471
|
server.once("error", reject);
|
|
2156
|
-
server.listen(port, () =>
|
|
2472
|
+
server.listen(port, () => resolve4());
|
|
2157
2473
|
});
|
|
2158
2474
|
}
|
|
2159
2475
|
function probe(port) {
|
|
2160
|
-
return new Promise((
|
|
2476
|
+
return new Promise((resolve4) => {
|
|
2161
2477
|
const req = httpGet({ host: "localhost", port, path: "/", timeout: 700 }, (r) => {
|
|
2162
2478
|
r.resume();
|
|
2163
|
-
|
|
2479
|
+
resolve4(true);
|
|
2164
2480
|
});
|
|
2165
|
-
req.on("error", () =>
|
|
2481
|
+
req.on("error", () => resolve4(false));
|
|
2166
2482
|
req.on("timeout", () => {
|
|
2167
2483
|
req.destroy();
|
|
2168
|
-
|
|
2484
|
+
resolve4(false);
|
|
2169
2485
|
});
|
|
2170
2486
|
});
|
|
2171
2487
|
}
|
|
@@ -2230,7 +2546,7 @@ function tokenHexes(w3c) {
|
|
|
2230
2546
|
if (obj.$type === "color" && typeof obj.$value === "string") {
|
|
2231
2547
|
const hex = normalizeHex(obj.$value);
|
|
2232
2548
|
if (hex) out.add(hex);
|
|
2233
|
-
const dark = obj.$extensions?.["design-
|
|
2549
|
+
const dark = obj.$extensions?.["design-pact"]?.dark;
|
|
2234
2550
|
if (typeof dark === "string") {
|
|
2235
2551
|
const dh = normalizeHex(dark);
|
|
2236
2552
|
if (dh) out.add(dh);
|
|
@@ -2387,19 +2703,348 @@ function reportCheck(result) {
|
|
|
2387
2703
|
return 1;
|
|
2388
2704
|
}
|
|
2389
2705
|
|
|
2706
|
+
// src/import.ts
|
|
2707
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
2708
|
+
import { basename, resolve as resolve2 } from "path";
|
|
2709
|
+
|
|
2710
|
+
// ../../lib/darkMode.ts
|
|
2711
|
+
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
2712
|
+
function deriveOppositeHex(hex, role) {
|
|
2713
|
+
const c = hexToOklch(hex);
|
|
2714
|
+
const flipped = 1 - c.l;
|
|
2715
|
+
let l;
|
|
2716
|
+
let chroma = c.c;
|
|
2717
|
+
switch (role) {
|
|
2718
|
+
case "background":
|
|
2719
|
+
l = c.l >= 0.5 ? clamp(flipped, 0.14, 0.24) : clamp(flipped, 0.92, 0.985);
|
|
2720
|
+
chroma = Math.min(c.c, 0.04);
|
|
2721
|
+
break;
|
|
2722
|
+
case "foreground":
|
|
2723
|
+
l = c.l < 0.5 ? clamp(flipped, 0.85, 0.97) : clamp(flipped, 0.12, 0.3);
|
|
2724
|
+
break;
|
|
2725
|
+
case "border":
|
|
2726
|
+
l = c.l >= 0.5 ? clamp(flipped, 0.25, 0.38) : clamp(flipped, 0.7, 0.85);
|
|
2727
|
+
chroma = Math.min(c.c, 0.04);
|
|
2728
|
+
break;
|
|
2729
|
+
case "muted":
|
|
2730
|
+
l = clamp(flipped, 0.55, 0.75);
|
|
2731
|
+
break;
|
|
2732
|
+
case "primary":
|
|
2733
|
+
case "accent":
|
|
2734
|
+
l = c.l < 0.55 ? clamp(c.l + 0.18, 0.55, 0.8) : c.l;
|
|
2735
|
+
break;
|
|
2736
|
+
default:
|
|
2737
|
+
l = clamp(flipped, 0.2, 0.9);
|
|
2738
|
+
}
|
|
2739
|
+
return oklchToHex({ mode: "oklch", l, c: chroma, h: c.h ?? 0 });
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
// ../../lib/semantic.ts
|
|
2743
|
+
var HUE = {
|
|
2744
|
+
success: 150,
|
|
2745
|
+
warning: 75,
|
|
2746
|
+
error: 27,
|
|
2747
|
+
info: 255
|
|
2748
|
+
};
|
|
2749
|
+
var CHROMA = {
|
|
2750
|
+
success: 0.15,
|
|
2751
|
+
warning: 0.16,
|
|
2752
|
+
error: 0.19,
|
|
2753
|
+
info: 0.16
|
|
2754
|
+
};
|
|
2755
|
+
function deriveSemantic(bgHex) {
|
|
2756
|
+
const bg = hexToOklch(bgHex);
|
|
2757
|
+
const darkBg = bg.l < 0.5;
|
|
2758
|
+
const out = {};
|
|
2759
|
+
for (const kind of SEMANTIC_KINDS) {
|
|
2760
|
+
const h = HUE[kind];
|
|
2761
|
+
const c = CHROMA[kind];
|
|
2762
|
+
let l = darkBg ? 0.72 : 0.58;
|
|
2763
|
+
let hex = oklchToHex({ mode: "oklch", l, c, h });
|
|
2764
|
+
for (let i = 0; i < 16 && contrastRatio(hex, bgHex) < 3.2; i++) {
|
|
2765
|
+
l = darkBg ? Math.min(0.92, l + 0.03) : Math.max(0.38, l - 0.03);
|
|
2766
|
+
hex = oklchToHex({ mode: "oklch", l, c, h });
|
|
2767
|
+
}
|
|
2768
|
+
out[kind] = hex;
|
|
2769
|
+
}
|
|
2770
|
+
return out;
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
// src/import.ts
|
|
2774
|
+
var ROLES2 = ["background", "foreground", "primary", "accent", "muted", "border"];
|
|
2775
|
+
var NAME_HINTS = [
|
|
2776
|
+
["background", /\b(?:background|bg|surface|canvas|page)\b/i],
|
|
2777
|
+
["foreground", /\b(?:foreground|fg|text|ink|body)\b/i],
|
|
2778
|
+
["primary", /\b(?:primary|brand|main|cta)\b/i],
|
|
2779
|
+
["accent", /\b(?:accent|highlight|secondary)\b/i],
|
|
2780
|
+
["muted", /\b(?:muted|gr[ae]y|neutral|subtle|placeholder)\b/i],
|
|
2781
|
+
["border", /\b(?:border|divider|outline|stroke|input|ring)\b/i]
|
|
2782
|
+
];
|
|
2783
|
+
var COLOR_LITERAL = /(#[0-9a-fA-F]{3,8}\b|rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*(?:,\s*[\d.]+\s*)?\))/g;
|
|
2784
|
+
function literalToHex(raw) {
|
|
2785
|
+
if (raw.startsWith("#")) return normalizeHex(raw);
|
|
2786
|
+
const m = /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})/.exec(raw);
|
|
2787
|
+
if (!m) return null;
|
|
2788
|
+
const [r, g, b] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
2789
|
+
if ([r, g, b].some((v) => v > 255)) return null;
|
|
2790
|
+
return "#" + [r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("");
|
|
2791
|
+
}
|
|
2792
|
+
function lineName(line, literalIndex) {
|
|
2793
|
+
const before = line.slice(0, literalIndex);
|
|
2794
|
+
const m = /([$@\w-]+)\s*:\s*["']?$/.exec(before);
|
|
2795
|
+
return m ? m[1] : "";
|
|
2796
|
+
}
|
|
2797
|
+
var CSS_PROPERTIES = /* @__PURE__ */ new Set([
|
|
2798
|
+
"background",
|
|
2799
|
+
"background-color",
|
|
2800
|
+
"color",
|
|
2801
|
+
"border",
|
|
2802
|
+
"border-color",
|
|
2803
|
+
"outline",
|
|
2804
|
+
"outline-color",
|
|
2805
|
+
"fill",
|
|
2806
|
+
"stroke",
|
|
2807
|
+
"caret-color",
|
|
2808
|
+
"accent-color"
|
|
2809
|
+
]);
|
|
2810
|
+
function scanSignals(text, weight, into) {
|
|
2811
|
+
for (const line of text.split("\n")) {
|
|
2812
|
+
for (const m of line.matchAll(COLOR_LITERAL)) {
|
|
2813
|
+
const hex = literalToHex(m[0]);
|
|
2814
|
+
if (!hex) continue;
|
|
2815
|
+
const sig = into.get(hex) ?? { hex, count: 0, hints: {} };
|
|
2816
|
+
sig.count += weight;
|
|
2817
|
+
const name = lineName(line, m.index ?? 0);
|
|
2818
|
+
if (name) {
|
|
2819
|
+
const hintWeight = CSS_PROPERTIES.has(name.toLowerCase()) ? 1 : weight * 2;
|
|
2820
|
+
const normalized = name.replace(/([a-z])([A-Z])/g, "$1-$2");
|
|
2821
|
+
for (const [role, re] of NAME_HINTS) {
|
|
2822
|
+
if (re.test(normalized)) sig.hints[role] = (sig.hints[role] ?? 0) + hintWeight;
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
into.set(hex, sig);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
var RADIUS_RE = /(?:border-radius|borderRadius|--radius[\w-]*)["']?\s*:\s*["']?(\d{1,2})px/g;
|
|
2830
|
+
var SPACING_VAR_RE = /--spacing-[\w-]*\s*:\s*(\d{1,2}(?:\.\d+)?)px/g;
|
|
2831
|
+
var FONT_FAMILY_RE = /font-family\s*:\s*([^;}"']{3,120})/i;
|
|
2832
|
+
var FONT_SIZE_RE = /(?:html|body|:root)[^{]*\{[^}]*?font-size\s*:\s*(\d{2})px/;
|
|
2833
|
+
var isTailwindConfig = (f) => /^tailwind\.config\.(js|cjs|mjs|ts)$/.test(basename(f));
|
|
2834
|
+
function pick(arr, score) {
|
|
2835
|
+
let best;
|
|
2836
|
+
let bestScore = -Infinity;
|
|
2837
|
+
for (const t2 of arr) {
|
|
2838
|
+
const s = score(t2);
|
|
2839
|
+
if (s > bestScore) {
|
|
2840
|
+
bestScore = s;
|
|
2841
|
+
best = t2;
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
return bestScore > 0 ? best : void 0;
|
|
2845
|
+
}
|
|
2846
|
+
var hueDist = (a, b) => {
|
|
2847
|
+
const d = Math.abs(a - b) % 360;
|
|
2848
|
+
return d > 180 ? 360 - d : d;
|
|
2849
|
+
};
|
|
2850
|
+
function blend(aHex, bHex, t2) {
|
|
2851
|
+
const a = hexToOklch(aHex);
|
|
2852
|
+
const b = hexToOklch(bHex);
|
|
2853
|
+
return oklchToHex({
|
|
2854
|
+
mode: "oklch",
|
|
2855
|
+
l: a.l + (b.l - a.l) * t2,
|
|
2856
|
+
c: a.c + (b.c - a.c) * t2,
|
|
2857
|
+
h: a.h ?? b.h ?? 0
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
function assignRoles(signals) {
|
|
2861
|
+
const used = /* @__PURE__ */ new Set();
|
|
2862
|
+
const out = /* @__PURE__ */ new Map();
|
|
2863
|
+
const take = (role, hex, provenance) => {
|
|
2864
|
+
out.set(role, { hex, provenance });
|
|
2865
|
+
used.add(hex);
|
|
2866
|
+
};
|
|
2867
|
+
const free = () => signals.filter((s) => !used.has(s.hex));
|
|
2868
|
+
for (const role of ROLES2) {
|
|
2869
|
+
const named = pick(free().filter((s) => (s.hints[role] ?? 0) > 0), (s) => (s.hints[role] ?? 0) * 1e6 + s.count);
|
|
2870
|
+
if (named) take(role, named.hex, "named");
|
|
2871
|
+
}
|
|
2872
|
+
const ok = (s) => hexToOklch(s.hex);
|
|
2873
|
+
if (!out.has("background")) {
|
|
2874
|
+
const bg = pick(free(), (s) => {
|
|
2875
|
+
const c = ok(s);
|
|
2876
|
+
return c.l >= 0.85 || c.l <= 0.2 ? s.count : 0;
|
|
2877
|
+
});
|
|
2878
|
+
if (bg) take("background", bg.hex, "heuristic");
|
|
2879
|
+
else take("background", "#ffffff", "derived");
|
|
2880
|
+
}
|
|
2881
|
+
const bgHex = out.get("background").hex;
|
|
2882
|
+
const bgDark = hexToOklch(bgHex).l < 0.5;
|
|
2883
|
+
if (!out.has("foreground")) {
|
|
2884
|
+
const fg = pick(free(), (s) => contrastRatio(s.hex, bgHex) >= 4.5 ? s.count * (1 + contrastRatio(s.hex, bgHex) / 21) : 0);
|
|
2885
|
+
if (fg) take("foreground", fg.hex, "heuristic");
|
|
2886
|
+
else take("foreground", bgDark ? "#e6e8ec" : "#1a1a1a", "derived");
|
|
2887
|
+
}
|
|
2888
|
+
const fgHex = out.get("foreground").hex;
|
|
2889
|
+
if (!out.has("primary")) {
|
|
2890
|
+
const p = pick(free(), (s) => {
|
|
2891
|
+
const c = ok(s);
|
|
2892
|
+
return c.c >= 0.09 && contrastRatio(s.hex, bgHex) >= 2 ? s.count : 0;
|
|
2893
|
+
});
|
|
2894
|
+
if (p) take("primary", p.hex, "heuristic");
|
|
2895
|
+
else take("primary", "#2f6df6", "derived");
|
|
2896
|
+
}
|
|
2897
|
+
const primaryHue = hexToOklch(out.get("primary").hex).h ?? 0;
|
|
2898
|
+
if (!out.has("accent")) {
|
|
2899
|
+
const a = pick(free(), (s) => {
|
|
2900
|
+
const c = ok(s);
|
|
2901
|
+
return c.c >= 0.09 && hueDist(c.h ?? 0, primaryHue) >= 40 ? s.count : 0;
|
|
2902
|
+
});
|
|
2903
|
+
if (a) take("accent", a.hex, "heuristic");
|
|
2904
|
+
else {
|
|
2905
|
+
const p = hexToOklch(out.get("primary").hex);
|
|
2906
|
+
take("accent", oklchToHex({ ...p, h: ((p.h ?? 0) + 60) % 360 }), "derived");
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
if (!out.has("muted")) {
|
|
2910
|
+
const m = pick(free(), (s) => {
|
|
2911
|
+
const c = ok(s);
|
|
2912
|
+
return c.c < 0.08 && c.l >= 0.3 && c.l <= 0.78 ? s.count : 0;
|
|
2913
|
+
});
|
|
2914
|
+
if (m) take("muted", m.hex, "heuristic");
|
|
2915
|
+
else take("muted", blend(bgHex, fgHex, 0.55), "derived");
|
|
2916
|
+
}
|
|
2917
|
+
if (!out.has("border")) {
|
|
2918
|
+
const bgL = hexToOklch(bgHex).l;
|
|
2919
|
+
const b = pick(free(), (s) => {
|
|
2920
|
+
const c = ok(s);
|
|
2921
|
+
return c.c < 0.08 && Math.abs(c.l - bgL) > 0.02 && Math.abs(c.l - bgL) < 0.25 ? s.count : 0;
|
|
2922
|
+
});
|
|
2923
|
+
if (b) take("border", b.hex, "heuristic");
|
|
2924
|
+
else take("border", blend(bgHex, fgHex, 0.12), "derived");
|
|
2925
|
+
}
|
|
2926
|
+
return ROLES2.map((role) => ({ role, ...out.get(role) }));
|
|
2927
|
+
}
|
|
2928
|
+
var mode = (values) => {
|
|
2929
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2930
|
+
for (const v of values) counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
2931
|
+
return pick([...counts.keys()], (k) => counts.get(k));
|
|
2932
|
+
};
|
|
2933
|
+
function scanProject(targets) {
|
|
2934
|
+
const signals = /* @__PURE__ */ new Map();
|
|
2935
|
+
const radii = [];
|
|
2936
|
+
const spacings = [];
|
|
2937
|
+
let fontFamily;
|
|
2938
|
+
let fontSizeBase;
|
|
2939
|
+
let filesScanned = 0;
|
|
2940
|
+
for (const target of targets) {
|
|
2941
|
+
for (const file of collectFiles(resolve2(target))) {
|
|
2942
|
+
let text;
|
|
2943
|
+
try {
|
|
2944
|
+
text = readFileSync2(file, "utf8");
|
|
2945
|
+
} catch {
|
|
2946
|
+
continue;
|
|
2947
|
+
}
|
|
2948
|
+
filesScanned++;
|
|
2949
|
+
const isCss = /\.(css|scss|sass|less)$/.test(file);
|
|
2950
|
+
scanSignals(text, isTailwindConfig(file) ? 5 : isCss ? 3 : 1, signals);
|
|
2951
|
+
for (const m of text.matchAll(RADIUS_RE)) {
|
|
2952
|
+
const px = Number(m[1]);
|
|
2953
|
+
if (px > 0 && px <= 24) radii.push(px);
|
|
2954
|
+
}
|
|
2955
|
+
for (const m of text.matchAll(SPACING_VAR_RE)) {
|
|
2956
|
+
const px = Number(m[1]);
|
|
2957
|
+
if (px >= 2 && px <= 8) spacings.push(px);
|
|
2958
|
+
}
|
|
2959
|
+
if (!fontFamily && (isCss || isTailwindConfig(file))) {
|
|
2960
|
+
const m = FONT_FAMILY_RE.exec(text);
|
|
2961
|
+
if (m) fontFamily = m[1].trim().replace(/["']/g, "").replace(/\s+/g, " ");
|
|
2962
|
+
}
|
|
2963
|
+
if (!fontSizeBase) {
|
|
2964
|
+
const m = FONT_SIZE_RE.exec(text);
|
|
2965
|
+
if (m) {
|
|
2966
|
+
const px = Number(m[1]);
|
|
2967
|
+
if (px >= 12 && px <= 22) fontSizeBase = px;
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
const all = [...signals.values()].filter((s) => s.count >= 2 || Object.keys(s.hints).length > 0);
|
|
2973
|
+
all.sort((a, b) => b.count - a.count);
|
|
2974
|
+
return {
|
|
2975
|
+
colors: assignRoles(all.slice(0, 64)),
|
|
2976
|
+
radiusBase: mode(radii),
|
|
2977
|
+
spacingBase: spacings.length > 0 ? Math.min(...spacings) : void 0,
|
|
2978
|
+
fontFamily,
|
|
2979
|
+
fontSizeBase,
|
|
2980
|
+
filesScanned
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
var PROPORTION = {
|
|
2984
|
+
background: 0.55,
|
|
2985
|
+
foreground: 0.2,
|
|
2986
|
+
primary: 0.1,
|
|
2987
|
+
accent: 0.06,
|
|
2988
|
+
muted: 0.05,
|
|
2989
|
+
border: 0.04
|
|
2990
|
+
};
|
|
2991
|
+
function draftToMarkdown(draft) {
|
|
2992
|
+
const detectedDark = hexToOklch(draft.colors.find((c) => c.role === "background").hex).l < 0.5;
|
|
2993
|
+
const face = (colors, flip) => colors.map(({ role, hex }, i) => {
|
|
2994
|
+
const display = flip ? deriveOppositeHex(hex, role) : hex;
|
|
2995
|
+
return {
|
|
2996
|
+
id: `c${i + 1}`,
|
|
2997
|
+
hex: display,
|
|
2998
|
+
baseHex: display,
|
|
2999
|
+
proportion: PROPORTION[role],
|
|
3000
|
+
role,
|
|
3001
|
+
displayHex: display
|
|
3002
|
+
};
|
|
3003
|
+
});
|
|
3004
|
+
const base = face(draft.colors, detectedDark);
|
|
3005
|
+
const dark = face(draft.colors, !detectedDark);
|
|
3006
|
+
const bg = base.find((c) => c.role === "background").displayHex;
|
|
3007
|
+
const darkBg = dark.find((c) => c.role === "background").displayHex;
|
|
3008
|
+
const typography = {
|
|
3009
|
+
...defaultTypography,
|
|
3010
|
+
...draft.fontSizeBase ? { base: draft.fontSizeBase } : {},
|
|
3011
|
+
...draft.fontFamily ? { fontFamily: draft.fontFamily, headingFamily: draft.fontFamily } : {}
|
|
3012
|
+
};
|
|
3013
|
+
return designSystemMarkdown(
|
|
3014
|
+
base,
|
|
3015
|
+
typography,
|
|
3016
|
+
draft.spacingBase ? { base: draft.spacingBase } : defaultSpacing,
|
|
3017
|
+
draft.radiusBase ? { base: draft.radiusBase } : defaultRadius,
|
|
3018
|
+
defaultShadow,
|
|
3019
|
+
defaultMotion,
|
|
3020
|
+
defaultBorder,
|
|
3021
|
+
defaultOpacity,
|
|
3022
|
+
dark,
|
|
3023
|
+
deriveSemantic(bg),
|
|
3024
|
+
deriveSemantic(darkBg)
|
|
3025
|
+
);
|
|
3026
|
+
}
|
|
3027
|
+
function studioQuery(draft) {
|
|
3028
|
+
const hexes = draft.colors.map((c) => c.hex.slice(1)).join("-");
|
|
3029
|
+
return `p=${hexes}~${encodeURIComponent("Imported")}~${encodeURIComponent("Derived from your codebase")}`;
|
|
3030
|
+
}
|
|
3031
|
+
|
|
2390
3032
|
// src/cli.ts
|
|
2391
3033
|
var FORMATS = ["css", "tailwind", "w3c"];
|
|
2392
3034
|
function fail(msg) {
|
|
2393
3035
|
console.error(`\u2717 ${msg}`);
|
|
2394
3036
|
process.exit(1);
|
|
2395
3037
|
}
|
|
3038
|
+
var BOOL_FLAGS = /* @__PURE__ */ new Set(["force", "global"]);
|
|
2396
3039
|
function parseArgs(argv) {
|
|
2397
3040
|
const positional = [];
|
|
2398
3041
|
const opts = {};
|
|
2399
3042
|
for (let i = 0; i < argv.length; i++) {
|
|
2400
3043
|
const a = argv[i];
|
|
2401
|
-
if (a.startsWith("--"))
|
|
2402
|
-
|
|
3044
|
+
if (a.startsWith("--")) {
|
|
3045
|
+
const name = a.slice(2);
|
|
3046
|
+
opts[name] = BOOL_FLAGS.has(name) ? "true" : argv[++i] ?? "";
|
|
3047
|
+
} else positional.push(a);
|
|
2403
3048
|
}
|
|
2404
3049
|
return { positional, opts };
|
|
2405
3050
|
}
|
|
@@ -2412,7 +3057,7 @@ function readMd(file) {
|
|
|
2412
3057
|
)
|
|
2413
3058
|
);
|
|
2414
3059
|
try {
|
|
2415
|
-
return
|
|
3060
|
+
return readFileSync3(resolve3(file), "utf8");
|
|
2416
3061
|
} catch {
|
|
2417
3062
|
return fail(t(`Cannot read file: ${file}`, `\u8BFB\u4E0D\u5230\u6587\u4EF6\uFF1A${file}`));
|
|
2418
3063
|
}
|
|
@@ -2421,7 +3066,7 @@ function cmdInspect(file) {
|
|
|
2421
3066
|
const { w3c } = parseDesignSystem(readMd(file));
|
|
2422
3067
|
const color = w3c.color ?? {};
|
|
2423
3068
|
const typo = w3c.typography ?? {};
|
|
2424
|
-
const ext = typo.$extensions?.["design-
|
|
3069
|
+
const ext = typo.$extensions?.["design-pact"] ?? {};
|
|
2425
3070
|
console.log(t("Design system summary", "\u8BBE\u8BA1\u7CFB\u7EDF\u6458\u8981"));
|
|
2426
3071
|
console.log(t(" Colors:", " \u989C\u8272\uFF1A"));
|
|
2427
3072
|
for (const [role, v] of Object.entries(color)) console.log(` ${role.padEnd(12)} ${v.$value}`);
|
|
@@ -2436,7 +3081,7 @@ function cmdAdd(file, opts) {
|
|
|
2436
3081
|
if (fmt !== "all" && !FORMATS.includes(fmt))
|
|
2437
3082
|
fail(t(`Unknown format: ${fmt} (css|tailwind|w3c|all)`, `\u672A\u77E5\u683C\u5F0F\uFF1A${fmt}\uFF08css|tailwind|w3c|all\uFF09`));
|
|
2438
3083
|
const want = fmt === "all" ? FORMATS : [fmt];
|
|
2439
|
-
const outDir =
|
|
3084
|
+
const outDir = resolve3(opts.out || ".");
|
|
2440
3085
|
mkdirSync(outDir, { recursive: true });
|
|
2441
3086
|
const written = [];
|
|
2442
3087
|
const write = (name, content) => {
|
|
@@ -2471,6 +3116,36 @@ async function main() {
|
|
|
2471
3116
|
const result = runCheck(ds.w3c, targets.length > 0 ? targets : ["."], allow);
|
|
2472
3117
|
process.exit(reportCheck(result));
|
|
2473
3118
|
}
|
|
3119
|
+
case "import": {
|
|
3120
|
+
const targets = positional.length > 0 ? positional : ["."];
|
|
3121
|
+
const outPath = resolve3(opts.out || "design.md");
|
|
3122
|
+
if (existsSync2(outPath) && !opts.force)
|
|
3123
|
+
fail(t(`${outPath} already exists \u2014 pass --force to overwrite.`, `${outPath} \u5DF2\u5B58\u5728\u2014\u2014\u7528 --force \u8986\u76D6\u3002`));
|
|
3124
|
+
const draft = scanProject(targets);
|
|
3125
|
+
writeFileSync(outPath, draftToMarkdown(draft), "utf8");
|
|
3126
|
+
console.log(t(
|
|
3127
|
+
`\u2713 Draft design.md written to ${outPath} (${draft.filesScanned} file(s) scanned)`,
|
|
3128
|
+
`\u2713 design.md \u8349\u7A3F\u5DF2\u5199\u5165 ${outPath}\uFF08\u626B\u63CF ${draft.filesScanned} \u4E2A\u6587\u4EF6\uFF09`
|
|
3129
|
+
));
|
|
3130
|
+
console.log(t(" Detected palette:", " \u8BC6\u522B\u51FA\u7684\u8272\u677F\uFF1A"));
|
|
3131
|
+
const provenanceLabel = {
|
|
3132
|
+
named: ["from a named variable", "\u6765\u81EA\u547D\u540D\u53D8\u91CF"],
|
|
3133
|
+
heuristic: ["by usage heuristic", "\u6309\u4F7F\u7528\u9891\u7387\u63A8\u65AD"],
|
|
3134
|
+
derived: ["derived (no signal found)", "\u6D3E\u751F\uFF08\u672A\u627E\u5230\u4FE1\u53F7\uFF09"]
|
|
3135
|
+
};
|
|
3136
|
+
for (const c of draft.colors) {
|
|
3137
|
+
const [en, zh] = provenanceLabel[c.provenance];
|
|
3138
|
+
console.log(` ${c.role.padEnd(12)} ${c.hex} ${t(en, zh)}`);
|
|
3139
|
+
}
|
|
3140
|
+
const q = studioQuery(draft);
|
|
3141
|
+
console.log("\n" + t(
|
|
3142
|
+
"Review it visually before adopting \u2014 heuristic/derived roles deserve a look:",
|
|
3143
|
+
"\u91C7\u7528\u524D\u5EFA\u8BAE\u5148\u5230 studio \u91CC\u8FC7\u76EE\u2014\u2014\u63A8\u65AD/\u6D3E\u751F\u7684\u89D2\u8272\u503C\u5F97\u786E\u8BA4\uFF1A"
|
|
3144
|
+
));
|
|
3145
|
+
console.log(` https://design-pact.vercel.app/?${q}`);
|
|
3146
|
+
console.log(t(` or locally: npx design-pact open "${q}"`, ` \u6216\u672C\u5730\u6253\u5F00\uFF1Anpx design-pact open "${q}"`));
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
2474
3149
|
case void 0:
|
|
2475
3150
|
case "-h":
|
|
2476
3151
|
case "--help":
|
|
@@ -2501,6 +3176,10 @@ async function main() {
|
|
|
2501
3176
|
' check <file> [paths\u2026] [--allow "#hex,#hex"] find color literals outside the contract',
|
|
2502
3177
|
' check <file> [paths\u2026] [--allow "#hex,#hex"] \u627E\u51FA\u5951\u7EA6\u5916\u7684\u989C\u8272\u5B57\u9762\u91CF'
|
|
2503
3178
|
),
|
|
3179
|
+
t(
|
|
3180
|
+
" import [paths\u2026] [--out design.md] [--force] derive a draft design.md from existing code",
|
|
3181
|
+
" import [paths\u2026] [--out design.md] [--force] \u4ECE\u73B0\u6709\u4EE3\u7801\u53CD\u63A8 design.md \u8349\u7A3F"
|
|
3182
|
+
),
|
|
2504
3183
|
"",
|
|
2505
3184
|
t(
|
|
2506
3185
|
'design.md is exported from the studio ("Download design.md").',
|
|
@@ -2510,7 +3189,7 @@ async function main() {
|
|
|
2510
3189
|
);
|
|
2511
3190
|
return;
|
|
2512
3191
|
default:
|
|
2513
|
-
fail(t(`Unknown command: ${cmd} (init|open|add|inspect|check)`, `\u672A\u77E5\u547D\u4EE4\uFF1A${cmd}\uFF08init|open|add|inspect|check\uFF09`));
|
|
3192
|
+
fail(t(`Unknown command: ${cmd} (init|open|add|inspect|check|import)`, `\u672A\u77E5\u547D\u4EE4\uFF1A${cmd}\uFF08init|open|add|inspect|check|import\uFF09`));
|
|
2514
3193
|
}
|
|
2515
3194
|
}
|
|
2516
3195
|
main().catch((e) => fail(e instanceof Error ? e.message : String(e)));
|