design-pact 0.2.2 → 0.3.1
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 +16 -0
- package/SKILL.md +12 -0
- package/dist/cli.js +712 -22
- package/package.json +1 -1
- package/web/404.html +1 -1
- package/web/__next.__PAGE__.txt +4 -4
- package/web/__next._full.txt +11 -11
- 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/{0si4eubpj00yw.js → 0ge~kyq7lzl~5.js} +2 -2
- 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 +9 -9
- 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 +9 -9
- package/web/index.html +1 -1
- package/web/index.txt +11 -11
- package/web/_next/static/chunks/0-l9p_pd2v836.js +0 -5
- 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/{sZ0sIOnTEk-ANZHZKcMr8 → _0bNIY4qsM7kv4cRcMSMv}/_buildManifest.js +0 -0
- /package/web/_next/static/{sZ0sIOnTEk-ANZHZKcMr8 → _0bNIY4qsM7kv4cRcMSMv}/_clientMiddlewareManifest.js +0 -0
- /package/web/_next/static/{sZ0sIOnTEk-ANZHZKcMr8 → _0bNIY4qsM7kv4cRcMSMv}/_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();
|
|
@@ -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) => {
|
|
@@ -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
|
}
|
|
@@ -2387,19 +2703,359 @@ 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
|
+
var STATE_WORDS = /\b(?:focus|hover|active|pressed|disabled|selected|visited|checked)\b/i;
|
|
2811
|
+
var STRUCTURAL_ROLES = /* @__PURE__ */ new Set(["background", "foreground", "border", "muted"]);
|
|
2812
|
+
function scanSignals(text, weight, into) {
|
|
2813
|
+
for (const line of text.split("\n")) {
|
|
2814
|
+
for (const m of line.matchAll(COLOR_LITERAL)) {
|
|
2815
|
+
const hex = literalToHex(m[0]);
|
|
2816
|
+
if (!hex) continue;
|
|
2817
|
+
const sig = into.get(hex) ?? { hex, count: 0, hints: {} };
|
|
2818
|
+
sig.count += weight;
|
|
2819
|
+
const name = lineName(line, m.index ?? 0);
|
|
2820
|
+
if (name) {
|
|
2821
|
+
const hintWeight = CSS_PROPERTIES.has(name.toLowerCase()) ? 1 : weight * 2;
|
|
2822
|
+
const normalized = name.replace(/([a-z])([A-Z])/g, "$1-$2");
|
|
2823
|
+
if (!STATE_WORDS.test(normalized)) {
|
|
2824
|
+
let matched = NAME_HINTS.filter(([, re]) => re.test(normalized)).map(([role]) => role);
|
|
2825
|
+
if (matched.some((r) => STRUCTURAL_ROLES.has(r))) {
|
|
2826
|
+
matched = matched.filter((r) => r !== "primary" && r !== "accent");
|
|
2827
|
+
}
|
|
2828
|
+
for (const role of matched) sig.hints[role] = (sig.hints[role] ?? 0) + hintWeight;
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
into.set(hex, sig);
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
var RADIUS_RE = /(?:border-radius|borderRadius|--radius[\w-]*)["']?\s*:\s*["']?(\d{1,2})px/g;
|
|
2836
|
+
var SPACING_VAR_RE = /--spacing-[\w-]*\s*:\s*(\d{1,2}(?:\.\d+)?)px/g;
|
|
2837
|
+
var FONT_FAMILY_RE = /font-family\s*:\s*([^;}]{3,120})/i;
|
|
2838
|
+
var FONT_SIZE_RE = /(?:html|body|:root)[^{]*\{[^}]*?font-size\s*:\s*(\d{2})px/;
|
|
2839
|
+
var isTailwindConfig = (f) => /^tailwind\.config\.(js|cjs|mjs|ts)$/.test(basename(f));
|
|
2840
|
+
function pick(arr, score) {
|
|
2841
|
+
let best;
|
|
2842
|
+
let bestScore = -Infinity;
|
|
2843
|
+
for (const t2 of arr) {
|
|
2844
|
+
const s = score(t2);
|
|
2845
|
+
if (s > bestScore) {
|
|
2846
|
+
bestScore = s;
|
|
2847
|
+
best = t2;
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
return bestScore > 0 ? best : void 0;
|
|
2851
|
+
}
|
|
2852
|
+
var hueDist = (a, b) => {
|
|
2853
|
+
const d = Math.abs(a - b) % 360;
|
|
2854
|
+
return d > 180 ? 360 - d : d;
|
|
2855
|
+
};
|
|
2856
|
+
function blend(aHex, bHex, t2) {
|
|
2857
|
+
const a = hexToOklch(aHex);
|
|
2858
|
+
const b = hexToOklch(bHex);
|
|
2859
|
+
return oklchToHex({
|
|
2860
|
+
mode: "oklch",
|
|
2861
|
+
l: a.l + (b.l - a.l) * t2,
|
|
2862
|
+
c: a.c + (b.c - a.c) * t2,
|
|
2863
|
+
h: a.h ?? b.h ?? 0
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
function assignRoles(signals) {
|
|
2867
|
+
const used = /* @__PURE__ */ new Set();
|
|
2868
|
+
const out = /* @__PURE__ */ new Map();
|
|
2869
|
+
const take = (role, hex, provenance) => {
|
|
2870
|
+
out.set(role, { hex, provenance });
|
|
2871
|
+
used.add(hex);
|
|
2872
|
+
};
|
|
2873
|
+
const free = () => signals.filter((s) => !used.has(s.hex));
|
|
2874
|
+
for (const role of ROLES2) {
|
|
2875
|
+
const named = pick(free().filter((s) => (s.hints[role] ?? 0) > 0), (s) => (s.hints[role] ?? 0) * 1e6 + s.count);
|
|
2876
|
+
if (named) take(role, named.hex, "named");
|
|
2877
|
+
}
|
|
2878
|
+
const ok = (s) => hexToOklch(s.hex);
|
|
2879
|
+
if (!out.has("background")) {
|
|
2880
|
+
const bg = pick(free(), (s) => {
|
|
2881
|
+
const c = ok(s);
|
|
2882
|
+
return c.l >= 0.85 || c.l <= 0.2 ? s.count : 0;
|
|
2883
|
+
});
|
|
2884
|
+
if (bg) take("background", bg.hex, "heuristic");
|
|
2885
|
+
else take("background", "#ffffff", "derived");
|
|
2886
|
+
}
|
|
2887
|
+
const bgHex = out.get("background").hex;
|
|
2888
|
+
const bgDark = hexToOklch(bgHex).l < 0.5;
|
|
2889
|
+
if (!out.has("foreground")) {
|
|
2890
|
+
const fg = pick(free(), (s) => contrastRatio(s.hex, bgHex) >= 4.5 ? s.count * (1 + contrastRatio(s.hex, bgHex) / 21) : 0);
|
|
2891
|
+
if (fg) take("foreground", fg.hex, "heuristic");
|
|
2892
|
+
else take("foreground", bgDark ? "#e6e8ec" : "#1a1a1a", "derived");
|
|
2893
|
+
}
|
|
2894
|
+
const fgHex = out.get("foreground").hex;
|
|
2895
|
+
if (!out.has("primary")) {
|
|
2896
|
+
const p = pick(free(), (s) => {
|
|
2897
|
+
const c = ok(s);
|
|
2898
|
+
return c.c >= 0.09 && contrastRatio(s.hex, bgHex) >= 2 ? s.count : 0;
|
|
2899
|
+
});
|
|
2900
|
+
if (p) take("primary", p.hex, "heuristic");
|
|
2901
|
+
else take("primary", "#2f6df6", "derived");
|
|
2902
|
+
}
|
|
2903
|
+
const primaryHue = hexToOklch(out.get("primary").hex).h ?? 0;
|
|
2904
|
+
if (!out.has("accent")) {
|
|
2905
|
+
const a = pick(free(), (s) => {
|
|
2906
|
+
const c = ok(s);
|
|
2907
|
+
return c.c >= 0.09 && hueDist(c.h ?? 0, primaryHue) >= 40 ? s.count : 0;
|
|
2908
|
+
});
|
|
2909
|
+
if (a) take("accent", a.hex, "heuristic");
|
|
2910
|
+
else {
|
|
2911
|
+
const p = hexToOklch(out.get("primary").hex);
|
|
2912
|
+
take("accent", oklchToHex({ ...p, h: ((p.h ?? 0) + 60) % 360 }), "derived");
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
if (!out.has("muted")) {
|
|
2916
|
+
const m = pick(free(), (s) => {
|
|
2917
|
+
const c = ok(s);
|
|
2918
|
+
return c.c < 0.08 && c.l >= 0.3 && c.l <= 0.78 ? s.count : 0;
|
|
2919
|
+
});
|
|
2920
|
+
if (m) take("muted", m.hex, "heuristic");
|
|
2921
|
+
else take("muted", blend(bgHex, fgHex, 0.55), "derived");
|
|
2922
|
+
}
|
|
2923
|
+
if (!out.has("border")) {
|
|
2924
|
+
const bgL = hexToOklch(bgHex).l;
|
|
2925
|
+
const b = pick(free(), (s) => {
|
|
2926
|
+
const c = ok(s);
|
|
2927
|
+
return c.c < 0.08 && Math.abs(c.l - bgL) > 0.02 && Math.abs(c.l - bgL) < 0.25 ? s.count : 0;
|
|
2928
|
+
});
|
|
2929
|
+
if (b) take("border", b.hex, "heuristic");
|
|
2930
|
+
else take("border", blend(bgHex, fgHex, 0.12), "derived");
|
|
2931
|
+
}
|
|
2932
|
+
return ROLES2.map((role) => ({ role, ...out.get(role) }));
|
|
2933
|
+
}
|
|
2934
|
+
var mode = (values) => {
|
|
2935
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2936
|
+
for (const v of values) counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
2937
|
+
return pick([...counts.keys()], (k) => counts.get(k));
|
|
2938
|
+
};
|
|
2939
|
+
function scanProject(targets) {
|
|
2940
|
+
const signals = /* @__PURE__ */ new Map();
|
|
2941
|
+
const radii = [];
|
|
2942
|
+
const spacings = [];
|
|
2943
|
+
let fontFamily;
|
|
2944
|
+
let fontSizeBase;
|
|
2945
|
+
let filesScanned = 0;
|
|
2946
|
+
for (const target of targets) {
|
|
2947
|
+
for (const file of collectFiles(resolve2(target))) {
|
|
2948
|
+
let text;
|
|
2949
|
+
try {
|
|
2950
|
+
text = readFileSync2(file, "utf8");
|
|
2951
|
+
} catch {
|
|
2952
|
+
continue;
|
|
2953
|
+
}
|
|
2954
|
+
filesScanned++;
|
|
2955
|
+
const isCss = /\.(css|scss|sass|less)$/.test(file);
|
|
2956
|
+
scanSignals(text, isTailwindConfig(file) ? 5 : isCss ? 3 : 1, signals);
|
|
2957
|
+
for (const m of text.matchAll(RADIUS_RE)) {
|
|
2958
|
+
const px = Number(m[1]);
|
|
2959
|
+
if (px > 0 && px <= 24) radii.push(px);
|
|
2960
|
+
}
|
|
2961
|
+
for (const m of text.matchAll(SPACING_VAR_RE)) {
|
|
2962
|
+
const px = Number(m[1]);
|
|
2963
|
+
if (px >= 2 && px <= 8) spacings.push(px);
|
|
2964
|
+
}
|
|
2965
|
+
if (!fontFamily && (isCss || isTailwindConfig(file))) {
|
|
2966
|
+
for (const m of text.matchAll(new RegExp(FONT_FAMILY_RE.source, "gi"))) {
|
|
2967
|
+
const value2 = m[1].trim().replace(/["']/g, "").replace(/\s+/g, " ");
|
|
2968
|
+
if (!/^var\(/i.test(value2)) {
|
|
2969
|
+
fontFamily = value2;
|
|
2970
|
+
break;
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
if (!fontSizeBase) {
|
|
2975
|
+
const m = FONT_SIZE_RE.exec(text);
|
|
2976
|
+
if (m) {
|
|
2977
|
+
const px = Number(m[1]);
|
|
2978
|
+
if (px >= 12 && px <= 22) fontSizeBase = px;
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
const all = [...signals.values()].filter((s) => s.count >= 2 || Object.keys(s.hints).length > 0);
|
|
2984
|
+
all.sort((a, b) => b.count - a.count);
|
|
2985
|
+
return {
|
|
2986
|
+
colors: assignRoles(all.slice(0, 64)),
|
|
2987
|
+
radiusBase: mode(radii),
|
|
2988
|
+
spacingBase: spacings.length > 0 ? Math.min(...spacings) : void 0,
|
|
2989
|
+
fontFamily,
|
|
2990
|
+
fontSizeBase,
|
|
2991
|
+
filesScanned
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2994
|
+
var PROPORTION = {
|
|
2995
|
+
background: 0.55,
|
|
2996
|
+
foreground: 0.2,
|
|
2997
|
+
primary: 0.1,
|
|
2998
|
+
accent: 0.06,
|
|
2999
|
+
muted: 0.05,
|
|
3000
|
+
border: 0.04
|
|
3001
|
+
};
|
|
3002
|
+
function draftToMarkdown(draft) {
|
|
3003
|
+
const detectedDark = hexToOklch(draft.colors.find((c) => c.role === "background").hex).l < 0.5;
|
|
3004
|
+
const face = (colors, flip) => colors.map(({ role, hex }, i) => {
|
|
3005
|
+
const display = flip ? deriveOppositeHex(hex, role) : hex;
|
|
3006
|
+
return {
|
|
3007
|
+
id: `c${i + 1}`,
|
|
3008
|
+
hex: display,
|
|
3009
|
+
baseHex: display,
|
|
3010
|
+
proportion: PROPORTION[role],
|
|
3011
|
+
role,
|
|
3012
|
+
displayHex: display
|
|
3013
|
+
};
|
|
3014
|
+
});
|
|
3015
|
+
const base = face(draft.colors, detectedDark);
|
|
3016
|
+
const dark = face(draft.colors, !detectedDark);
|
|
3017
|
+
const bg = base.find((c) => c.role === "background").displayHex;
|
|
3018
|
+
const darkBg = dark.find((c) => c.role === "background").displayHex;
|
|
3019
|
+
const typography = {
|
|
3020
|
+
...defaultTypography,
|
|
3021
|
+
...draft.fontSizeBase ? { base: draft.fontSizeBase } : {},
|
|
3022
|
+
...draft.fontFamily ? { fontFamily: draft.fontFamily, headingFamily: draft.fontFamily } : {}
|
|
3023
|
+
};
|
|
3024
|
+
return designSystemMarkdown(
|
|
3025
|
+
base,
|
|
3026
|
+
typography,
|
|
3027
|
+
draft.spacingBase ? { base: draft.spacingBase } : defaultSpacing,
|
|
3028
|
+
draft.radiusBase ? { base: draft.radiusBase } : defaultRadius,
|
|
3029
|
+
defaultShadow,
|
|
3030
|
+
defaultMotion,
|
|
3031
|
+
defaultBorder,
|
|
3032
|
+
defaultOpacity,
|
|
3033
|
+
dark,
|
|
3034
|
+
deriveSemantic(bg),
|
|
3035
|
+
deriveSemantic(darkBg)
|
|
3036
|
+
);
|
|
3037
|
+
}
|
|
3038
|
+
function studioQuery(draft) {
|
|
3039
|
+
const hexes = draft.colors.map((c) => c.hex.slice(1)).join("-");
|
|
3040
|
+
return `p=${hexes}~${encodeURIComponent("Imported")}~${encodeURIComponent("Derived from your codebase")}`;
|
|
3041
|
+
}
|
|
3042
|
+
|
|
2390
3043
|
// src/cli.ts
|
|
2391
3044
|
var FORMATS = ["css", "tailwind", "w3c"];
|
|
2392
3045
|
function fail(msg) {
|
|
2393
3046
|
console.error(`\u2717 ${msg}`);
|
|
2394
3047
|
process.exit(1);
|
|
2395
3048
|
}
|
|
3049
|
+
var BOOL_FLAGS = /* @__PURE__ */ new Set(["force", "global"]);
|
|
2396
3050
|
function parseArgs(argv) {
|
|
2397
3051
|
const positional = [];
|
|
2398
3052
|
const opts = {};
|
|
2399
3053
|
for (let i = 0; i < argv.length; i++) {
|
|
2400
3054
|
const a = argv[i];
|
|
2401
|
-
if (a.startsWith("--"))
|
|
2402
|
-
|
|
3055
|
+
if (a.startsWith("--")) {
|
|
3056
|
+
const name = a.slice(2);
|
|
3057
|
+
opts[name] = BOOL_FLAGS.has(name) ? "true" : argv[++i] ?? "";
|
|
3058
|
+
} else positional.push(a);
|
|
2403
3059
|
}
|
|
2404
3060
|
return { positional, opts };
|
|
2405
3061
|
}
|
|
@@ -2412,7 +3068,7 @@ function readMd(file) {
|
|
|
2412
3068
|
)
|
|
2413
3069
|
);
|
|
2414
3070
|
try {
|
|
2415
|
-
return
|
|
3071
|
+
return readFileSync3(resolve3(file), "utf8");
|
|
2416
3072
|
} catch {
|
|
2417
3073
|
return fail(t(`Cannot read file: ${file}`, `\u8BFB\u4E0D\u5230\u6587\u4EF6\uFF1A${file}`));
|
|
2418
3074
|
}
|
|
@@ -2436,7 +3092,7 @@ function cmdAdd(file, opts) {
|
|
|
2436
3092
|
if (fmt !== "all" && !FORMATS.includes(fmt))
|
|
2437
3093
|
fail(t(`Unknown format: ${fmt} (css|tailwind|w3c|all)`, `\u672A\u77E5\u683C\u5F0F\uFF1A${fmt}\uFF08css|tailwind|w3c|all\uFF09`));
|
|
2438
3094
|
const want = fmt === "all" ? FORMATS : [fmt];
|
|
2439
|
-
const outDir =
|
|
3095
|
+
const outDir = resolve3(opts.out || ".");
|
|
2440
3096
|
mkdirSync(outDir, { recursive: true });
|
|
2441
3097
|
const written = [];
|
|
2442
3098
|
const write = (name, content) => {
|
|
@@ -2471,6 +3127,36 @@ async function main() {
|
|
|
2471
3127
|
const result = runCheck(ds.w3c, targets.length > 0 ? targets : ["."], allow);
|
|
2472
3128
|
process.exit(reportCheck(result));
|
|
2473
3129
|
}
|
|
3130
|
+
case "import": {
|
|
3131
|
+
const targets = positional.length > 0 ? positional : ["."];
|
|
3132
|
+
const outPath = resolve3(opts.out || "design.md");
|
|
3133
|
+
if (existsSync2(outPath) && !opts.force)
|
|
3134
|
+
fail(t(`${outPath} already exists \u2014 pass --force to overwrite.`, `${outPath} \u5DF2\u5B58\u5728\u2014\u2014\u7528 --force \u8986\u76D6\u3002`));
|
|
3135
|
+
const draft = scanProject(targets);
|
|
3136
|
+
writeFileSync(outPath, draftToMarkdown(draft), "utf8");
|
|
3137
|
+
console.log(t(
|
|
3138
|
+
`\u2713 Draft design.md written to ${outPath} (${draft.filesScanned} file(s) scanned)`,
|
|
3139
|
+
`\u2713 design.md \u8349\u7A3F\u5DF2\u5199\u5165 ${outPath}\uFF08\u626B\u63CF ${draft.filesScanned} \u4E2A\u6587\u4EF6\uFF09`
|
|
3140
|
+
));
|
|
3141
|
+
console.log(t(" Detected palette:", " \u8BC6\u522B\u51FA\u7684\u8272\u677F\uFF1A"));
|
|
3142
|
+
const provenanceLabel = {
|
|
3143
|
+
named: ["from a named variable", "\u6765\u81EA\u547D\u540D\u53D8\u91CF"],
|
|
3144
|
+
heuristic: ["by usage heuristic", "\u6309\u4F7F\u7528\u9891\u7387\u63A8\u65AD"],
|
|
3145
|
+
derived: ["derived (no signal found)", "\u6D3E\u751F\uFF08\u672A\u627E\u5230\u4FE1\u53F7\uFF09"]
|
|
3146
|
+
};
|
|
3147
|
+
for (const c of draft.colors) {
|
|
3148
|
+
const [en, zh] = provenanceLabel[c.provenance];
|
|
3149
|
+
console.log(` ${c.role.padEnd(12)} ${c.hex} ${t(en, zh)}`);
|
|
3150
|
+
}
|
|
3151
|
+
const q = studioQuery(draft);
|
|
3152
|
+
console.log("\n" + t(
|
|
3153
|
+
"Review it visually before adopting \u2014 heuristic/derived roles deserve a look:",
|
|
3154
|
+
"\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"
|
|
3155
|
+
));
|
|
3156
|
+
console.log(` https://design-pact.vercel.app/?${q}`);
|
|
3157
|
+
console.log(t(` or locally: npx design-pact open "${q}"`, ` \u6216\u672C\u5730\u6253\u5F00\uFF1Anpx design-pact open "${q}"`));
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
2474
3160
|
case void 0:
|
|
2475
3161
|
case "-h":
|
|
2476
3162
|
case "--help":
|
|
@@ -2501,6 +3187,10 @@ async function main() {
|
|
|
2501
3187
|
' check <file> [paths\u2026] [--allow "#hex,#hex"] find color literals outside the contract',
|
|
2502
3188
|
' check <file> [paths\u2026] [--allow "#hex,#hex"] \u627E\u51FA\u5951\u7EA6\u5916\u7684\u989C\u8272\u5B57\u9762\u91CF'
|
|
2503
3189
|
),
|
|
3190
|
+
t(
|
|
3191
|
+
" import [paths\u2026] [--out design.md] [--force] derive a draft design.md from existing code",
|
|
3192
|
+
" import [paths\u2026] [--out design.md] [--force] \u4ECE\u73B0\u6709\u4EE3\u7801\u53CD\u63A8 design.md \u8349\u7A3F"
|
|
3193
|
+
),
|
|
2504
3194
|
"",
|
|
2505
3195
|
t(
|
|
2506
3196
|
'design.md is exported from the studio ("Download design.md").',
|
|
@@ -2510,7 +3200,7 @@ async function main() {
|
|
|
2510
3200
|
);
|
|
2511
3201
|
return;
|
|
2512
3202
|
default:
|
|
2513
|
-
fail(t(`Unknown command: ${cmd} (init|open|add|inspect|check)`, `\u672A\u77E5\u547D\u4EE4\uFF1A${cmd}\uFF08init|open|add|inspect|check\uFF09`));
|
|
3203
|
+
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
3204
|
}
|
|
2515
3205
|
}
|
|
2516
3206
|
main().catch((e) => fail(e instanceof Error ? e.message : String(e)));
|