@spectrabrain/eds 0.0.3 → 0.1.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/AI.md +43 -16
- package/AI_CONTEXT.json +159 -0
- package/CHANGELOG.md +58 -0
- package/CONSUMER_AGENT.md +230 -0
- package/QUICKSTART.md +47 -0
- package/README.md +33 -10
- package/TAILWIND_V4.md +233 -0
- package/TOKEN_CONTEXT.md +71 -0
- package/consumer/cli.mjs +107 -0
- package/consumer/css-root.mjs +239 -0
- package/consumer/doctor.mjs +178 -0
- package/consumer/generate-tailwind.mjs +83 -0
- package/consumer/init.mjs +172 -0
- package/consumer/managed-block.mjs +120 -0
- package/consumer/paths.mjs +211 -0
- package/consumer/register-scripts.mjs +124 -0
- package/consumer/scan-classes.mjs +70 -0
- package/consumer/sync.mjs +133 -0
- package/consumer/update-agent-context.mjs +87 -0
- package/consumer-postinstall.mjs +59 -0
- package/dist/base.css +4 -0
- package/dist/components/Button/README.md +29 -10
- package/dist/components/Button/spec.json +64 -7
- package/dist/components.css +0 -2
- package/dist/index.d.mts +39 -2
- package/dist/index.d.ts +39 -2
- package/dist/index.js +120 -6266
- package/dist/index.mjs +122 -6268
- package/dist/styles.css +1 -0
- package/dist/tokens.css +19 -10
- package/dist/tokens.d.mts +3 -3
- package/dist/tokens.d.ts +3 -3
- package/dist/tokens.js +92 -17
- package/dist/tokens.json +104 -10
- package/dist/tokens.mjs +92 -17
- package/llms.txt +15 -7
- package/package.json +33 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { applyBlock, locateBlock, maskFencedBlocks, readBlockVersion, readTemplateBlock, renderBlock } from "./managed-block.mjs";
|
|
3
|
+
import { detectEol, readTextIfExists, realpathOrNull, relativeTo, withEol } from "./paths.mjs";
|
|
4
|
+
|
|
5
|
+
export const claudeBeginMarker = "<!-- EDS:CLAUDE:BEGIN -->";
|
|
6
|
+
export const claudeEndMarker = "<!-- EDS:CLAUDE:END -->";
|
|
7
|
+
export const claudeImportLine = "@AGENTS.md";
|
|
8
|
+
export const geminiBeginMarker = "<!-- EDS:GEMINI:BEGIN -->";
|
|
9
|
+
export const geminiEndMarker = "<!-- EDS:GEMINI:END -->";
|
|
10
|
+
// Gemini CLI documents its import syntax with an explicit relative path.
|
|
11
|
+
export const geminiImportLine = "@./AGENTS.md";
|
|
12
|
+
|
|
13
|
+
// Claude Code and Gemini CLI accept `@AGENTS.md` and `@./AGENTS.md` as the same import and ignore fenced code blocks.
|
|
14
|
+
const importPattern = /^[ \t]*@(?:\.\/)?AGENTS\.md[ \t]*$/m;
|
|
15
|
+
|
|
16
|
+
export function planAgentContext({ consumerRoot, pkg, packageManager = null }) {
|
|
17
|
+
const filePath = path.join(consumerRoot, "AGENTS.md");
|
|
18
|
+
const before = readTextIfExists(filePath);
|
|
19
|
+
// The block is rendered with the target file's dominant line ending; a new file uses LF.
|
|
20
|
+
const eol = before === null ? "\n" : detectEol(before);
|
|
21
|
+
const block = withEol(renderBlock(readTemplateBlock(pkg.packageRoot), pkg.version, packageManager), eol);
|
|
22
|
+
const located = before === null ? null : locateBlock(before);
|
|
23
|
+
return {
|
|
24
|
+
label: "agents",
|
|
25
|
+
path: filePath,
|
|
26
|
+
relativePath: relativeTo(consumerRoot, filePath),
|
|
27
|
+
before,
|
|
28
|
+
after: applyBlock(before, block, eol),
|
|
29
|
+
block,
|
|
30
|
+
existingBlock: located?.block ?? null,
|
|
31
|
+
existingVersion: located ? readBlockVersion(located.block) : null,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hasImport(contents, beginMarker, endMarker) {
|
|
36
|
+
const visible = maskFencedBlocks(contents);
|
|
37
|
+
if (visible.includes(beginMarker) && visible.includes(endMarker)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return importPattern.test(visible);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Returns null when the file does not exist, or when it and AGENTS.md are the same file (a symlink in either
|
|
44
|
+
// direction): the managed block already lives in that file and an import would point at itself.
|
|
45
|
+
function planImportFile({ consumerRoot, fileName, label, beginMarker, endMarker, importLine }) {
|
|
46
|
+
const filePath = path.join(consumerRoot, fileName);
|
|
47
|
+
const before = readTextIfExists(filePath);
|
|
48
|
+
if (before === null) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const realPath = realpathOrNull(filePath);
|
|
52
|
+
const agentsRealPath = realpathOrNull(path.join(consumerRoot, "AGENTS.md"));
|
|
53
|
+
if (realPath !== null && realPath === agentsRealPath) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const eol = detectEol(before);
|
|
57
|
+
const block = [beginMarker, importLine, endMarker].join(eol);
|
|
58
|
+
const trimmed = before.replace(/\s*$/, "");
|
|
59
|
+
const after = hasImport(before, beginMarker, endMarker)
|
|
60
|
+
? before
|
|
61
|
+
: (trimmed ? trimmed + eol + eol + block + eol : block + eol);
|
|
62
|
+
return { label, path: filePath, relativePath: relativeTo(consumerRoot, filePath), before, after };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Claude Code reads CLAUDE.md; the import makes it follow the AGENTS.md managed block.
|
|
66
|
+
export function planClaudeImport({ consumerRoot }) {
|
|
67
|
+
return planImportFile({
|
|
68
|
+
consumerRoot,
|
|
69
|
+
fileName: "CLAUDE.md",
|
|
70
|
+
label: "claude",
|
|
71
|
+
beginMarker: claudeBeginMarker,
|
|
72
|
+
endMarker: claudeEndMarker,
|
|
73
|
+
importLine: claudeImportLine,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Gemini CLI reads GEMINI.md by default and AGENTS.md only with a context.fileName setting, so it gets the same import.
|
|
78
|
+
export function planGeminiImport({ consumerRoot }) {
|
|
79
|
+
return planImportFile({
|
|
80
|
+
consumerRoot,
|
|
81
|
+
fileName: "GEMINI.md",
|
|
82
|
+
label: "gemini",
|
|
83
|
+
beginMarker: geminiBeginMarker,
|
|
84
|
+
endMarker: geminiEndMarker,
|
|
85
|
+
importLine: geminiImportLine,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { registerConsumerScripts } from "./consumer/register-scripts.mjs";
|
|
5
|
+
|
|
6
|
+
const packageRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const consumerRoot = process.env.INIT_CWD;
|
|
8
|
+
|
|
9
|
+
if (!consumerRoot || path.resolve(consumerRoot) === packageRoot) {
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const consumerPackagePath = path.join(consumerRoot, "package.json");
|
|
14
|
+
if (!fs.existsSync(consumerPackagePath)) {
|
|
15
|
+
console.log("[EVA Design System] Consumer package.json was not found; run the documented setup manually.");
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function realpathOrNull(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
return fs.realpathSync(filePath);
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const installedPath = realpathOrNull(path.join(consumerRoot, "node_modules", "@spectrabrain", "eds"));
|
|
28
|
+
if (installedPath === null || installedPath !== realpathOrNull(packageRoot)) {
|
|
29
|
+
console.log("[EVA Design System] Skipping consumer setup: this install is not the consumer's node_modules/@spectrabrain/eds. Run npx @spectrabrain/eds init from the consumer root if needed.");
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// pnpm rewrites the consumer package.json after dependency lifecycle scripts, so edits made here are lost.
|
|
34
|
+
if (String(process.env.npm_config_user_agent ?? "").startsWith("pnpm/")) {
|
|
35
|
+
console.log("[EVA Design System] pnpm discards package.json edits made during install; run pnpm exec eds init to register the EDS scripts.");
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let consumerPackage;
|
|
40
|
+
try {
|
|
41
|
+
consumerPackage = JSON.parse(fs.readFileSync(consumerPackagePath, "utf8"));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.log("[EVA Design System] Consumer package.json could not be parsed (" + error.message + "); run npx @spectrabrain/eds init after fixing it.");
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A workspaces root hoists the package but is not the app that uses it: registering the scripts and hooks
|
|
48
|
+
// there would make the root build look for src/styles/eds-tokens.css and AGENTS.md in the wrong place.
|
|
49
|
+
if (consumerPackage.workspaces !== undefined) {
|
|
50
|
+
console.log("[EVA Design System] Skipping consumer setup: package.json declares workspaces. Run npx eds init inside the app package that depends on @spectrabrain/eds.");
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The install must never fail because of this setup step; anything unexpected is reported and left to eds doctor.
|
|
55
|
+
try {
|
|
56
|
+
registerConsumerScripts(consumerRoot);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.log("[EVA Design System] Consumer setup was skipped (" + (error instanceof Error ? error.message : String(error)) + "). Run npx eds init to register the EDS scripts.");
|
|
59
|
+
}
|
package/dist/base.css
ADDED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
이 문서는 `Button` 디렉토리의 기준 문서입니다. `Button`의 개요·사용 맥락·공개 API·접근성 계약을 기록합니다. Button을 사용하기 전에는 이 문서를 먼저 확인합니다.
|
|
4
4
|
|
|
5
|
-
패키지 설치와 공개 경로는 루트 [README.md](../../../README.md)를, 소비처 AI의
|
|
5
|
+
패키지 설치와 공개 경로는 루트 [README.md](../../../README.md)를, 소비처 AI의 필수 규칙은 루트 [QUICKSTART.md](../../../QUICKSTART.md)를, 공개 범위와 금지 사례는 루트 [AI.md](../../../AI.md)를 참고합니다. 루트 문서에는 컴포넌트별 정책을 중복해서 기록하지 않습니다.
|
|
6
6
|
|
|
7
7
|
## 개요
|
|
8
8
|
|
|
@@ -10,6 +10,21 @@
|
|
|
10
10
|
|
|
11
11
|
탐색 목적의 링크는 `neutral/link` 조합으로 표현하고, 상태 변경·폼 제출·명령 실행은 native `<button>`으로 표현합니다. `Button`은 현재 React 웹과 light theme만 지원합니다.
|
|
12
12
|
|
|
13
|
+
화면에서 반복되는 행동 맥락은 다음처럼 선택합니다.
|
|
14
|
+
|
|
15
|
+
| 행동 맥락 | 우선 검토할 조합 | 설명 |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| 생성·저장·관리·확인처럼 화면의 핵심 행동 | `brand/solid` | 사용자가 완료해야 할 대표 행동을 강조합니다. 한 화면에 하나를 권장합니다. |
|
|
18
|
+
| 검색·조회·일반적인 주요 실행 | `neutral/solid` | 브랜드 핵심 행동보다 낮은 주목도로 주요 액션을 표현합니다. |
|
|
19
|
+
| 취소·닫기·수정 같은 보조 작업 | `neutral/outline` | 주 행동과 시각적 위계를 분리합니다. |
|
|
20
|
+
| 초기화·갱신·되돌리기처럼 부담이 낮은 반복 행동 | `neutral/weak` | CTA를 보조하면서 반복 가능한 행동을 표현합니다. 되돌리기 어려운 초기화는 `critical/weak`입니다. |
|
|
21
|
+
| 되돌리기 어려운 삭제·초기화 | `critical/weak` | 위험을 분명히 전달하고 가능하면 확인 절차와 함께 사용합니다. |
|
|
22
|
+
| 비동기 저장·확인 처리 | 선택한 조합과 `loading` | 중복 실행을 막고 라벨·아이콘 공간과 accessible name을 유지합니다. |
|
|
23
|
+
|
|
24
|
+
기존 화면에서 다른 조합을 사용하고 있더라도 새 코드에서는 행동의 의미에 맞는 위계를 먼저 검토합니다. 보조 행동은 한 조합으로 고정합니다. 취소·닫기·수정은 `neutral/outline`, 초기화·갱신·되돌리기처럼 부담이 낮은 반복 행동은 `neutral/weak`입니다. `neutral/link`는 페이지나 콘텐츠로 이동하는 탐색에만 사용합니다.
|
|
25
|
+
|
|
26
|
+
표 행 조작이나 pagination처럼 공간이 좁은 곳에서는 `iconOnly`를 사용할 수 있습니다. 아이콘만으로 의미가 충분한지 확인하고, 필요한 경우 비어 있지 않은 `aria-label`을 지정합니다. 아이콘은 생성·수정·삭제·외부 이동·다운로드처럼 행동의 목적이나 방향을 보조할 때만 사용합니다.
|
|
27
|
+
|
|
13
28
|
## 사용 맥락
|
|
14
29
|
|
|
15
30
|
### Size
|
|
@@ -32,7 +47,7 @@ Layout은 라벨과 아이콘의 조합으로 결정합니다. 아이콘은 행
|
|
|
32
47
|
| `iconFirst` | 라벨 앞에 아이콘을 둡니다. | 아이콘이 행동의 목적을 보조할 때 사용합니다. |
|
|
33
48
|
| `iconLast` | 라벨 뒤에 아이콘을 둡니다. | 이동 방향이나 다음 단계를 나타낼 때 사용합니다. |
|
|
34
49
|
| `iconBoth` | 라벨 양쪽에 아이콘을 둡니다. | 양쪽 아이콘이 모두 의미를 더하는 명확한 경우에만 사용합니다. |
|
|
35
|
-
| `iconOnly` | 아이콘만 제공합니다. | 아이콘만으로 행동의 의미가 충분히 명확할 때 사용하며,
|
|
50
|
+
| `iconOnly` | 아이콘만 제공합니다. | 아이콘만으로 행동의 의미가 충분히 명확할 때 사용하며, 접근성 이름을 제공하려면 `aria-label`을 지정합니다. |
|
|
36
51
|
|
|
37
52
|
### Role
|
|
38
53
|
|
|
@@ -53,8 +68,8 @@ Variant는 같은 Role 안에서 시각적 위계를 정의합니다.
|
|
|
53
68
|
| `brand/solid` | 서비스의 핵심 행동에 사용합니다. 한 화면에 하나를 권장합니다. |
|
|
54
69
|
| `brand/outline` | `brand/solid`보다 낮은 위계의 브랜드 보조 행동에 사용합니다. `neutral/outline`과 조합할 수 있으며 같은 액션 그룹에서 `brand/solid`와 함께 사용하지 않는 것을 권장합니다. |
|
|
55
70
|
| `neutral/solid` | `brand/solid` 다음 위계의 일반적인 주요 CTA에 사용합니다. 한 화면에 하나를 권장합니다. |
|
|
56
|
-
| `neutral/weak` |
|
|
57
|
-
| `neutral/outline` |
|
|
71
|
+
| `neutral/weak` | 초기화·갱신·되돌리기처럼 부담이 낮은 반복 행동에 사용합니다. 취소·닫기·수정은 `neutral/outline`을 사용합니다. |
|
|
72
|
+
| `neutral/outline` | 취소·닫기·수정처럼 주 행동과 위계를 분리하는 보조 행동에 사용합니다. `neutral/solid`과 같은 액션 그룹에서 함께 사용하지 않으며, `brand/outline`과 조합할 수 있습니다. |
|
|
58
73
|
| `neutral/ghost` | 툴바·헤더·옵션 메뉴처럼 주변 UI와의 간섭을 줄여야 하는 보조 행동에 사용합니다. |
|
|
59
74
|
| `neutral/link` | 다른 페이지·콘텐츠로 이동하거나 관련 정보를 확인하는 탐색성 행동에 사용합니다. 상태 변경이나 일반 CTA에는 사용하지 않습니다. `href`가 필요하며 `<a>`로 렌더링합니다. |
|
|
60
75
|
| `critical/weak` | 삭제·초기화처럼 되돌리기 어려운 행동에 사용합니다. 위험을 분명히 전달하며 가능하면 확인 대화상자나 Alert Dialog와 함께 사용합니다. |
|
|
@@ -98,10 +113,10 @@ Variant는 같은 Role 안에서 시각적 위계를 정의합니다.
|
|
|
98
113
|
| --- | --- |
|
|
99
114
|
| `children` | `ReactNode` label입니다. 렌더링 가능한 내용이 하나 이상 필요하며 `startIcon`·`endIcon`과 조합할 수 있습니다. |
|
|
100
115
|
| `icon` | icon-only용 단일 custom `ReactElement`입니다. children이나 start/end icon과 함께 사용하지 않습니다. |
|
|
101
|
-
| `iconName` | icon-only용 `ButtonIconName` lucide 이름입니다. |
|
|
116
|
+
| `iconName` | icon-only용 `ButtonIconName` lucide 이름입니다. 승인된 아이콘 목록의 이름만 허용합니다. |
|
|
102
117
|
| `startIcon` / `endIcon` | label 앞·뒤의 단일 custom `ReactElement` 슬롯입니다. |
|
|
103
|
-
| `startIconName` / `endIconName` | label 앞·뒤의 `ButtonIconName` lucide 이름 슬롯입니다. |
|
|
104
|
-
| `aria-label` | `string
|
|
118
|
+
| `startIconName` / `endIconName` | label 앞·뒤의 `ButtonIconName` lucide 이름 슬롯입니다. 승인된 아이콘 목록의 이름만 허용합니다. |
|
|
119
|
+
| `aria-label` | 선택적인 `string`입니다. icon-only의 accessible name을 제공할 때 지정하며, 필수로 강제하지 않습니다. |
|
|
105
120
|
| `disabled` | `boolean`입니다. native button은 `disabled`, link는 href 제거·`aria-disabled="true"`·`tabIndex={-1}`를 적용합니다. |
|
|
106
121
|
| `loading` | `boolean`입니다. label·아이콘 공간을 유지한 progress circle을 표시하고 상호작용을 차단합니다. `aria-busy="true"`를 적용합니다. |
|
|
107
122
|
| `className` | `string`이며 배치·컨테이너 보정에만 허용합니다. |
|
|
@@ -114,15 +129,19 @@ Variant는 같은 Role 안에서 시각적 위계를 정의합니다.
|
|
|
114
129
|
|
|
115
130
|
`startIcon`, `endIcon`, `icon`은 단일 `ReactElement` 슬롯입니다. lucide 아이콘은 `startIconName`, `endIconName`, `iconName`으로 PascalCase 전체 이름을 지정합니다. 이름 prop은 `ButtonIconName` 자동완성을 제공하며 custom ReactElement와 같은 슬롯에 함께 지정할 수 없습니다.
|
|
116
131
|
|
|
132
|
+
`ButtonIconName`은 EVA 화면에서 실제로 사용하는 lucide 아이콘을 모은 승인된 아이콘 목록입니다. 목록은 `lucide-react`의 canonical 이름만 포함하며 `AlertCircle`처럼 deprecated된 alias는 허용하지 않습니다. 승인된 이름 전체는 설치된 패키지의 공개 `spec.json`의 `api.iconNames`와 내부 Storybook 아이콘 카탈로그에서 확인합니다. 이름으로 지정한 아이콘은 동기적으로 렌더링하며 SSR에서도 SVG를 바로 출력합니다. 목록에 없는 이름은 TypeScript 오류로 막고, 런타임에 전달되면 render 오류로 처리합니다.
|
|
133
|
+
|
|
134
|
+
승인된 아이콘 목록 밖의 아이콘이 필요하면 `icon`, `startIcon`, `endIcon`에 `ReactElement`로 전달합니다. 같은 아이콘이 두 화면 이상에서 반복되면 승인된 아이콘 목록에 추가하는 PR을 엽니다. 슬롯 아이콘을 `lucide-react`에서 직접 import할 때는 소비처 `package.json`에 `lucide-react`를 EDS와 같은 `1.45.0`으로 선언합니다. pnpm처럼 엄격한 node_modules에서는 선언 없이 resolve되지 않고, 버전이 다르면 아이콘 코드가 두 벌 번들됩니다. 슬롯 아이콘의 크기는 Button이 맞추므로 stroke 두께만 lucide 기본값 `2`로 두면 이름으로 지정한 아이콘과 같아 보입니다.
|
|
135
|
+
|
|
117
136
|
이름으로 지정한 lucide 아이콘은 `lucide-react`의 기본 시각 설정을 사용합니다. Button은 아이콘 영역의 크기만 조정하며, `color`, `size`, `strokeWidth`, `strokeLinecap`, `strokeLinejoin`을 소비처 prop으로 노출하거나 덮어쓰지 않습니다. `aria-hidden`과 `focusable`은 Button 접근성 contract에 따라 내부에서 설정합니다.
|
|
118
137
|
|
|
119
138
|
- children만 있으면 `textOnly`입니다.
|
|
120
139
|
- children과 `startIcon`이 있으면 `iconFirst`입니다.
|
|
121
140
|
- children과 `endIcon`이 있으면 `iconLast`입니다.
|
|
122
141
|
- children과 두 아이콘이 있으면 `iconBoth`입니다.
|
|
123
|
-
- children 없이 `icon`만 있으면 `iconOnly
|
|
142
|
+
- children 없이 `icon` 또는 `iconName`만 있으면 `iconOnly`입니다. 접근성 이름이 필요하면 비어 있지 않은 `aria-label`을 지정합니다.
|
|
124
143
|
|
|
125
|
-
빈 label·icon
|
|
144
|
+
빈 label·icon, 제공된 빈 `aria-label`과 잘못된 슬롯 조합은 모든 환경에서 오류로 처리합니다. label은 한 줄을 유지합니다.
|
|
126
145
|
|
|
127
146
|
`className`, native `aria-*`, `data-*`, `forwardRef`를 지원합니다. `style`과 임의 스타일 override는 공개하지 않습니다.
|
|
128
147
|
|
|
@@ -135,7 +154,7 @@ Variant는 같은 Role 안에서 시각적 위계를 정의합니다.
|
|
|
135
154
|
- hover는 CSS `:hover`로 처리합니다.
|
|
136
155
|
- 승인된 시각 상태는 enabled, hover, disabled, loading이며 focus-visible은 별도 focus ring 상태로 제공합니다. active·pressed·visited 상태는 지원하지 않습니다.
|
|
137
156
|
- focus-visible ring은 `color-stroke-focusRing` semantic token과 2px outline·offset 구조값을 사용합니다.
|
|
138
|
-
- 아이콘은 시각 장식으로 `aria-hidden="true"` 처리하며, icon-only의
|
|
157
|
+
- 아이콘은 시각 장식으로 `aria-hidden="true"` 처리하며, icon-only의 accessible name을 제공할 때 Button의 `aria-label`을 사용합니다. `aria-label`은 필수로 강제하지 않습니다.
|
|
139
158
|
- loading progress circle은 `800ms linear infinite` 회전하며 `prefers-reduced-motion: reduce`에서는 정지합니다.
|
|
140
159
|
|
|
141
160
|
## 문서와 검증
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"specVersion": 1,
|
|
3
|
-
"packageVersion": "0.0
|
|
3
|
+
"packageVersion": "0.1.0",
|
|
4
4
|
"name": "Button",
|
|
5
5
|
"purpose": "사용자가 즉시 실행할 수 있는 행동과 탐색을 표현하는 인터랙티브 요소입니다.",
|
|
6
6
|
"usageContexts": [
|
|
@@ -8,12 +8,20 @@
|
|
|
8
8
|
"다른 페이지나 콘텐츠로 이동하는 탐색에는 neutral/link branch를 사용합니다.",
|
|
9
9
|
"brand는 화면에서 가장 중요한 행동을 강조할 때 사용합니다.",
|
|
10
10
|
"neutral은 일반적인 액션과 탐색에 사용합니다.",
|
|
11
|
-
"critical은 삭제·초기화처럼 되돌리기 어려운 행동에 사용합니다."
|
|
11
|
+
"critical은 삭제·초기화처럼 되돌리기 어려운 행동에 사용합니다.",
|
|
12
|
+
"생성·저장·관리·확인처럼 화면의 핵심 행동에는 brand/solid를 먼저 검토합니다.",
|
|
13
|
+
"검색·조회·일반적인 주요 실행에는 neutral/solid를 먼저 검토합니다.",
|
|
14
|
+
"취소·닫기·수정 같은 보조 작업에는 neutral/outline을 사용합니다.",
|
|
15
|
+
"초기화·갱신·되돌리기처럼 부담이 낮은 반복 행동에는 neutral/weak를 사용합니다.",
|
|
16
|
+
"되돌리기 어려운 삭제·초기화는 critical/weak와 확인 절차를 먼저 검토합니다.",
|
|
17
|
+
"비동기 저장·확인에는 loading을 사용해 중복 실행을 막고 라벨 공간을 유지합니다.",
|
|
18
|
+
"표 행 조작·pagination처럼 공간이 좁은 곳의 icon-only는 accessible name을 확인합니다.",
|
|
19
|
+
"icon은 생성·수정·삭제·외부 이동·다운로드처럼 행동의 목적이나 방향을 보조할 때 사용합니다.",
|
|
20
|
+
"icon-only의 접근성 이름을 제공하려면 aria-label을 지정합니다. aria-label은 필수로 강제하지 않습니다."
|
|
12
21
|
],
|
|
13
22
|
"unsupportedContexts": [
|
|
14
23
|
"문서에 없는 role·variant·상태를 className이나 inline style로 우회하지 않습니다.",
|
|
15
|
-
"상태 변경이나 일반 CTA를 neutral/link로 표현하지 않습니다."
|
|
16
|
-
"icon-only에서는 accessible name 없이 아이콘만 사용하지 않습니다."
|
|
24
|
+
"상태 변경이나 일반 CTA를 neutral/link로 표현하지 않습니다."
|
|
17
25
|
],
|
|
18
26
|
"api": {
|
|
19
27
|
"props": [
|
|
@@ -95,7 +103,7 @@
|
|
|
95
103
|
"name": "aria-label",
|
|
96
104
|
"type": "string",
|
|
97
105
|
"required": false,
|
|
98
|
-
"description": "accessible name입니다. icon-only
|
|
106
|
+
"description": "accessible name입니다. icon-only의 접근성 이름을 제공할 때 사용합니다."
|
|
99
107
|
},
|
|
100
108
|
{
|
|
101
109
|
"name": "disabled",
|
|
@@ -155,8 +163,57 @@
|
|
|
155
163
|
"iconFirst": "children과 startIcon 또는 startIconName을 제공합니다.",
|
|
156
164
|
"iconLast": "children과 endIcon 또는 endIconName을 제공합니다.",
|
|
157
165
|
"iconBoth": "children과 앞·뒤 아이콘을 모두 제공합니다.",
|
|
158
|
-
"iconOnly": "children 없이 icon 또는 iconName을
|
|
166
|
+
"iconOnly": "children 없이 icon 또는 iconName을 제공합니다. 접근성 이름이 필요하면 aria-label을 지정합니다."
|
|
159
167
|
},
|
|
168
|
+
"iconNames": [
|
|
169
|
+
"ArrowLeft",
|
|
170
|
+
"ArrowRight",
|
|
171
|
+
"Check",
|
|
172
|
+
"ChevronDown",
|
|
173
|
+
"ChevronLeft",
|
|
174
|
+
"ChevronRight",
|
|
175
|
+
"ChevronUp",
|
|
176
|
+
"ChevronsUpDown",
|
|
177
|
+
"Circle",
|
|
178
|
+
"CircleAlert",
|
|
179
|
+
"CircleCheckBig",
|
|
180
|
+
"CirclePause",
|
|
181
|
+
"CirclePlay",
|
|
182
|
+
"CircleQuestionMark",
|
|
183
|
+
"ClockAlert",
|
|
184
|
+
"Copy",
|
|
185
|
+
"Eye",
|
|
186
|
+
"EyeOff",
|
|
187
|
+
"FaceNeutral",
|
|
188
|
+
"FileDown",
|
|
189
|
+
"FilePenLine",
|
|
190
|
+
"FileX",
|
|
191
|
+
"FolderClock",
|
|
192
|
+
"Fullscreen",
|
|
193
|
+
"ImageOff",
|
|
194
|
+
"ImagePlus",
|
|
195
|
+
"Info",
|
|
196
|
+
"Key",
|
|
197
|
+
"Languages",
|
|
198
|
+
"Layers2",
|
|
199
|
+
"List",
|
|
200
|
+
"LoaderCircle",
|
|
201
|
+
"LockOpen",
|
|
202
|
+
"Mail",
|
|
203
|
+
"Megaphone",
|
|
204
|
+
"Minimize",
|
|
205
|
+
"Minus",
|
|
206
|
+
"Moon",
|
|
207
|
+
"Paperclip",
|
|
208
|
+
"Plus",
|
|
209
|
+
"RotateCw",
|
|
210
|
+
"Search",
|
|
211
|
+
"Sun",
|
|
212
|
+
"ThumbsUp",
|
|
213
|
+
"Trash",
|
|
214
|
+
"VectorSquare",
|
|
215
|
+
"X"
|
|
216
|
+
],
|
|
160
217
|
"typeExports": [
|
|
161
218
|
"ButtonProps",
|
|
162
219
|
"ButtonComponent",
|
|
@@ -171,7 +228,7 @@
|
|
|
171
228
|
"accessibility": {
|
|
172
229
|
"nativeButtonType": "native button branch에서는 type을 필수로 지정합니다.",
|
|
173
230
|
"linkHref": "neutral/link branch에서는 href를 필수로 지정합니다.",
|
|
174
|
-
"iconOnlyName": "icon-only layout에서는 비어 있지 않은 aria-label을
|
|
231
|
+
"iconOnlyName": "icon-only layout에서는 접근성 이름을 제공할 때 비어 있지 않은 aria-label을 지정합니다. aria-label은 필수가 아닙니다.",
|
|
175
232
|
"disabledLink": "비활성 link는 href를 제거하고 aria-disabled와 tabIndex를 적용합니다.",
|
|
176
233
|
"loading": "loading 중에는 aria-busy를 적용하고 상호작용을 차단합니다.",
|
|
177
234
|
"focusVisible": "keyboard focus에는 승인된 focus ring을 표시합니다."
|