@spectrabrain/eds 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/consumer/cli.mjs CHANGED
@@ -8,13 +8,13 @@ const usage = [
8
8
  "Usage: eds <command> [options]",
9
9
  "",
10
10
  "Commands:",
11
- " init Register sync:eds and eds:doctor, plus pretypecheck/prebuild (eds doctor) when typecheck/build exist,",
12
- " write allowScripts on npm 12+, then run sync. Run it in the package that declares @spectrabrain/eds",
11
+ " init Add sync:eds and eds:doctor when missing; add pretypecheck/prebuild when target scripts exist",
12
+ " and hooks are missing; write allowScripts on npm 12+, then sync in the package declaring @spectrabrain/eds",
13
13
  " sync Generate src/styles/eds-tokens.css, wire the Tailwind root CSS and update AGENTS.md",
14
14
  " and the @AGENTS.md import in CLAUDE.md (or .claude/CLAUDE.md) and GEMINI.md when they exist",
15
15
  " (without a Tailwind root only those change); notes say when an agent may not load the block",
16
- " doctor Check whether sync is up to date, the EDS font is imported and every eds-* class in the sources",
17
- " is a public token (exit 1 when something is off)",
16
+ " doctor Check sync status and the EDS font import; with a Tailwind CSS v4 root, also check that each eds-*",
17
+ " class in the sources is a public token (exit 1 when something is off)",
18
18
  " align One-time reorganization of a Vite + React frontend into the EVA conventions (FRONTEND_CONVENTIONS.md):",
19
19
  " write .eds/align/PLAN.md, the steps an AI agent follows, plus templates; source files are not changed",
20
20
  "",
@@ -25,9 +25,9 @@ const usage = [
25
25
  " --no-allow-scripts init: do not write allowScripts to package.json on npm 12+",
26
26
  " --print-ci github|gitlab init: print a CI job that runs eds doctor with this repository's package manager",
27
27
  " --warn doctor: report findings but exit 0 (same as EDS_DOCTOR=warn)",
28
- " --scan doctor: also list Tailwind palette and arbitrary color classes that bypass the EDS tokens",
29
- " --check align: check the conventions and that no backend or out-of-package file changed since",
30
- " the plan's base commit (exit 1 when something is off)",
28
+ " --scan doctor: with a Tailwind CSS v4 root, also list palette and arbitrary color classes that bypass EDS tokens",
29
+ " --check align: check conventions and protected, preserved and out-of-package files",
30
+ " since the plan's base commit; repository root lockfiles are excluded (exit 1 on error)",
31
31
  " --frontend <dir> align: the frontend package when the current directory is not it",
32
32
  " --help Show this message",
33
33
  "",
@@ -11,10 +11,6 @@ const propertyNames = [
11
11
 
12
12
  export const fontSansStyleName = "body1";
13
13
 
14
- function safeTailwindAlias(name) {
15
- return "eds-" + name.replace(/\./g, "-");
16
- }
17
-
18
14
  function cssVariableForToken(manifest, name) {
19
15
  const cssVariable = manifest.tokens?.[name]?.$extensions?.eds?.cssVariable;
20
16
  if (typeof cssVariable === "string") {
@@ -24,27 +20,9 @@ function cssVariableForToken(manifest, name) {
24
20
  return "--eds-" + name.replace(/\./g, "\\.");
25
21
  }
26
22
 
27
- function collectDimensionAliases(manifest, prefix) {
28
- const aliases = new Map();
29
-
30
- for (const name of Object.keys(manifest.tokens ?? {}).filter((entry) => entry.startsWith(prefix))) {
31
- const alias = safeTailwindAlias(name);
32
- const existing = aliases.get(alias);
33
- if (existing !== undefined && existing !== name) {
34
- throw new Error(`Tailwind alias collision: ${existing} and ${name} both map to ${alias}.`);
35
- }
36
-
37
- aliases.set(alias, name);
38
- }
39
-
40
- return [...aliases.entries()].sort(([left], [right]) => left.localeCompare(right));
41
- }
42
-
43
23
  export function renderTailwindCss(manifest, version) {
44
24
  const tokens = Object.keys(manifest.tokens ?? {});
45
25
  const colorNames = tokens.filter((name) => name.startsWith("color-"));
46
- const spacingAliases = collectDimensionAliases(manifest, "spacing-");
47
- const radiusAliases = collectDimensionAliases(manifest, "radius-");
48
26
  const typography = new Map();
49
27
 
50
28
  for (const name of tokens.filter((entry) => entry.startsWith("typography-"))) {
@@ -74,12 +52,6 @@ export function renderTailwindCss(manifest, version) {
74
52
  const colorAliases = colorNames
75
53
  .map((name) => ` --color-eds-${name.slice("color-".length)}: var(${cssVariableForToken(manifest, name)});`)
76
54
  .join("\n");
77
- const spacingThemeAliases = spacingAliases
78
- .map(([alias, name]) => ` --spacing-${alias}: var(${cssVariableForToken(manifest, name)});`)
79
- .join("\n");
80
- const radiusThemeAliases = radiusAliases
81
- .map(([alias, name]) => ` --radius-${alias}: var(${cssVariableForToken(manifest, name)});`)
82
- .join("\n");
83
55
  // Last line of the @theme block: Tailwind's default font family becomes the EDS body font.
84
56
  const fontSans = ` --font-sans: var(--eds-typography-${fontSansStyleName}-fontFamily);`;
85
57
  const typographyUtilities = [...typography.keys()].sort().map((styleName) => [
@@ -96,8 +68,6 @@ export function renderTailwindCss(manifest, version) {
96
68
  `/* Generated from @spectrabrain/eds@${version}. Do not edit manually. */`,
97
69
  "@theme inline {",
98
70
  colorAliases,
99
- spacingThemeAliases,
100
- radiusThemeAliases,
101
71
  fontSans,
102
72
  "}",
103
73
  "",
@@ -10,9 +10,10 @@ const opacity = "(?:/[0-9]{1,3})?";
10
10
 
11
11
  const edsColorClass = new RegExp(boundaryBefore + variantPrefix + colorUtility + "-eds-([A-Za-z0-9-]+)" + opacity + boundaryAfter, "g");
12
12
  const edsTypographyClass = new RegExp(boundaryBefore + variantPrefix + "eds-typography-([A-Za-z0-9-]+)" + boundaryAfter, "g");
13
- const dimensionUtility = "(?:p|px|py|ps|pe|pt|pr|pb|pl|m|mx|my|ms|me|mt|mr|mb|ml|gap|gap-x|gap-y|space-x|space-y|inset|inset-x|inset-y|start|end|top|right|bottom|left|w|min-w|max-w|h|min-h|max-h|size|basis|scroll-m|scroll-p)";
14
- const edsSpacingClass = new RegExp(boundaryBefore + variantPrefix + dimensionUtility + "-eds-(spacing-[A-Za-z0-9-]+)" + boundaryAfter, "g");
15
- const edsRadiusClass = new RegExp(boundaryBefore + variantPrefix + "rounded(?:-(?:s|e|t|r|b|l|ss|se|ee|es|tl|tr|br|bl))?-eds-(radius-[A-Za-z0-9-]+)" + boundaryAfter, "g");
13
+ const retiredDimensionClass = new RegExp(
14
+ boundaryBefore + variantPrefix + "-?[A-Za-z0-9-]+-eds-(?:spacing|radius)-[A-Za-z0-9-]+" + boundaryAfter,
15
+ "g",
16
+ );
16
17
  const rawColorClass = new RegExp(
17
18
  boundaryBefore + variantPrefix + colorUtility +
18
19
  "-(?:(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-[0-9]{2,3}|white|black|\\[(?:#|rgba?\\(|hsla?\\(|oklch\\(|oklab\\(|color\\()[^\\]]*\\])" +
@@ -20,11 +21,10 @@ const rawColorClass = new RegExp(
20
21
  "g",
21
22
  );
22
23
 
23
- // The public class names are derived exactly like eds sync derives the Tailwind aliases from tokens.json.
24
+ // EDS publishes color and typography utilities. Spacing and radius use Tailwind CSS v4 defaults.
24
25
  export function publicClassNames(manifest) {
25
26
  const colors = new Set();
26
27
  const typography = new Set();
27
- const dimensions = new Set();
28
28
  for (const name of Object.keys(manifest.tokens ?? {})) {
29
29
  if (name.startsWith("color-")) {
30
30
  colors.add("eds-" + name.slice("color-".length));
@@ -35,18 +35,15 @@ export function publicClassNames(manifest) {
35
35
  typography.add(match[1]);
36
36
  continue;
37
37
  }
38
- if (name.startsWith("spacing-") || name.startsWith("radius-")) {
39
- dimensions.add("eds-" + name.replace(/\./g, "-"));
40
- }
41
38
  }
42
- return { colors, typography, dimensions };
39
+ return { colors, typography };
43
40
  }
44
41
 
45
42
  // files: [{ path, contents }] as readSources() lists them (the generated eds-tokens.css is not among them).
46
43
  // unknown: eds-* classes that no public token backs (always a mistake, Tailwind silently drops them).
47
44
  // raw: Tailwind palette, white/black and arbitrary color classes that bypass the EDS tokens (informational).
48
45
  export function scanClassNames({ consumerRoot, manifest, files }) {
49
- const { colors, typography, dimensions } = publicClassNames(manifest);
46
+ const { colors, typography } = publicClassNames(manifest);
50
47
  const unknown = [];
51
48
  const raw = [];
52
49
  for (const file of files) {
@@ -70,15 +67,8 @@ export function scanClassNames({ consumerRoot, manifest, files }) {
70
67
  unknown.push({ location, className: match[0] });
71
68
  }
72
69
  }
73
- for (const match of line.matchAll(edsSpacingClass)) {
74
- if (match[1] !== undefined && !dimensions.has("eds-" + match[1])) {
75
- unknown.push({ location, className: match[0] });
76
- }
77
- }
78
- for (const match of line.matchAll(edsRadiusClass)) {
79
- if (match[1] !== undefined && !dimensions.has("eds-" + match[1])) {
80
- unknown.push({ location, className: match[0] });
81
- }
70
+ for (const match of line.matchAll(retiredDimensionClass)) {
71
+ unknown.push({ location, className: match[0] });
82
72
  }
83
73
  for (const match of line.matchAll(rawColorClass)) {
84
74
  raw.push({ location, className: match[0] });
@@ -65,10 +65,8 @@ Layout은 prop으로 지정하지 않고 슬롯 조합에서 추론합니다.
65
65
  | `iconBoth` | `children`과 앞·뒤 아이콘을 모두 지정합니다. | 양쪽 아이콘이 모두 의미를 더하는 명확한 경우에만 사용합니다. |
66
66
  | `iconOnly` | `children` 없이 `icon` 또는 `iconName`과 `aria-label`을 지정합니다. | |
67
67
 
68
- - 이름 슬롯(`iconName`·`startIconName`·`endIconName`)은 승인된 아이콘 목록의 lucide PascalCase 전체 이름만 받습니다. `ButtonIconName`이 이 목록과 같은 union이라 목록에 없는 이름은 TypeScript 오류이고, 런타임에 전달되면 render 오류입니다. 전체 목록은 설치된 패키지 `spec.json`의 `api.iconNames`에 있습니다.
69
- - 목록은 `lucide-react`의 canonical 이름만 포함하며 `AlertCircle`처럼 deprecated된 alias는 허용하지 않습니다.
70
- - 목록 밖 아이콘은 `icon`, `startIcon`, `endIcon`에 `ReactElement`로 전달하며, 같은 슬롯에 이름과 custom element를 함께 지정하지 않습니다. `lucide-react`에서 직접 import할 때는 EDS package root `package.json`의 `dependencies["lucide-react"]`와 같은 exact 버전을 소비처 `package.json`에 선언합니다. pnpm처럼 엄격한 node_modules에서는 선언 없이 resolve되지 않고, 버전이 다르면 아이콘 코드가 두 벌 번들됩니다. custom lucide 아이콘은 lucide 기본 stroke를 그대로 써서 이름으로 지정한 아이콘과 맞춥니다.
71
- - 목록 밖 아이콘을 `ReactElement`로 썼다면 완료 보고에 아이콘 이름과 사용 파일을 allowlist 추가 후보로 적습니다.
68
+ - 이름 슬롯(`iconName`·`startIconName`·`endIconName`)은 설치된 `lucide-react`의 실제 아이콘 component 이름과 alias를 모두 받습니다. `ButtonIconName`이 이 목록을 자동완성하며, 전체 이름은 `spec.json`의 `api.iconNames`에 있습니다. 잘못된 이름은 TypeScript 오류이고 런타임에도 render 오류입니다.
69
+ - custom 아이콘은 `icon`·`startIcon`·`endIcon`에 `ReactElement`로 전달합니다. 같은 슬롯에 이름과 custom element를 함께 지정하지 않습니다. `lucide-react`에서 직접 import할 때는 EDS package root `package.json`의 `dependencies["lucide-react"]`와 같은 exact 버전을 소비처 `package.json`에 선언합니다. pnpm처럼 엄격한 node_modules에서는 선언 없이 resolve되지 않고, 버전이 다르면 아이콘 코드가 두 벌 번들됩니다. 이름 슬롯은 설치된 아이콘 크기와 stroke를 사용합니다.
72
70
 
73
71
  ### State
74
72
 
@@ -109,7 +107,7 @@ Layout은 prop으로 지정하지 않고 슬롯 조합에서 추론합니다.
109
107
  | `aria-label` | 선택적인 `string`이며, 지정하면 비어 있지 않아야 합니다. icon-only에서는 항상 지정합니다. |
110
108
  | `disabled` | `boolean`입니다. native button은 `disabled`, link는 href 제거·`aria-disabled="true"`·`tabIndex={-1}`를 적용합니다. |
111
109
  | `loading` | `boolean`입니다. label·아이콘 공간을 유지한 progress circle을 표시하고 상호작용을 차단하며 `aria-busy="true"`를 적용합니다. |
112
- | `className` | `string`입니다. 허용 범위는 `QUICKSTART.md` 규칙 5를 따릅니다. |
110
+ | `className` | `string`입니다. 허용 범위는 `QUICKSTART.md`의 `className` 규칙을 따릅니다. |
113
111
  | `style` | 지원하지 않습니다(`never`). 스타일 override는 공개 API가 아닙니다. |
114
112
  | `aria-*` / `data-*` | 각 branch의 native HTML 속성으로 전달할 수 있습니다. |
115
113
  | `ref` | branch에 따라 `HTMLButtonElement` 또는 `HTMLAnchorElement`를 전달합니다. |