@axis-ds/mcp 1.3.1 → 1.3.3

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 CHANGED
@@ -50,7 +50,7 @@ Claude Desktop 또는 기타 MCP 호환 클라이언트에서 설정:
50
50
  | 도구 | 인자 | 응답 |
51
51
  |------|------|------|
52
52
  | `axis.list_brands` | `targetDir?` | 브랜드 이름 · 설명 · 테마 · import 경로 |
53
- | `axis.get_brand` | `name`, `targetDir?` | import 경로 · Provider 예시(`defaultBrand`) · 주요 토큰(라이트 · 다크) · 보정 내역(a11y 메타) |
53
+ | `axis.get_brand` | `name`, `targetDir?` | import 경로 · 브랜드 켜기 예시(`<html data-brand>` · `setBrand`) · 주요 토큰(라이트 · 다크) · 보정 내역(a11y 메타) |
54
54
 
55
55
  - `targetDir` 를 주지 않으면 서버의 현재 디렉터리를 봅니다.
56
56
  - `@axis-ds/tokens` 가 없거나 1.3.0 미만이거나 빌드 산출물이 없으면 오류(`isError`)가 아니라 설치 · 업그레이드 안내를 돌려줍니다. 빈 목록으로 답하지 않습니다.
package/dist/index.js CHANGED
@@ -71,7 +71,7 @@ function getTokenStats() {
71
71
  }
72
72
 
73
73
  // ../axis-registry-core/src/add/add.ts
74
- import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
74
+ import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
75
75
  import path5 from "path";
76
76
  import fs3 from "fs-extra";
77
77
 
@@ -865,7 +865,7 @@ var RegistryClient = class {
865
865
  };
866
866
 
867
867
  // ../axis-registry-core/src/project.ts
868
- import { existsSync as existsSync2 } from "fs";
868
+ import { existsSync as existsSync2, statSync } from "fs";
869
869
  import path3 from "path";
870
870
  function detectInstallCommand(baseDir) {
871
871
  if (existsSync2(path3.join(baseDir, "pnpm-lock.yaml"))) return "pnpm install";
@@ -885,9 +885,22 @@ function hasDependency(pkg, name) {
885
885
  function isPlainObject(value) {
886
886
  return typeof value === "object" && value !== null && !Array.isArray(value);
887
887
  }
888
+ function addCommandFor(installCommand, deps) {
889
+ const specs = Object.entries(deps).map(([name, range]) => `"${name}@${range}"`).join(" ");
890
+ if (installCommand.startsWith("yarn")) return `yarn add ${specs}`;
891
+ if (installCommand.startsWith("npm")) return `npm install ${specs}`;
892
+ return `pnpm add ${specs}`;
893
+ }
894
+ var GLOBAL_CSS_CANDIDATES = [
895
+ "app/globals.css",
896
+ "src/app/globals.css",
897
+ "styles/globals.css",
898
+ "src/styles/globals.css",
899
+ "globals.css"
900
+ ];
888
901
 
889
902
  // ../axis-registry-core/src/template/tailwind-core.ts
890
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
903
+ import { existsSync as existsSync3, lstatSync, readdirSync, readFileSync as readFileSync3 } from "fs";
891
904
  import { join as join2, resolve } from "path";
892
905
  function hasDependency2(pkg, name) {
893
906
  for (const field of ["dependencies", "devDependencies"]) {
@@ -1293,29 +1306,229 @@ function checkTailwindSetup(baseDirInput) {
1293
1306
  message: `${label}: @import "tailwindcss" \uB97C \uD655\uC778\uD560 CSS \uD30C\uC77C\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4`
1294
1307
  };
1295
1308
  }
1296
- const [tsFile, ...otherFiles] = TAILWIND_CONFIG_FILES;
1297
- const tsPath = join2(baseDir, tsFile);
1298
- if (existsSync3(tsPath)) {
1299
- const content = readFileSync3(tsPath, "utf8");
1300
- const hasTokens = content.includes("--background") || content.includes("hsl(var(");
1301
- if (hasTokens) {
1302
- return { name: "tailwind.config", severity: "pass", message: "tailwind.config.ts\uC5D0 AXIS \uD1A0\uD070\uC774 \uC124\uC815\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4" };
1303
- }
1309
+ for (const file of TAILWIND_CONFIG_FILES) {
1310
+ const path11 = join2(baseDir, file);
1311
+ if (!existsSync3(path11)) continue;
1312
+ const content = readFileSync3(path11, "utf8");
1304
1313
  if (hasTokensPresetReference(content)) {
1305
- return { name: "tailwind.config", severity: "pass", message: `tailwind.config.ts${PRESET_PASS_SUFFIX}` };
1314
+ return { name: "tailwind.config", severity: "pass", message: `${file}${PRESET_PASS_SUFFIX}` };
1306
1315
  }
1307
- return { name: "tailwind.config", severity: "warn", message: "tailwind.config.ts\uC5D0 AXIS \uD1A0\uD070 \uC124\uC815\uC774 \uC5C6\uC2B5\uB2C8\uB2E4" };
1316
+ const ownMapping = content.includes("--background") || content.includes("hsl(var(");
1317
+ return {
1318
+ name: "tailwind.config",
1319
+ severity: "warn",
1320
+ message: ownMapping ? `${file}\uC5D0 AXIS preset(${TOKENS_TAILWIND3_PRESET})\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (\uC790\uCCB4 CSS \uBCC0\uC218 \uB9E4\uD551\uB9CC \uC788\uC74C, presets \uC5D0 \uB123\uC73C\uC138\uC694)` : `${file}\uC5D0 AXIS preset(${TOKENS_TAILWIND3_PRESET})\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (presets \uC5D0 \uB123\uC73C\uC138\uC694)`
1321
+ };
1308
1322
  }
1309
- for (const file of otherFiles) {
1310
- const path11 = join2(baseDir, file);
1311
- if (existsSync3(path11)) {
1312
- if (hasTokensPresetReference(readFileSync3(path11, "utf8"))) {
1313
- return { name: "tailwind.config", severity: "pass", message: `${file}${PRESET_PASS_SUFFIX}` };
1323
+ return { name: "tailwind.config", severity: "fail", message: "tailwind.config \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4" };
1324
+ }
1325
+ function readAxisConfigLike(baseDir) {
1326
+ try {
1327
+ const parsed = JSON.parse(readFileSync3(join2(baseDir, "axis.config.json"), "utf8"));
1328
+ return isPlainObject2(parsed) ? parsed : void 0;
1329
+ } catch {
1330
+ return void 0;
1331
+ }
1332
+ }
1333
+ function contentGlobToRegExp(glob) {
1334
+ const g = glob.trim().replace(/^\.\//, "");
1335
+ let re = "";
1336
+ for (let i = 0; i < g.length; i++) {
1337
+ const c = g[i];
1338
+ if (c === "*") {
1339
+ if (g[i + 1] === "*") {
1340
+ const slash = g[i + 2] === "/";
1341
+ re += slash ? "(?:.*/)?" : ".*";
1342
+ i += slash ? 2 : 1;
1343
+ } else re += "[^/]*";
1344
+ } else if (c === "?") re += "[^/]";
1345
+ else if (c === "{") {
1346
+ const end = g.indexOf("}", i);
1347
+ if (end === -1) {
1348
+ re += "\\{";
1349
+ continue;
1350
+ }
1351
+ re += `(?:${g.slice(i + 1, end).split(",").map((part) => part.replace(/[.+^$()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")).join("|")})`;
1352
+ i = end;
1353
+ } else if (c === "[") {
1354
+ const end = g.indexOf("]", i);
1355
+ if (end === -1) re += "\\[";
1356
+ else {
1357
+ re += g.slice(i, end + 1);
1358
+ i = end;
1314
1359
  }
1315
- return { name: "tailwind.config", severity: "warn", message: `${file} \uC874\uC7AC (TypeScript \uAD8C\uC7A5)` };
1360
+ } else re += c.replace(/[.+^$()|\\]/g, "\\$&");
1361
+ }
1362
+ return new RegExp(`^${re}$`);
1363
+ }
1364
+ function balancedSlice(src, start) {
1365
+ const open = src[start];
1366
+ const close = open === "[" ? "]" : "}";
1367
+ let depth = 0;
1368
+ for (let i = start; i < src.length; i++) {
1369
+ const c = src[i];
1370
+ if (c === '"' || c === "'" || c === "`") {
1371
+ let j = i + 1;
1372
+ while (j < src.length && src[j] !== c) j += src[j] === "\\" ? 2 : 1;
1373
+ i = j;
1374
+ continue;
1316
1375
  }
1376
+ if (c === open) depth++;
1377
+ else if (c === close && --depth === 0) return src.slice(start, i + 1);
1317
1378
  }
1318
- return { name: "tailwind.config", severity: "fail", message: "tailwind.config \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4" };
1379
+ return void 0;
1380
+ }
1381
+ function stringLiterals(src) {
1382
+ return [...src.matchAll(/(["'`])((?:\\.|(?!\1)[^\\])*)\1/g)].map((m) => m[2]).filter((s) => !s.includes("${"));
1383
+ }
1384
+ function readTailwindContent(source) {
1385
+ const src = stripJsComments(source);
1386
+ const key = /\bcontent\s*:\s*/g;
1387
+ for (const m of src.matchAll(key)) {
1388
+ let at = (m.index ?? 0) + m[0].length;
1389
+ let block = src[at] === "[" || src[at] === "{" ? balancedSlice(src, at) : void 0;
1390
+ if (block === void 0) continue;
1391
+ if (block.startsWith("{")) {
1392
+ const files = /\bfiles\s*:\s*\[/.exec(block);
1393
+ if (!files) return { globs: [], dynamic: true };
1394
+ at = files.index + files[0].length - 1;
1395
+ block = balancedSlice(block, at);
1396
+ if (block === void 0) return { globs: [], dynamic: true };
1397
+ }
1398
+ const inner = block.slice(1, -1);
1399
+ const globs = stringLiterals(inner);
1400
+ const rest = inner.replace(/(["'`])((?:\\.|(?!\1)[^\\])*)\1/g, "").replace(/[\s,]/g, "");
1401
+ return { globs, dynamic: rest !== "" };
1402
+ }
1403
+ return void 0;
1404
+ }
1405
+ function normalizeRel(p) {
1406
+ const s = p.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
1407
+ if (s === "" || s.startsWith("../") || s.startsWith("/") || /^[A-Za-z]:/.test(s)) return void 0;
1408
+ return s;
1409
+ }
1410
+ function inspectTailwindContent(baseDirInput) {
1411
+ const baseDir = resolve(baseDirInput);
1412
+ const pkg = readPackageJson(baseDir);
1413
+ const tailwind = detectTailwind(baseDir, pkg);
1414
+ if (tailwind.major !== void 0 && tailwind.major >= 4) return null;
1415
+ const configFile = TAILWIND_CONFIG_FILES.find((f) => existsSync3(join2(baseDir, f)));
1416
+ if (configFile === void 0) return null;
1417
+ const targets = [];
1418
+ const componentsDir = readAxisConfigLike(baseDir)?.componentsDir;
1419
+ const rel2 = typeof componentsDir === "string" ? normalizeRel(componentsDir) : void 0;
1420
+ if (rel2 !== void 0) {
1421
+ targets.push({ label: `componentsDir(${componentsDir})`, sample: `${rel2}/button.tsx`, suggest: `./${rel2}/**/*.{ts,tsx}` });
1422
+ }
1423
+ for (const name of TAILWIND4_COMPONENT_PACKAGES) {
1424
+ if (pkg !== void 0 && hasDependency2(pkg, name) !== void 0) {
1425
+ targets.push({ label: name, sample: `node_modules/${name}/dist/index.js`, suggest: `./node_modules/${name}/dist/**/*.{js,mjs}` });
1426
+ }
1427
+ }
1428
+ if (targets.length === 0) return null;
1429
+ const read = readTailwindContent(readFileSync3(join2(baseDir, configFile), "utf8"));
1430
+ const patterns = (read?.globs ?? []).filter((g) => !g.trim().startsWith("!")).map(contentGlobToRegExp);
1431
+ return {
1432
+ configFile,
1433
+ unreadable: read === void 0,
1434
+ dynamic: read?.dynamic ?? false,
1435
+ targets: targets.map((t) => ({ ...t, covered: patterns.some((re) => re.test(t.sample)) }))
1436
+ };
1437
+ }
1438
+ function checkTailwindContent(baseDir) {
1439
+ const r = inspectTailwindContent(baseDir);
1440
+ if (r === null) return null;
1441
+ const missing = r.targets.filter((t) => !t.covered);
1442
+ if (missing.length === 0) {
1443
+ return {
1444
+ name: "tailwind.content",
1445
+ severity: "pass",
1446
+ message: `${r.configFile}\uC758 content \uAC00 ${r.targets.map((t) => t.label).join(" \xB7 ")} \uB97C \uB36E\uC2B5\uB2C8\uB2E4`
1447
+ };
1448
+ }
1449
+ const list = missing.map((t) => `${t.label} \u2192 "${t.suggest}"`).join(" \xB7 ");
1450
+ if (r.unreadable || r.dynamic) {
1451
+ return {
1452
+ name: "tailwind.content",
1453
+ severity: "warn",
1454
+ message: `${r.configFile}\uC758 content \uB97C \uC815\uC801\uC73C\uB85C \uB2E4 \uC77D\uC9C0 \uBABB\uD574 \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4: ${list}`
1455
+ };
1456
+ }
1457
+ return {
1458
+ name: "tailwind.content",
1459
+ severity: "fail",
1460
+ message: `${r.configFile}\uC758 content \uAC00 \uCEF4\uD3EC\uB10C\uD2B8 \uACBD\uB85C\uB97C \uB36E\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4(\uD074\uB798\uC2A4\uAC00 \uC0DD\uC131\uB418\uC9C0 \uC54A\uC74C): ${list}`
1461
+ };
1462
+ }
1463
+ var ANIMATION_PLUGIN_TAILWIND3 = "tailwindcss-animate";
1464
+ var ANIMATION_PLUGIN_TAILWIND4 = "tw-animate-css";
1465
+ var ANIMATION_SCAN_ENTRY_LIMIT = 2e3;
1466
+ var ANIMATION_SCAN_DEPTH_LIMIT = 8;
1467
+ var ANIMATION_SCAN_FILE_BYTES = 512 * 1024;
1468
+ function componentsUseAnimation(baseDir, componentsDir) {
1469
+ const rel2 = normalizeRel(componentsDir);
1470
+ if (rel2 === void 0) return false;
1471
+ const stack = [[join2(baseDir, rel2), 0]];
1472
+ let visited = 0;
1473
+ while (stack.length > 0) {
1474
+ const [dir, depth] = stack.pop();
1475
+ let names;
1476
+ try {
1477
+ names = readdirSync(dir);
1478
+ } catch {
1479
+ continue;
1480
+ }
1481
+ for (const name of names) {
1482
+ if (++visited > ANIMATION_SCAN_ENTRY_LIMIT) return false;
1483
+ const p = join2(dir, name);
1484
+ let st;
1485
+ try {
1486
+ st = lstatSync(p);
1487
+ } catch {
1488
+ continue;
1489
+ }
1490
+ if (st.isSymbolicLink()) continue;
1491
+ if (st.isDirectory()) {
1492
+ if (name !== "node_modules" && depth < ANIMATION_SCAN_DEPTH_LIMIT) stack.push([p, depth + 1]);
1493
+ } else if (st.isFile() && st.size <= ANIMATION_SCAN_FILE_BYTES && /\.(tsx|ts|jsx|js)$/.test(name)) {
1494
+ if (/\banimate-(?:in|out)\b/.test(readFileSync3(p, "utf8"))) return true;
1495
+ }
1496
+ }
1497
+ }
1498
+ return false;
1499
+ }
1500
+ function checkAnimationPlugin(baseDirInput) {
1501
+ const baseDir = resolve(baseDirInput);
1502
+ const pkg = readPackageJson(baseDir);
1503
+ const config = readAxisConfigLike(baseDir);
1504
+ const declared = pkg !== void 0 && hasDependency2(pkg, "@axis-ds/ui-react") !== void 0;
1505
+ const copied = typeof config?.componentsDir === "string" && componentsUseAnimation(baseDir, config.componentsDir);
1506
+ if (!declared && !copied) return null;
1507
+ const by = declared ? "@axis-ds/ui-react \uC120\uC5B8" : "componentsDir \uC758 animate-in \uB958 \uD074\uB798\uC2A4";
1508
+ const tailwind = detectTailwind(baseDir, pkg);
1509
+ if (tailwind.major !== void 0 && tailwind.major >= 4) {
1510
+ const file = TAILWIND_CSS_CANDIDATES.find((c) => existsSync3(join2(baseDir, c)));
1511
+ const css = file !== void 0 ? stripCssComments(readFileSync3(join2(baseDir, file), "utf8")) : "";
1512
+ const ok = new RegExp(`@import\\s+(?:url\\(\\s*)?["']${ANIMATION_PLUGIN_TAILWIND4}["']`).test(css) || new RegExp(`@plugin\\s+["']${ANIMATION_PLUGIN_TAILWIND3}["']`).test(css);
1513
+ return ok ? { name: "animation", severity: "pass", message: `${file}\uC5D0 \uC560\uB2C8\uBA54\uC774\uC158 \uD50C\uB7EC\uADF8\uC778\uC774 \uC788\uC2B5\uB2C8\uB2E4` } : {
1514
+ name: "animation",
1515
+ severity: "warn",
1516
+ message: `\uC560\uB2C8\uBA54\uC774\uC158 \uD50C\uB7EC\uADF8\uC778\uC774 \uC5C6\uC2B5\uB2C8\uB2E4(${by}): Dialog \xB7 Select \uB4F1\uC758 \uC5F4\uACE0 \uB2EB\uB294 \uC560\uB2C8\uBA54\uC774\uC158\uC774 \uB3D9\uC791\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2192 ${ANIMATION_PLUGIN_TAILWIND4} \uC124\uCE58 \uB4A4 @import "${ANIMATION_PLUGIN_TAILWIND4}";`
1517
+ };
1518
+ }
1519
+ const configFile = TAILWIND_CONFIG_FILES.find((f) => existsSync3(join2(baseDir, f)));
1520
+ const source = configFile !== void 0 ? stripJsComments(readFileSync3(join2(baseDir, configFile), "utf8")) : "";
1521
+ const pluginRef = new RegExp(`["']${ANIMATION_PLUGIN_TAILWIND3}["']`).test(source);
1522
+ const pluginDeclared = pkg !== void 0 && hasDependency2(pkg, ANIMATION_PLUGIN_TAILWIND3) !== void 0;
1523
+ if (pluginRef && pluginDeclared) {
1524
+ return { name: "animation", severity: "pass", message: `${configFile}\uC5D0 ${ANIMATION_PLUGIN_TAILWIND3} \uD50C\uB7EC\uADF8\uC778\uC774 \uC788\uC2B5\uB2C8\uB2E4` };
1525
+ }
1526
+ const why = !pluginDeclared ? `${ANIMATION_PLUGIN_TAILWIND3} \uBBF8\uC124\uCE58` : `${configFile ?? "tailwind.config"} plugins \uC5D0 \uC5C6\uC74C`;
1527
+ return {
1528
+ name: "animation",
1529
+ severity: "warn",
1530
+ message: `\uC560\uB2C8\uBA54\uC774\uC158 \uD50C\uB7EC\uADF8\uC778\uC774 \uC5C6\uC2B5\uB2C8\uB2E4(${by}, ${why}): Dialog \xB7 Select \uB4F1\uC758 \uC5F4\uACE0 \uB2EB\uB294 \uC560\uB2C8\uBA54\uC774\uC158\uC774 \uB3D9\uC791\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2192 ${ANIMATION_PLUGIN_TAILWIND3} \uC124\uCE58 \uB4A4 plugins \uC5D0 require("${ANIMATION_PLUGIN_TAILWIND3}")`
1531
+ };
1319
1532
  }
1320
1533
 
1321
1534
  // ../axis-registry-core/src/utils-deps.ts
@@ -1332,7 +1545,7 @@ function selectUtilsDependencies(tailwind) {
1332
1545
  }
1333
1546
 
1334
1547
  // ../axis-registry-core/src/add/safe-write.ts
1335
- import { constants as fsConstants, existsSync as existsSync4, lstatSync, realpathSync, statSync } from "fs";
1548
+ import { constants as fsConstants, existsSync as existsSync4, lstatSync as lstatSync2, realpathSync, statSync as statSync2 } from "fs";
1336
1549
  import path4 from "path";
1337
1550
  import fs2 from "fs-extra";
1338
1551
  var NEW_FILE_FLAGS = fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | (fsConstants.O_NOFOLLOW ?? 0);
@@ -1370,7 +1583,7 @@ function escapeReason(root, target, options = {}) {
1370
1583
  }
1371
1584
  let st = null;
1372
1585
  try {
1373
- st = lstatSync(target);
1586
+ st = lstatSync2(target);
1374
1587
  } catch {
1375
1588
  st = null;
1376
1589
  }
@@ -1388,7 +1601,7 @@ function escapeReason(root, target, options = {}) {
1388
1601
  }
1389
1602
  let followed;
1390
1603
  try {
1391
- followed = statSync(realTarget);
1604
+ followed = statSync2(realTarget);
1392
1605
  } catch {
1393
1606
  return `\uAE30\uC874 \uD30C\uC77C \uC815\uBCF4\uB97C \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4(${target})`;
1394
1607
  }
@@ -1404,7 +1617,7 @@ function escapeReason(root, target, options = {}) {
1404
1617
  }
1405
1618
  function isSymlink(p) {
1406
1619
  try {
1407
- return lstatSync(p).isSymbolicLink();
1620
+ return lstatSync2(p).isSymbolicLink();
1408
1621
  } catch {
1409
1622
  return false;
1410
1623
  }
@@ -1420,7 +1633,7 @@ function outsideLinkTarget(root, target) {
1420
1633
  }
1421
1634
  function hardLinkCount(target) {
1422
1635
  try {
1423
- const st = lstatSync(target);
1636
+ const st = lstatSync2(target);
1424
1637
  return st.isFile() && st.nlink >= 2 ? st.nlink : null;
1425
1638
  } catch {
1426
1639
  return null;
@@ -1660,7 +1873,7 @@ function relativeSpecifierResolves(fromFile, spec) {
1660
1873
  const base = path5.resolve(path5.dirname(fromFile), spec);
1661
1874
  return RESOLVE_EXTS.some((ext) => {
1662
1875
  try {
1663
- return statSync2(base + ext).isFile();
1876
+ return statSync3(base + ext).isFile();
1664
1877
  } catch {
1665
1878
  return false;
1666
1879
  }
@@ -1735,7 +1948,7 @@ function missingDependencies(root, items) {
1735
1948
  const installCommand = detectInstallCommand(root);
1736
1949
  let pkg;
1737
1950
  try {
1738
- const text = readFileSync4(path5.join(root, "package.json"), "utf8").replace(/^/, "");
1951
+ const text = readFileSync4(path5.join(root, "package.json"), "utf8").replace(/^\uFEFF/, "");
1739
1952
  const parsed = JSON.parse(text);
1740
1953
  pkg = isPlainObject(parsed) ? parsed : void 0;
1741
1954
  } catch {
@@ -2417,7 +2630,7 @@ function formatTokenList(result) {
2417
2630
  }
2418
2631
 
2419
2632
  // src/tools/install.ts
2420
- import { statSync as statSync3 } from "fs";
2633
+ import { statSync as statSync4 } from "fs";
2421
2634
  import path6 from "path";
2422
2635
  var DEFAULT_LAYOUT_CONFIG = { componentsDir: "components/ui" };
2423
2636
  var MCP_ADD_HINTS = {
@@ -2456,7 +2669,7 @@ async function handleInstallComponent(params, deps = {}) {
2456
2669
  const root = path6.resolve(params.targetDir);
2457
2670
  let isDir = false;
2458
2671
  try {
2459
- isDir = statSync3(root).isDirectory();
2672
+ isDir = statSync4(root).isDirectory();
2460
2673
  } catch {
2461
2674
  isDir = false;
2462
2675
  }
@@ -2570,7 +2783,7 @@ var REUSABLE_PATTERNS = {
2570
2783
  };
2571
2784
  var PROJECT_SPECIFIC_PATTERNS = [
2572
2785
  // 파일 경로
2573
- /(?:src|packages|apps|lib|components)\/[\w\-\/]+\.(?:ts|tsx|js|jsx|md)/gi,
2786
+ /(?:src|packages|apps|lib|components)\/[\w\-/]+\.(?:ts|tsx|js|jsx|md)/gi,
2574
2787
  // 컴포넌트명 (PascalCase)
2575
2788
  /\b[A-Z][a-z]+(?:[A-Z][a-z]+)+\b/g,
2576
2789
  // 버전
@@ -2584,7 +2797,7 @@ function segmentText(text) {
2584
2797
  const sections = text.split(/(?=^#{1,3}\s)/m);
2585
2798
  return sections.filter((s) => s.trim().length > 50);
2586
2799
  }
2587
- function calculateRepeatability(text, _context) {
2800
+ function calculateRepeatability(text) {
2588
2801
  let score = 0;
2589
2802
  const structuredPatterns = [
2590
2803
  REUSABLE_PATTERNS.goal,
@@ -2597,7 +2810,7 @@ function calculateRepeatability(text, _context) {
2597
2810
  }
2598
2811
  }
2599
2812
  if (/^#{1,3}\s/m.test(text)) score += 5;
2600
- if (/^[\-\*]\s/m.test(text)) score += 3;
2813
+ if (/^[-*]\s/m.test(text)) score += 3;
2601
2814
  if (/^\d+\.\s/m.test(text)) score += 2;
2602
2815
  return Math.min(25, score);
2603
2816
  }
@@ -2648,8 +2861,8 @@ function calculateClarity(text) {
2648
2861
  }
2649
2862
  return Math.min(20, score);
2650
2863
  }
2651
- function calculateScores(text, context) {
2652
- const repeatability = calculateRepeatability(text, context);
2864
+ function calculateScores(text) {
2865
+ const repeatability = calculateRepeatability(text);
2653
2866
  const generality = calculateGenerality(text);
2654
2867
  const independence = calculateIndependence(text);
2655
2868
  const clarity = calculateClarity(text);
@@ -2752,7 +2965,7 @@ function handleDetect(params) {
2752
2965
  const candidates = [];
2753
2966
  for (let i = 0; i < segments.length; i++) {
2754
2967
  const segment = segments[i];
2755
- const scores = calculateScores(segment, context);
2968
+ const scores = calculateScores(segment);
2756
2969
  if (scores.total >= minScore) {
2757
2970
  candidates.push({
2758
2971
  id: `candidate-${i + 1}`,
@@ -2882,7 +3095,7 @@ var CATEGORY_KEYWORDS = {
2882
3095
  ]
2883
3096
  };
2884
3097
  var PROJECT_PATTERNS = {
2885
- path: /(?:src|packages|apps|lib|components)\/[\w\-\/]+\.(?:ts|tsx|js|jsx|md)/gi,
3098
+ path: /(?:src|packages|apps|lib|components)\/[\w\-/]+\.(?:ts|tsx|js|jsx|md)/gi,
2886
3099
  name: /\b[A-Z][a-z]+(?:[A-Z][a-z]+)+\b/g,
2887
3100
  version: /\b\d+\.\d+\.\d+(?:-[\w.]+)?\b/g,
2888
3101
  date: /\b\d{4}-\d{2}-\d{2}\b/g,
@@ -2899,7 +3112,7 @@ function calculateRepeatability2(text) {
2899
3112
  score += 4;
2900
3113
  reasons.push(`${headerCount}\uAC1C \uC139\uC158 \uC874\uC7AC`);
2901
3114
  }
2902
- const listCount = (text.match(/^[\-\*]\s/gm) || []).length;
3115
+ const listCount = (text.match(/^[-*]\s/gm) || []).length;
2903
3116
  const numberedCount = (text.match(/^\d+\.\s/gm) || []).length;
2904
3117
  if (listCount + numberedCount >= 5) {
2905
3118
  score += 8;
@@ -3229,7 +3442,7 @@ function formatAnalyzeResult(result) {
3229
3442
  // src/tools/prompt/refine.ts
3230
3443
  var VARIABLE_PATTERNS = [
3231
3444
  {
3232
- pattern: /(?:src|packages|apps|lib|components)\/[\w\-\/]+\.(?:ts|tsx|js|jsx|md)/gi,
3445
+ pattern: /(?:src|packages|apps|lib|components)\/[\w\-/]+\.(?:ts|tsx|js|jsx|md)/gi,
3233
3446
  variable: "{{FILE_PATH}}",
3234
3447
  description: "\uD30C\uC77C \uACBD\uB85C",
3235
3448
  type: "path"
@@ -3554,7 +3767,7 @@ function checkRequiredSections(text) {
3554
3767
  function checkHardcodedPaths(text) {
3555
3768
  const hardcodedPatterns = [
3556
3769
  // 절대 경로 (변수화되지 않은)
3557
- /(?<!{{)(?:src|packages|apps|lib|components)\/[\w\-\/]+\.(?:ts|tsx|js|jsx|md)(?!}})/gi,
3770
+ /(?<!{{)(?:src|packages|apps|lib|components)\/[\w\-/]+\.(?:ts|tsx|js|jsx|md)(?!}})/gi,
3558
3771
  // 특정 프로젝트 경로
3559
3772
  /D:\\|C:\\|\/Users\/|\/home\//gi
3560
3773
  ];
@@ -3586,7 +3799,7 @@ function checkVariablesDocumented(text) {
3586
3799
  undocumented
3587
3800
  };
3588
3801
  }
3589
- function checkDuplicate(text, _basePath) {
3802
+ function checkDuplicate(text) {
3590
3803
  const similar = [];
3591
3804
  try {
3592
3805
  const indexPath = path7.resolve(process.cwd(), INDEX_PATH);
@@ -3832,7 +4045,7 @@ function savePromptFile(basePath, category, name, content) {
3832
4045
  fs5.writeFileSync(promptPath, content, "utf-8");
3833
4046
  return `${PROMPTS_BASE_PATH}/${category}/${name}.md`;
3834
4047
  }
3835
- function updateIndex(index, entry, isNew) {
4048
+ function updateIndex(index, entry) {
3836
4049
  const now = (/* @__PURE__ */ new Date()).toISOString();
3837
4050
  const existingIndex = index.prompts.findIndex((p) => p.id === entry.id);
3838
4051
  if (existingIndex >= 0) {
@@ -3900,8 +4113,7 @@ function handleSave(params) {
3900
4113
  description,
3901
4114
  path: relativePath,
3902
4115
  variables
3903
- },
3904
- !isDuplicate
4116
+ }
3905
4117
  );
3906
4118
  saveIndex(basePath, index);
3907
4119
  return {
@@ -3962,11 +4174,12 @@ import fs6 from "fs-extra";
3962
4174
  import path9 from "path";
3963
4175
 
3964
4176
  // src/tools/brand-core.ts
3965
- import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
4177
+ import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
3966
4178
  import { createRequire } from "module";
3967
4179
  import { dirname as dirname3, join as join4, relative, resolve as resolve3, sep } from "path";
3968
4180
  var TOKENS_PACKAGE = "@axis-ds/tokens";
3969
4181
  var MIN_BRAND_TOKENS_VERSION = "1.3.0";
4182
+ var TOKENS_DEPENDENCY_MIN_VERSION = "1.6.0";
3970
4183
  var KEY_TOKENS = [
3971
4184
  "--primary",
3972
4185
  "--primary-foreground",
@@ -4022,10 +4235,9 @@ function resolveTokens(baseDir) {
4022
4235
  function brandImportPath(entry) {
4023
4236
  return tokensSpecifier(entry.css);
4024
4237
  }
4025
- var GLOBAL_CSS_CANDIDATES = ["app/globals.css", "src/app/globals.css", "globals.css"];
4026
4238
  function isFile(file) {
4027
4239
  try {
4028
- return statSync4(file).isFile();
4240
+ return statSync5(file).isFile();
4029
4241
  } catch {
4030
4242
  return false;
4031
4243
  }
@@ -4165,11 +4377,62 @@ function pickKeyTokens(decls) {
4165
4377
  }
4166
4378
  function providerExample(name) {
4167
4379
  return [
4168
- 'import { ThemeProvider } from "@axis-ds/theme";',
4380
+ "// 1) Provider \uC5C6\uC774: \uB8E8\uD2B8 \uB808\uC774\uC544\uC6C3(app/layout.tsx)\uC758 <html> \uC5D0 \uBD99\uC785\uB2C8\uB2E4(\uCCAB \uD654\uBA74\uBD80\uD130 \uC801\uC6A9)",
4381
+ `<html lang="ko" data-brand="${name}">`,
4169
4382
  "",
4170
- `<ThemeProvider defaultBrand="${name}">{children}</ThemeProvider>`
4383
+ "// 2) ThemeProvider \uB97C \uC4F0\uBA74 \uB9C8\uC6B4\uD2B8 \uB54C \uBE0C\uB79C\uB4DC\uAC00 \uC5C6\uC73C\uBA74 \uC18D\uC131\uC744 \uC9C0\uC6B0\uBBC0\uB85C setBrand \uB85C \uCF2D\uB2C8\uB2E4(\uC800\uC7A5\uB3FC \uC720\uC9C0, setBrand(undefined) \uB85C \uB054)",
4384
+ 'import { useTheme } from "@axis-ds/theme";',
4385
+ "const { setBrand } = useTheme();",
4386
+ `setBrand("${name}");`
4171
4387
  ].join("\n");
4172
4388
  }
4389
+ var BRAND_ACTIVATION_FILES = [
4390
+ "app/layout.tsx",
4391
+ "app/layout.jsx",
4392
+ "app/layout.js",
4393
+ "src/app/layout.tsx",
4394
+ "src/app/layout.jsx",
4395
+ "src/app/layout.js",
4396
+ "pages/_app.tsx",
4397
+ "pages/_app.jsx",
4398
+ "pages/_document.tsx",
4399
+ "pages/_document.jsx",
4400
+ "src/pages/_app.tsx",
4401
+ "src/pages/_app.jsx",
4402
+ "src/pages/_document.tsx",
4403
+ "src/pages/_document.jsx"
4404
+ ];
4405
+ var BRAND_ACTIVATION_PATTERN = /data-brand|setBrand\s*\(|defaultBrand\s*=/;
4406
+ function stripJsLikeComments(src) {
4407
+ return src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'`])\/\/[^\n]*/g, "$1");
4408
+ }
4409
+ function findBrandActivation(baseDir) {
4410
+ const root = resolve3(baseDir);
4411
+ const layoutFiles = [];
4412
+ for (const candidate of BRAND_ACTIVATION_FILES) {
4413
+ const file = join4(root, candidate);
4414
+ if (!isFile(file)) continue;
4415
+ layoutFiles.push(candidate);
4416
+ const text = stripJsLikeComments(readFileSync7(file, "utf8"));
4417
+ if (BRAND_ACTIVATION_PATTERN.test(text)) return { found: candidate, layoutFiles };
4418
+ for (const m of text.matchAll(/from\s+["']\.\/(providers?)["']/g)) {
4419
+ for (const ext of [".tsx", ".jsx", ".ts", ".js"]) {
4420
+ const rel2 = `${dirname3(candidate)}/${m[1]}${ext}`;
4421
+ const p = join4(root, rel2);
4422
+ if (isFile(p) && BRAND_ACTIVATION_PATTERN.test(stripJsLikeComments(readFileSync7(p, "utf8")))) {
4423
+ return { found: rel2, layoutFiles };
4424
+ }
4425
+ }
4426
+ }
4427
+ }
4428
+ return { layoutFiles };
4429
+ }
4430
+ function inspectBrandActivation(baseDir) {
4431
+ const config = readBrandConfig(baseDir);
4432
+ const brand = config?.brand;
4433
+ if (typeof brand !== "string" || brand.trim() === "") return null;
4434
+ return { brand, ...findBrandActivation(baseDir) };
4435
+ }
4173
4436
  function getBrand(baseDir, name) {
4174
4437
  const tokens = resolveTokens(baseDir);
4175
4438
  if (tokens.status !== "ok" || !tokens.indexPath) {
@@ -4358,6 +4621,21 @@ function brandCheckResult(check) {
4358
4621
  };
4359
4622
  }
4360
4623
  }
4624
+ function brandActivationResult(check) {
4625
+ if (check.found) {
4626
+ return {
4627
+ name: "brand.activation",
4628
+ severity: "pass",
4629
+ message: `${check.found}\uC5D0 \uBE0C\uB79C\uB4DC\uB97C \uCF1C\uB294 \uCF54\uB4DC(data-brand \xB7 setBrand \uB4F1)\uAC00 \uC788\uC2B5\uB2C8\uB2E4`
4630
+ };
4631
+ }
4632
+ const where = check.layoutFiles.length > 0 ? check.layoutFiles.join(" \xB7 ") : "\uB8E8\uD2B8 \uB808\uC774\uC544\uC6C3(app/layout.tsx \uB4F1)";
4633
+ return {
4634
+ name: "brand.activation",
4635
+ severity: "warn",
4636
+ message: `brand "${check.brand}" \uAC00 \uC124\uC815\uB410\uC9C0\uB9CC ${where}\uC5D0 \uBE0C\uB79C\uB4DC\uB97C \uCF1C\uB294 \uCF54\uB4DC(data-brand \xB7 setBrand \uB4F1)\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4(ThemeProvider \uB9CC\uC73C\uB85C\uB294 \uB9C8\uC6B4\uD2B8 \uB54C \uAEBC\uC9D0) \u2192 <html data-brand="${check.brand}"> \uB610\uB294 setBrand("${check.brand}")`
4637
+ };
4638
+ }
4361
4639
  async function handleCheckProject(params) {
4362
4640
  const baseDir = path9.resolve(params.targetDir || ".");
4363
4641
  const results = [];
@@ -4380,8 +4658,8 @@ async function handleCheckProject(params) {
4380
4658
  for (const candidate of utilsCandidates) {
4381
4659
  const fullPath = path9.join(baseDir, candidate);
4382
4660
  if (await fs6.pathExists(fullPath)) {
4383
- const content = await fs6.readFile(fullPath, "utf-8");
4384
- const hasCn = content.includes("function cn");
4661
+ const content2 = await fs6.readFile(fullPath, "utf-8");
4662
+ const hasCn = content2.includes("function cn");
4385
4663
  results.push({
4386
4664
  name: "utils.ts",
4387
4665
  severity: hasCn ? "pass" : "warn",
@@ -4434,6 +4712,12 @@ async function handleCheckProject(params) {
4434
4712
  }
4435
4713
  const brandCheck = inspectBrandImport(baseDir);
4436
4714
  if (brandCheck) results.push(brandCheckResult(brandCheck));
4715
+ const activation = inspectBrandActivation(baseDir);
4716
+ if (activation) results.push(brandActivationResult(activation));
4717
+ const content = checkTailwindContent(baseDir);
4718
+ if (content) results.push(content);
4719
+ const animation = checkAnimationPlugin(baseDir);
4720
+ if (animation) results.push(animation);
4437
4721
  const summary = {
4438
4722
  pass: results.filter((r) => r.severity === "pass").length,
4439
4723
  warn: results.filter((r) => r.severity === "warn").length,
@@ -4550,7 +4834,7 @@ function formatCheckProjectResult(result) {
4550
4834
 
4551
4835
  // src/tools/brand.ts
4552
4836
  import path10 from "path";
4553
- var TOKENS_INSTALL_COMMAND = `pnpm add ${TOKENS_PACKAGE}@^${MIN_BRAND_TOKENS_VERSION}`;
4837
+ var TOKENS_INSTALL_COMMAND = addCommandFor("pnpm install", { [TOKENS_PACKAGE]: `^${TOKENS_DEPENDENCY_MIN_VERSION}` });
4554
4838
  function baseDirOf(targetDir) {
4555
4839
  return path10.resolve(targetDir || process.cwd());
4556
4840
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/message-bubble.tsx",
15
15
  "type": "registry:ui",
16
- "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { MESSAGE_ROLE_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type MessageRole = \"user\" | \"assistant\" | \"system\";\nexport type MessageStatus = \"sending\" | \"sent\" | \"error\";\n\nexport interface MessageBubbleProps {\n /** 메시지 역할 */\n role: MessageRole;\n /** 메시지 내용 */\n content: React.ReactNode;\n /** 타임스탬프 */\n timestamp?: Date;\n /** 아바타 (ReactNode 또는 이미지 URL) */\n avatar?: React.ReactNode | string;\n /** 전송 상태 */\n status?: MessageStatus;\n /** 하단 액션 영역 */\n actions?: React.ReactNode;\n /** 메타데이터 (모델명 등) */\n metadata?: string;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst roleStyles: Record<MessageRole, { container: string; bubble: string }> = {\n user: {\n container: \"flex-row-reverse\",\n bubble: \"bg-[var(--axis-agent-message-user-bg)] text-[var(--axis-agent-message-user-text)] rounded-br-sm\",\n },\n assistant: {\n container: \"flex-row\",\n bubble: \"bg-[var(--axis-surface-secondary)] text-[var(--axis-text-primary)] rounded-bl-sm\",\n },\n system: {\n container: \"justify-center\",\n bubble: \"bg-[var(--axis-surface-tertiary)] text-[var(--axis-text-secondary)] text-xs italic\",\n },\n};\n\nconst statusIcons: Record<MessageStatus, React.ReactNode> = {\n sending: (\n <svg className=\"w-3 h-3 animate-spin\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n ),\n sent: (\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ),\n error: (\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 8v4m0 4h.01\" />\n </svg>\n ),\n};\n\n/**\n * 대화 메시지 버블 컴포넌트\n */\nexport function MessageBubble({\n role,\n content,\n timestamp,\n avatar,\n status,\n actions,\n metadata,\n className,\n}: MessageBubbleProps) {\n const roleLabel = MESSAGE_ROLE_LABELS[role];\n\n const renderAvatar = () => {\n if (!avatar) return null;\n if (typeof avatar === \"string\") {\n return (\n <img\n src={avatar}\n alt=\"\"\n aria-hidden=\"true\"\n className=\"w-8 h-8 rounded-full object-cover flex-shrink-0\"\n />\n );\n }\n return <div className=\"flex-shrink-0\">{avatar}</div>;\n };\n\n return (\n <div\n role=\"article\"\n aria-label={`${roleLabel} 메시지`}\n className={cn(\"flex items-start gap-2\", roleStyles[role].container, className)}\n >\n {role !== \"system\" && renderAvatar()}\n <div className={cn(\"max-w-[80%]\", role === \"system\" && \"max-w-full\")}>\n <div\n className={cn(\n \"rounded-lg px-3 py-2\",\n roleStyles[role].bubble\n )}\n >\n <div className=\"text-sm whitespace-pre-wrap\">{content}</div>\n </div>\n <div className=\"flex items-center gap-2 mt-1 px-1\">\n {timestamp && (\n <time\n dateTime={timestamp.toISOString()}\n className=\"text-xs text-[var(--axis-text-tertiary)]\"\n >\n {timestamp.toLocaleTimeString(\"ko-KR\", { hour: \"2-digit\", minute: \"2-digit\" })}\n </time>\n )}\n {metadata && (\n <span className=\"text-xs text-[var(--axis-text-tertiary)]\">{metadata}</span>\n )}\n {status && (\n <span\n className={cn(\n \"text-[var(--axis-text-tertiary)]\",\n status === \"error\" && \"text-[var(--axis-text-error)]\"\n )}\n aria-label={status === \"sending\" ? \"전송 중\" : status === \"sent\" ? \"전송됨\" : \"전송 실패\"}\n >\n {statusIcons[status]}\n </span>\n )}\n </div>\n {actions && <div className=\"mt-1\">{actions}</div>}\n </div>\n </div>\n );\n}\n"
16
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { MESSAGE_ROLE_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type MessageRole = \"user\" | \"assistant\" | \"system\";\nexport type MessageStatus = \"sending\" | \"sent\" | \"error\";\n\nexport interface MessageBubbleProps {\n /** 메시지 역할 */\n role: MessageRole;\n /** 메시지 내용 */\n content: React.ReactNode;\n /** 타임스탬프 */\n timestamp?: Date;\n /** 아바타 (ReactNode 또는 이미지 URL) */\n avatar?: React.ReactNode | string;\n /** 전송 상태 */\n status?: MessageStatus;\n /** 하단 액션 영역 */\n actions?: React.ReactNode;\n /** 메타데이터 (모델명 등) */\n metadata?: string;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst roleStyles: Record<MessageRole, { container: string; bubble: string }> = {\n user: {\n container: \"flex-row-reverse\",\n bubble: \"bg-[var(--axis-agent-message-user-bg)] text-[var(--axis-agent-message-user-text)] rounded-br-sm\",\n },\n assistant: {\n container: \"flex-row\",\n bubble: \"bg-[var(--axis-surface-secondary)] text-[var(--axis-text-primary)] rounded-bl-sm\",\n },\n system: {\n container: \"justify-center\",\n bubble: \"bg-[var(--axis-surface-tertiary)] text-[var(--axis-text-secondary)] text-xs italic\",\n },\n};\n\nconst statusIcons: Record<MessageStatus, React.ReactNode> = {\n sending: (\n <svg className=\"w-3 h-3 animate-spin motion-reduce:!animate-none\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n ),\n sent: (\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ),\n error: (\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 8v4m0 4h.01\" />\n </svg>\n ),\n};\n\n/**\n * 대화 메시지 버블 컴포넌트\n */\nexport function MessageBubble({\n role,\n content,\n timestamp,\n avatar,\n status,\n actions,\n metadata,\n className,\n}: MessageBubbleProps) {\n const roleLabel = MESSAGE_ROLE_LABELS[role];\n\n const renderAvatar = () => {\n if (!avatar) return null;\n if (typeof avatar === \"string\") {\n return (\n <img\n src={avatar}\n alt=\"\"\n aria-hidden=\"true\"\n className=\"w-8 h-8 rounded-full object-cover flex-shrink-0\"\n />\n );\n }\n return <div className=\"flex-shrink-0\">{avatar}</div>;\n };\n\n return (\n <div\n role=\"article\"\n aria-label={`${roleLabel} 메시지`}\n className={cn(\"flex items-start gap-2\", roleStyles[role].container, className)}\n >\n {role !== \"system\" && renderAvatar()}\n <div className={cn(\"max-w-[80%]\", role === \"system\" && \"max-w-full\")}>\n <div\n className={cn(\n \"rounded-lg px-3 py-2\",\n roleStyles[role].bubble\n )}\n >\n <div className=\"text-sm whitespace-pre-wrap\">{content}</div>\n </div>\n <div className=\"flex items-center gap-2 mt-1 px-1\">\n {timestamp && (\n <time\n dateTime={timestamp.toISOString()}\n className=\"text-xs text-[var(--axis-text-tertiary)]\"\n >\n {timestamp.toLocaleTimeString(\"ko-KR\", { hour: \"2-digit\", minute: \"2-digit\" })}\n </time>\n )}\n {metadata && (\n <span className=\"text-xs text-[var(--axis-text-tertiary)]\">{metadata}</span>\n )}\n {status && (\n <span\n className={cn(\n \"text-[var(--axis-text-tertiary)]\",\n status === \"error\" && \"text-[var(--axis-text-error)]\"\n )}\n aria-label={status === \"sending\" ? \"전송 중\" : status === \"sent\" ? \"전송됨\" : \"전송 실패\"}\n >\n {statusIcons[status]}\n </span>\n )}\n </div>\n {actions && <div className=\"mt-1\">{actions}</div>}\n </div>\n </div>\n );\n}\n"
17
17
  }
18
18
  ]
19
19
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/plan-card.tsx",
15
15
  "type": "registry:ui",
16
- "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { PLAN_STATUS_LABELS, STATUS_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type PlanStatus = \"draft\" | \"pending\" | \"approved\" | \"rejected\" | \"executing\" | \"completed\";\nexport type PlanStepStatus = \"pending\" | \"running\" | \"complete\" | \"error\" | \"skipped\";\n\nexport interface PlanStep {\n id: string;\n label: string;\n status: PlanStepStatus;\n description?: string;\n}\n\nexport interface PlanCardProps {\n /** 계획 제목 */\n title: string;\n /** 단계 목록 */\n steps: PlanStep[];\n /** 계획 상태 */\n status?: PlanStatus;\n /** 승인 콜백 */\n onApprove?: () => void;\n /** 수정 콜백 */\n onEdit?: () => void;\n /** 거절 콜백 */\n onReject?: () => void;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst planStatusStyles: Record<PlanStatus, { border: string; badge: string }> = {\n draft: {\n border: \"border-l-[var(--axis-status-neutral)]\",\n badge: \"bg-[var(--axis-status-neutral-bg)] text-[var(--axis-status-neutral-strong)]\",\n },\n pending: {\n border: \"border-l-[var(--axis-status-warning)]\",\n badge: \"bg-[var(--axis-badge-warning-bg)] text-[var(--axis-badge-warning-text)]\",\n },\n approved: {\n border: \"border-l-[var(--axis-status-success)]\",\n badge: \"bg-[var(--axis-badge-success-bg)] text-[var(--axis-badge-success-text)]\",\n },\n rejected: {\n border: \"border-l-[var(--axis-status-error)]\",\n badge: \"bg-[var(--axis-badge-error-bg)] text-[var(--axis-badge-error-text)]\",\n },\n executing: {\n border: \"border-l-[var(--axis-status-running)]\",\n badge: \"bg-[var(--axis-badge-info-bg)] text-[var(--axis-badge-info-text)]\",\n },\n completed: {\n border: \"border-l-[var(--axis-status-success)]\",\n badge: \"bg-[var(--axis-badge-success-bg)] text-[var(--axis-badge-success-text)]\",\n },\n};\n\nconst stepStatusIcons: Record<PlanStepStatus, React.ReactNode> = {\n pending: (\n <svg className=\"w-4 h-4 text-[var(--axis-status-neutral)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" strokeWidth={2} />\n </svg>\n ),\n running: (\n <svg className=\"w-4 h-4 text-[var(--axis-status-running)] animate-spin\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n ),\n complete: (\n <svg className=\"w-4 h-4 text-[var(--axis-status-success)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ),\n error: (\n <svg className=\"w-4 h-4 text-[var(--axis-status-error)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n ),\n skipped: (\n <svg className=\"w-4 h-4 text-[var(--axis-status-neutral)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 5l7 7-7 7M5 5l7 7-7 7\" />\n </svg>\n ),\n};\n\n/**\n * AI 실행 계획을 표시하고 승인/거절하는 카드 컴포넌트\n */\nexport function PlanCard({\n title,\n steps,\n status = \"pending\",\n onApprove,\n onEdit,\n onReject,\n className,\n}: PlanCardProps) {\n const styles = planStatusStyles[status];\n const titleId = React.useId();\n const statusLabel = PLAN_STATUS_LABELS[status];\n const completedCount = steps.filter((s) => s.status === \"complete\").length;\n\n return (\n <div\n role=\"region\"\n aria-labelledby={titleId}\n className={cn(\n \"rounded-lg border border-l-[length:var(--axis-plan-card-accent-width)] overflow-hidden\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n styles.border,\n className\n )}\n >\n {/* 헤더 */}\n <div className=\"p-4\">\n <div className=\"flex items-center justify-between mb-2\">\n <div className=\"flex items-center gap-2\">\n <svg className=\"w-4 h-4 text-[var(--axis-icon-secondary)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\" />\n </svg>\n <h4 id={titleId} className=\"text-sm font-semibold text-[var(--axis-text-primary)]\">\n {title}\n </h4>\n </div>\n <span className={cn(\"text-xs px-2 py-0.5 rounded-full font-medium\", styles.badge)}>\n {statusLabel}\n </span>\n </div>\n\n {/* 진행률 */}\n <div className=\"flex items-center gap-2 text-xs text-[var(--axis-text-tertiary)]\">\n <span>{completedCount}/{steps.length} 단계</span>\n {steps.length > 0 && (\n <div className=\"flex-1 h-1 rounded-full bg-[var(--axis-surface-secondary)] overflow-hidden\">\n <div\n className=\"h-full rounded-full bg-[var(--axis-status-success)] transition-all\"\n style={{ width: `${(completedCount / steps.length) * 100}%` }}\n aria-hidden=\"true\"\n />\n </div>\n )}\n </div>\n </div>\n\n {/* 단계 목록 */}\n <div role=\"list\" aria-label=\"실행 단계\" className=\"border-t border-[var(--axis-border-default)] divide-y divide-[var(--axis-border-default)]\">\n {steps.map((step) => {\n const stepStatusLabel = STATUS_LABELS[step.status] || step.status;\n return (\n <div\n key={step.id}\n role=\"listitem\"\n aria-current={step.status === \"running\" ? \"step\" : undefined}\n className=\"flex items-start gap-3 px-4 py-2.5\"\n >\n <span aria-hidden=\"true\" className=\"mt-0.5 flex-shrink-0\">\n {stepStatusIcons[step.status]}\n </span>\n <div className=\"flex-1 min-w-0\">\n <span className=\"sr-only\">{stepStatusLabel}:</span>\n <p className=\"text-sm text-[var(--axis-text-primary)]\">{step.label}</p>\n {step.description && (\n <p className=\"text-xs text-[var(--axis-text-secondary)] mt-0.5\">{step.description}</p>\n )}\n </div>\n </div>\n );\n })}\n </div>\n\n {/* 액션 버튼 */}\n {(status === \"pending\" || status === \"draft\") && (onApprove || onEdit || onReject) && (\n <div className=\"flex gap-2 p-3 border-t border-[var(--axis-border-default)] bg-[var(--axis-surface-secondary)]\">\n {onApprove && (\n <button\n onClick={onApprove}\n className=\"px-3 py-1.5 text-xs font-medium rounded-md bg-[var(--axis-status-success)] text-[var(--axis-status-success-contrast)] hover:bg-[var(--axis-status-success-strong)] transition-colors\"\n >\n 승인\n </button>\n )}\n {onEdit && (\n <button\n onClick={onEdit}\n className=\"px-3 py-1.5 text-xs font-medium rounded-md bg-[var(--axis-button-secondary-bg-default)] text-[var(--axis-text-primary)] hover:bg-[var(--axis-button-secondary-bg-hover)] transition-colors\"\n >\n 수정\n </button>\n )}\n {onReject && (\n <button\n onClick={onReject}\n className=\"px-3 py-1.5 text-xs font-medium rounded-md text-[var(--axis-status-error)] hover:text-[var(--axis-plan-card-reject-hover-text)] hover:bg-[color:color-mix(in_srgb,var(--axis-status-error)_10%,transparent)] transition-colors\"\n >\n 거절\n </button>\n )}\n </div>\n )}\n </div>\n );\n}\n"
16
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { PLAN_STATUS_LABELS, STATUS_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type PlanStatus = \"draft\" | \"pending\" | \"approved\" | \"rejected\" | \"executing\" | \"completed\";\nexport type PlanStepStatus = \"pending\" | \"running\" | \"complete\" | \"error\" | \"skipped\";\n\nexport interface PlanStep {\n id: string;\n label: string;\n status: PlanStepStatus;\n description?: string;\n}\n\nexport interface PlanCardProps {\n /** 계획 제목 */\n title: string;\n /** 단계 목록 */\n steps: PlanStep[];\n /** 계획 상태 */\n status?: PlanStatus;\n /** 승인 콜백 */\n onApprove?: () => void;\n /** 수정 콜백 */\n onEdit?: () => void;\n /** 거절 콜백 */\n onReject?: () => void;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst planStatusStyles: Record<PlanStatus, { border: string; badge: string }> = {\n draft: {\n border: \"border-l-[var(--axis-status-neutral)]\",\n badge: \"bg-[var(--axis-status-neutral-bg)] text-[var(--axis-status-neutral-strong)]\",\n },\n pending: {\n border: \"border-l-[var(--axis-status-warning)]\",\n badge: \"bg-[var(--axis-badge-warning-bg)] text-[var(--axis-badge-warning-text)]\",\n },\n approved: {\n border: \"border-l-[var(--axis-status-success)]\",\n badge: \"bg-[var(--axis-badge-success-bg)] text-[var(--axis-badge-success-text)]\",\n },\n rejected: {\n border: \"border-l-[var(--axis-status-error)]\",\n badge: \"bg-[var(--axis-badge-error-bg)] text-[var(--axis-badge-error-text)]\",\n },\n executing: {\n border: \"border-l-[var(--axis-status-running)]\",\n badge: \"bg-[var(--axis-badge-info-bg)] text-[var(--axis-badge-info-text)]\",\n },\n completed: {\n border: \"border-l-[var(--axis-status-success)]\",\n badge: \"bg-[var(--axis-badge-success-bg)] text-[var(--axis-badge-success-text)]\",\n },\n};\n\nconst stepStatusIcons: Record<PlanStepStatus, React.ReactNode> = {\n pending: (\n <svg className=\"w-4 h-4 text-[var(--axis-status-neutral)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" strokeWidth={2} />\n </svg>\n ),\n running: (\n <svg className=\"w-4 h-4 text-[var(--axis-status-running)] animate-spin motion-reduce:!animate-none\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n ),\n complete: (\n <svg className=\"w-4 h-4 text-[var(--axis-status-success)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ),\n error: (\n <svg className=\"w-4 h-4 text-[var(--axis-status-error)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n ),\n skipped: (\n <svg className=\"w-4 h-4 text-[var(--axis-status-neutral)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 5l7 7-7 7M5 5l7 7-7 7\" />\n </svg>\n ),\n};\n\n/**\n * AI 실행 계획을 표시하고 승인/거절하는 카드 컴포넌트\n */\nexport function PlanCard({\n title,\n steps,\n status = \"pending\",\n onApprove,\n onEdit,\n onReject,\n className,\n}: PlanCardProps) {\n const styles = planStatusStyles[status];\n const titleId = React.useId();\n const statusLabel = PLAN_STATUS_LABELS[status];\n const completedCount = steps.filter((s) => s.status === \"complete\").length;\n\n return (\n <div\n role=\"region\"\n aria-labelledby={titleId}\n className={cn(\n \"rounded-lg border border-l-[length:var(--axis-plan-card-accent-width)] overflow-hidden\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n styles.border,\n className\n )}\n >\n {/* 헤더 */}\n <div className=\"p-4\">\n <div className=\"flex items-center justify-between mb-2\">\n <div className=\"flex items-center gap-2\">\n <svg className=\"w-4 h-4 text-[var(--axis-icon-secondary)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\" />\n </svg>\n <h4 id={titleId} className=\"text-sm font-semibold text-[var(--axis-text-primary)]\">\n {title}\n </h4>\n </div>\n <span className={cn(\"text-xs px-2 py-0.5 rounded-full font-medium\", styles.badge)}>\n {statusLabel}\n </span>\n </div>\n\n {/* 진행률 */}\n <div className=\"flex items-center gap-2 text-xs text-[var(--axis-text-tertiary)]\">\n <span>{completedCount}/{steps.length} 단계</span>\n {steps.length > 0 && (\n <div className=\"flex-1 h-1 rounded-full bg-[var(--axis-surface-secondary)] overflow-hidden\">\n <div\n className=\"h-full rounded-full bg-[var(--axis-status-success)] transition-all\"\n style={{ width: `${(completedCount / steps.length) * 100}%` }}\n aria-hidden=\"true\"\n />\n </div>\n )}\n </div>\n </div>\n\n {/* 단계 목록 */}\n <div role=\"list\" aria-label=\"실행 단계\" className=\"border-t border-[var(--axis-border-default)] divide-y divide-[var(--axis-border-default)]\">\n {steps.map((step) => {\n const stepStatusLabel = STATUS_LABELS[step.status] || step.status;\n return (\n <div\n key={step.id}\n role=\"listitem\"\n aria-current={step.status === \"running\" ? \"step\" : undefined}\n className=\"flex items-start gap-3 px-4 py-2.5\"\n >\n <span aria-hidden=\"true\" className=\"mt-0.5 flex-shrink-0\">\n {stepStatusIcons[step.status]}\n </span>\n <div className=\"flex-1 min-w-0\">\n <span className=\"sr-only\">{stepStatusLabel}:</span>\n <p className=\"text-sm text-[var(--axis-text-primary)]\">{step.label}</p>\n {step.description && (\n <p className=\"text-xs text-[var(--axis-text-secondary)] mt-0.5\">{step.description}</p>\n )}\n </div>\n </div>\n );\n })}\n </div>\n\n {/* 액션 버튼 */}\n {(status === \"pending\" || status === \"draft\") && (onApprove || onEdit || onReject) && (\n <div className=\"flex gap-2 p-3 border-t border-[var(--axis-border-default)] bg-[var(--axis-surface-secondary)]\">\n {onApprove && (\n <button\n onClick={onApprove}\n className=\"px-3 py-1.5 text-xs font-medium rounded-md bg-[var(--axis-plan-card-approve-bg)] text-[var(--axis-status-success-contrast)] hover:bg-[var(--axis-plan-card-approve-hover-bg)] transition-colors\"\n >\n 승인\n </button>\n )}\n {onEdit && (\n <button\n onClick={onEdit}\n className=\"px-3 py-1.5 text-xs font-medium rounded-md bg-[var(--axis-button-secondary-bg-default)] text-[var(--axis-text-primary)] hover:bg-[var(--axis-button-secondary-bg-hover)] transition-colors\"\n >\n 수정\n </button>\n )}\n {onReject && (\n <button\n onClick={onReject}\n className=\"px-3 py-1.5 text-xs font-medium rounded-md text-[var(--axis-plan-card-reject-text)] hover:text-[var(--axis-plan-card-reject-hover-text)] hover:bg-[color:color-mix(in_srgb,var(--axis-status-error)_10%,transparent)] transition-colors\"\n >\n 거절\n </button>\n )}\n </div>\n )}\n </div>\n );\n}\n"
17
17
  }
18
18
  ]
19
19
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/run-progress.tsx",
15
15
  "type": "registry:ui",
16
- "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { STATUS_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type StepStatus = \"pending\" | \"running\" | \"complete\" | \"error\";\n\nexport interface Step {\n id: string;\n label: string;\n status: StepStatus;\n description?: string;\n}\n\nexport interface RunProgressProps {\n /** 전체 실행 상태 */\n status: \"idle\" | \"running\" | \"complete\" | \"error\";\n /** 단계 목록 */\n steps: Step[];\n /** 현재 진행 중인 단계 인덱스 */\n currentStep?: number;\n /** 취소 콜백 */\n onCancel?: () => void;\n /** 재시도 콜백 */\n onRetry?: () => void;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst statusColors: Record<StepStatus, string> = {\n pending: \"bg-[var(--axis-status-neutral-muted)] text-[var(--axis-status-neutral-muted-contrast)]\",\n running: \"bg-[var(--axis-status-running)] text-[var(--axis-status-running-contrast)] animate-pulse\",\n complete: \"bg-[var(--axis-status-success)] text-[var(--axis-status-success-contrast)]\",\n error: \"bg-[var(--axis-status-error)] text-[var(--axis-status-error-contrast)]\",\n};\n\nconst statusIcons: Record<StepStatus, React.ReactNode> = {\n pending: null,\n running: (\n <svg className=\"w-3 h-3 animate-spin\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n ),\n complete: (\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ),\n error: (\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n ),\n};\n\n/**\n * 에이전트 실행 진행 상태를 표시하는 컴포넌트\n */\nexport function RunProgress({\n status,\n steps,\n currentStep,\n onCancel,\n onRetry,\n className,\n}: RunProgressProps) {\n const progressPercent = React.useMemo(() => {\n const completed = steps.filter((s) => s.status === \"complete\").length;\n return Math.round((completed / steps.length) * 100);\n }, [steps]);\n\n return (\n <div\n className={cn(\n \"rounded-lg border p-4\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n className\n )}\n >\n {/* 헤더 */}\n <div className=\"flex items-center justify-between mb-4\">\n <div className=\"flex items-center gap-2\">\n {status === \"running\" && (\n <div className=\"w-2 h-2 rounded-full bg-[var(--axis-status-running)] animate-pulse\" />\n )}\n <span className=\"text-sm font-medium text-[var(--axis-text-primary)]\">\n {status === \"idle\" && \"대기 중\"}\n {status === \"running\" && \"실행 중\"}\n {status === \"complete\" && \"완료\"}\n {status === \"error\" && \"오류 발생\"}\n </span>\n </div>\n <span className=\"text-xs text-[var(--axis-text-secondary)]\">\n {progressPercent}%\n </span>\n </div>\n\n {/* 진행 바 */}\n <div\n role=\"progressbar\"\n aria-valuenow={progressPercent}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label={`실행 진행률 ${progressPercent}%`}\n className=\"w-full h-1.5 rounded-full bg-[var(--axis-surface-secondary)] mb-4\"\n >\n <div\n className=\"h-full rounded-full bg-[var(--axis-status-running)] transition-all duration-300\"\n style={{ width: `${progressPercent}%` }}\n />\n </div>\n\n {/* 단계 목록 */}\n <div role=\"list\" aria-label=\"실행 단계\" className=\"space-y-2\">\n {steps.map((step, index) => (\n <div\n key={step.id}\n role=\"listitem\"\n aria-current={currentStep === index ? \"step\" : undefined}\n className={cn(\n \"flex items-center gap-3 p-2 rounded-md transition-colors\",\n currentStep === index && \"bg-[var(--axis-surface-secondary)]\"\n )}\n >\n <div\n className={cn(\n \"w-5 h-5 rounded-full flex items-center justify-center text-xs\",\n statusColors[step.status]\n )}\n aria-hidden=\"true\"\n >\n {statusIcons[step.status] || index + 1}\n </div>\n <div className=\"flex-1 min-w-0\">\n <span className=\"sr-only\">{STATUS_LABELS[step.status]}:</span>\n <p className=\"text-sm font-medium text-[var(--axis-text-primary)] truncate\">\n {step.label}\n </p>\n {step.description && (\n <p className=\"text-xs text-[var(--axis-text-secondary)] truncate\">\n {step.description}\n </p>\n )}\n </div>\n </div>\n ))}\n </div>\n\n {/* 액션 버튼 */}\n {(onCancel || onRetry) && (\n <div className=\"flex justify-end gap-2 mt-4 pt-4 border-t border-[var(--axis-border-default)]\">\n {status === \"running\" && onCancel && (\n <button\n onClick={onCancel}\n className=\"px-3 py-1.5 text-sm rounded-md bg-[var(--axis-surface-secondary)] text-[var(--axis-text-primary)] hover:bg-[var(--axis-surface-tertiary)]\"\n >\n 취소\n </button>\n )}\n {status === \"error\" && onRetry && (\n <button\n onClick={onRetry}\n className=\"px-3 py-1.5 text-sm rounded-md bg-[var(--axis-button-bg-default)] text-[var(--axis-button-text-default)] hover:bg-[var(--axis-button-bg-hover)]\"\n >\n 재시도\n </button>\n )}\n </div>\n )}\n </div>\n );\n}\n"
16
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { STATUS_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type StepStatus = \"pending\" | \"running\" | \"complete\" | \"error\";\n\nexport interface Step {\n id: string;\n label: string;\n status: StepStatus;\n description?: string;\n}\n\nexport interface RunProgressProps {\n /** 전체 실행 상태 */\n status: \"idle\" | \"running\" | \"complete\" | \"error\";\n /** 단계 목록 */\n steps: Step[];\n /** 현재 진행 중인 단계 인덱스 */\n currentStep?: number;\n /** 취소 콜백 */\n onCancel?: () => void;\n /** 재시도 콜백 */\n onRetry?: () => void;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst statusColors: Record<StepStatus, string> = {\n pending: \"bg-[var(--axis-status-neutral-muted)] text-[var(--axis-status-neutral-muted-contrast)]\",\n running: \"bg-[var(--axis-status-running)] text-[var(--axis-status-running-contrast)] animate-pulse motion-reduce:!animate-none\",\n complete: \"bg-[var(--axis-status-success)] text-[var(--axis-status-success-contrast)]\",\n error: \"bg-[var(--axis-status-error)] text-[var(--axis-status-error-contrast)]\",\n};\n\nconst statusIcons: Record<StepStatus, React.ReactNode> = {\n pending: null,\n running: (\n <svg className=\"w-3 h-3 animate-spin motion-reduce:!animate-none\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n ),\n complete: (\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ),\n error: (\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n ),\n};\n\n/**\n * 에이전트 실행 진행 상태를 표시하는 컴포넌트\n */\nexport function RunProgress({\n status,\n steps,\n currentStep,\n onCancel,\n onRetry,\n className,\n}: RunProgressProps) {\n const progressPercent = React.useMemo(() => {\n const completed = steps.filter((s) => s.status === \"complete\").length;\n return Math.round((completed / steps.length) * 100);\n }, [steps]);\n\n return (\n <div\n className={cn(\n \"rounded-lg border p-4\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n className\n )}\n >\n {/* 헤더 */}\n <div className=\"flex items-center justify-between mb-4\">\n <div className=\"flex items-center gap-2\">\n {status === \"running\" && (\n <div className=\"w-2 h-2 rounded-full bg-[var(--axis-status-running)] animate-pulse motion-reduce:!animate-none\" />\n )}\n <span className=\"text-sm font-medium text-[var(--axis-text-primary)]\">\n {status === \"idle\" && \"대기 중\"}\n {status === \"running\" && \"실행 중\"}\n {status === \"complete\" && \"완료\"}\n {status === \"error\" && \"오류 발생\"}\n </span>\n </div>\n <span className=\"text-xs text-[var(--axis-text-secondary)]\">\n {progressPercent}%\n </span>\n </div>\n\n {/* 진행 바 */}\n <div\n role=\"progressbar\"\n aria-valuenow={progressPercent}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label={`실행 진행률 ${progressPercent}%`}\n className=\"w-full h-1.5 rounded-full bg-[var(--axis-surface-secondary)] mb-4\"\n >\n <div\n className=\"h-full rounded-full bg-[var(--axis-status-running)] transition-all duration-300\"\n style={{ width: `${progressPercent}%` }}\n />\n </div>\n\n {/* 단계 목록 */}\n <div role=\"list\" aria-label=\"실행 단계\" className=\"space-y-2\">\n {steps.map((step, index) => (\n <div\n key={step.id}\n role=\"listitem\"\n aria-current={currentStep === index ? \"step\" : undefined}\n className={cn(\n \"flex items-center gap-3 p-2 rounded-md transition-colors\",\n currentStep === index && \"bg-[var(--axis-surface-secondary)]\"\n )}\n >\n <div\n className={cn(\n \"w-5 h-5 rounded-full flex items-center justify-center text-xs\",\n statusColors[step.status]\n )}\n aria-hidden=\"true\"\n >\n {statusIcons[step.status] || index + 1}\n </div>\n <div className=\"flex-1 min-w-0\">\n <span className=\"sr-only\">{STATUS_LABELS[step.status]}:</span>\n <p className=\"text-sm font-medium text-[var(--axis-text-primary)] truncate\">\n {step.label}\n </p>\n {step.description && (\n <p className=\"text-xs text-[var(--axis-text-secondary)] truncate\">\n {step.description}\n </p>\n )}\n </div>\n </div>\n ))}\n </div>\n\n {/* 액션 버튼 */}\n {(onCancel || onRetry) && (\n <div className=\"flex justify-end gap-2 mt-4 pt-4 border-t border-[var(--axis-border-default)]\">\n {status === \"running\" && onCancel && (\n <button\n onClick={onCancel}\n className=\"px-3 py-1.5 text-sm rounded-md bg-[var(--axis-surface-secondary)] text-[var(--axis-text-primary)] hover:bg-[var(--axis-surface-tertiary)]\"\n >\n 취소\n </button>\n )}\n {status === \"error\" && onRetry && (\n <button\n onClick={onRetry}\n className=\"px-3 py-1.5 text-sm rounded-md bg-[var(--axis-button-bg-default)] text-[var(--axis-button-text-default)] hover:bg-[var(--axis-button-bg-hover)]\"\n >\n 재시도\n </button>\n )}\n </div>\n )}\n </div>\n );\n}\n"
17
17
  }
18
18
  ]
19
19
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/step-timeline.tsx",
15
15
  "type": "registry:ui",
16
- "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { STATUS_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type TimelineStepStatus = \"pending\" | \"running\" | \"complete\" | \"error\" | \"skipped\";\n\nexport interface TimelineStep {\n id: string;\n label: string;\n status: TimelineStepStatus;\n timestamp?: Date;\n description?: string;\n}\n\nexport interface StepTimelineProps {\n steps: TimelineStep[];\n orientation?: \"vertical\" | \"horizontal\";\n className?: string;\n}\n\n/**\n * 단계별 타임라인 컴포넌트\n */\nexport function StepTimeline({\n steps,\n orientation = \"vertical\",\n className,\n}: StepTimelineProps) {\n const isVertical = orientation === \"vertical\";\n\n return (\n <div\n role=\"list\"\n aria-label=\"작업 타임라인\"\n className={cn(\n isVertical ? \"flex flex-col gap-4\" : \"flex items-center gap-2\",\n className\n )}\n >\n {steps.map((step, index) => (\n <div\n key={step.id}\n role=\"listitem\"\n aria-current={step.status === \"running\" ? \"step\" : undefined}\n className={cn(\n \"relative flex\",\n isVertical ? \"items-start gap-3\" : \"flex-col items-center\"\n )}\n >\n {/* 연결선 */}\n {index < steps.length - 1 && (\n <div\n aria-hidden=\"true\"\n className={cn(\n \"absolute bg-[var(--axis-border-default)]\",\n isVertical\n ? \"left-2.5 top-6 w-0.5 h-full -translate-x-1/2\"\n : \"top-2.5 left-6 h-0.5 w-full\"\n )}\n />\n )}\n\n {/* 상태 인디케이터 */}\n <div\n aria-hidden=\"true\"\n className={cn(\n \"relative z-10 w-5 h-5 rounded-full flex items-center justify-center\",\n step.status === \"complete\" && \"bg-[var(--axis-status-success)]\",\n step.status === \"running\" && \"bg-[var(--axis-status-running)] animate-pulse\",\n step.status === \"error\" && \"bg-[var(--axis-status-error)]\",\n step.status === \"skipped\" && \"bg-[var(--axis-status-neutral-muted)]\",\n step.status === \"pending\" && \"bg-[var(--axis-status-neutral-muted)]\"\n )}\n >\n {step.status === \"complete\" && (\n <svg className=\"w-3 h-3 text-[var(--axis-status-success-contrast)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={3} d=\"M5 13l4 4L19 7\" />\n </svg>\n )}\n </div>\n\n {/* 라벨 */}\n <div className={cn(isVertical ? \"flex-1\" : \"text-center mt-2\")}>\n <span className=\"sr-only\">{STATUS_LABELS[step.status]}:</span>\n <p className=\"text-sm font-medium text-[var(--axis-text-primary)]\">\n {step.label}\n </p>\n {step.description && (\n <p className=\"text-xs text-[var(--axis-text-secondary)]\">\n {step.description}\n </p>\n )}\n {step.timestamp && (\n <p className=\"text-xs text-[var(--axis-text-tertiary)]\">\n {step.timestamp.toLocaleTimeString()}\n </p>\n )}\n </div>\n </div>\n ))}\n </div>\n );\n}\n"
16
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { STATUS_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type TimelineStepStatus = \"pending\" | \"running\" | \"complete\" | \"error\" | \"skipped\";\n\nexport interface TimelineStep {\n id: string;\n label: string;\n status: TimelineStepStatus;\n timestamp?: Date;\n description?: string;\n}\n\nexport interface StepTimelineProps {\n steps: TimelineStep[];\n orientation?: \"vertical\" | \"horizontal\";\n className?: string;\n}\n\n/**\n * 단계별 타임라인 컴포넌트\n */\nexport function StepTimeline({\n steps,\n orientation = \"vertical\",\n className,\n}: StepTimelineProps) {\n const isVertical = orientation === \"vertical\";\n\n return (\n <div\n role=\"list\"\n aria-label=\"작업 타임라인\"\n className={cn(\n isVertical ? \"flex flex-col gap-4\" : \"flex items-center gap-2\",\n className\n )}\n >\n {steps.map((step, index) => (\n <div\n key={step.id}\n role=\"listitem\"\n aria-current={step.status === \"running\" ? \"step\" : undefined}\n className={cn(\n \"relative flex\",\n isVertical ? \"items-start gap-3\" : \"flex-col items-center\"\n )}\n >\n {/* 연결선 */}\n {index < steps.length - 1 && (\n <div\n aria-hidden=\"true\"\n className={cn(\n \"absolute bg-[var(--axis-border-default)]\",\n isVertical\n ? \"left-2.5 top-6 w-0.5 h-full -translate-x-1/2\"\n : \"top-2.5 left-6 h-0.5 w-full\"\n )}\n />\n )}\n\n {/* 상태 인디케이터 */}\n <div\n aria-hidden=\"true\"\n className={cn(\n \"relative z-10 w-5 h-5 rounded-full flex items-center justify-center\",\n step.status === \"complete\" && \"bg-[var(--axis-status-success)]\",\n step.status === \"running\" && \"bg-[var(--axis-status-running)] animate-pulse motion-reduce:!animate-none\",\n step.status === \"error\" && \"bg-[var(--axis-status-error)]\",\n step.status === \"skipped\" && \"bg-[var(--axis-status-neutral-muted)]\",\n step.status === \"pending\" && \"bg-[var(--axis-status-neutral-muted)]\"\n )}\n >\n {step.status === \"complete\" && (\n <svg className=\"w-3 h-3 text-[var(--axis-status-success-contrast)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={3} d=\"M5 13l4 4L19 7\" />\n </svg>\n )}\n </div>\n\n {/* 라벨 */}\n <div className={cn(isVertical ? \"flex-1\" : \"text-center mt-2\")}>\n <span className=\"sr-only\">{STATUS_LABELS[step.status]}:</span>\n <p className=\"text-sm font-medium text-[var(--axis-text-primary)]\">\n {step.label}\n </p>\n {step.description && (\n <p className=\"text-xs text-[var(--axis-text-secondary)]\">\n {step.description}\n </p>\n )}\n {step.timestamp && (\n <p className=\"text-xs text-[var(--axis-text-tertiary)]\">\n {step.timestamp.toLocaleTimeString()}\n </p>\n )}\n </div>\n </div>\n ))}\n </div>\n );\n}\n"
17
17
  }
18
18
  ]
19
19
  }
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "components/agentic/streaming-text.tsx",
14
14
  "type": "registry:ui",
15
- "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nexport interface StreamingTextProps {\n /** 접근성 라이브 영역 모드 */\n \"aria-live\"?: \"polite\" | \"assertive\" | \"off\";\n /** 접근성 레이블 */\n \"aria-label\"?: string;\n /** 스트리밍할 텍스트 */\n text: string;\n /** 스트리밍 속도 (ms) */\n speed?: number;\n /** 스트리밍 완료 여부 */\n isComplete?: boolean;\n /** 커서 표시 여부 */\n showCursor?: boolean;\n /** 완료 콜백 */\n onComplete?: () => void;\n /** 추가 클래스 */\n className?: string;\n}\n\n/**\n * 텍스트를 실시간으로 스트리밍하는 컴포넌트\n * AI 응답을 타이핑 효과로 표시할 때 사용\n */\nexport function StreamingText({\n text,\n speed = 20,\n isComplete = false,\n showCursor = true,\n onComplete,\n \"aria-live\": ariaLive,\n \"aria-label\": ariaLabel,\n className,\n}: StreamingTextProps) {\n // 글자는 코드 포인트 단위로 센다(이모지 등 서로게이트 쌍을 반쪽으로 보이지 않게)\n const chars = React.useMemo(() => Array.from(text), [text]);\n // 상태는 \"보여 줄 글자 수\"만 두고 화면 문자열은 text 에서 잘라 만든다.\n // effect 가 정리 후 다시 실행돼도(StrictMode · 부모 재렌더) 이 위치에서 이어 간다.\n const [visibleLength, setVisibleLength] = React.useState(0);\n const [prevText, setPrevText] = React.useState(text);\n // text 변경: 이어 붙이기면 진행 위치 유지, 통째 교체면 처음부터(렌더 중 상태 조정)\n if (text !== prevText) {\n setPrevText(text);\n if (!text.startsWith(prevText)) setVisibleLength(0);\n }\n\n const targetLength = chars.length;\n const isStreaming = !isComplete && visibleLength < targetLength;\n\n // onComplete 는 ref 로 읽어 effect 의존성에서 뺀다(부모가 매 렌더 새 함수를 넘겨도 진행 유지)\n const onCompleteRef = React.useRef(onComplete);\n React.useEffect(() => {\n onCompleteRef.current = onComplete;\n });\n\n React.useEffect(() => {\n if (!isStreaming) return;\n const interval = setInterval(() => {\n setVisibleLength((n) => Math.min(n + 1, targetLength));\n }, speed);\n return () => clearInterval(interval);\n }, [isStreaming, targetLength, speed]);\n\n // 화면이 text 끝까지 따라잡은 순간 한 번 onComplete 를 부른다(isComplete 로 즉시 표시한 경우는 제외)\n const wasStreamingRef = React.useRef(false);\n React.useEffect(() => {\n if (isStreaming) {\n wasStreamingRef.current = true;\n return;\n }\n if (wasStreamingRef.current) {\n wasStreamingRef.current = false;\n if (!isComplete) onCompleteRef.current?.();\n }\n }, [isStreaming, isComplete]);\n\n const displayedText = isComplete ? text : chars.slice(0, visibleLength).join(\"\");\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel || \"AI 응답\"}\n aria-live={ariaLive ?? \"polite\"}\n aria-busy={isStreaming}\n className={cn(\"relative\", className)}\n >\n <span className=\"text-[var(--axis-text-primary)] whitespace-pre-wrap\">\n {displayedText}\n </span>\n {showCursor && isStreaming && (\n <span className=\"inline-block w-0.5 h-4 ml-0.5 bg-[var(--axis-text-primary)] animate-pulse\" aria-hidden=\"true\" />\n )}\n </div>\n );\n}\n"
15
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nexport interface StreamingTextProps {\n /** 접근성 라이브 영역 모드 */\n \"aria-live\"?: \"polite\" | \"assertive\" | \"off\";\n /** 접근성 레이블 */\n \"aria-label\"?: string;\n /** 스트리밍할 텍스트 */\n text: string;\n /** 스트리밍 속도 (ms) */\n speed?: number;\n /** 스트리밍 완료 여부 */\n isComplete?: boolean;\n /** 커서 표시 여부 */\n showCursor?: boolean;\n /** 완료 콜백 */\n onComplete?: () => void;\n /** 추가 클래스 */\n className?: string;\n}\n\n/**\n * 텍스트를 실시간으로 스트리밍하는 컴포넌트\n * AI 응답을 타이핑 효과로 표시할 때 사용\n */\nexport function StreamingText({\n text,\n speed = 20,\n isComplete = false,\n showCursor = true,\n onComplete,\n \"aria-live\": ariaLive,\n \"aria-label\": ariaLabel,\n className,\n}: StreamingTextProps) {\n // 글자는 코드 포인트 단위로 센다(이모지 등 서로게이트 쌍을 반쪽으로 보이지 않게)\n const chars = React.useMemo(() => Array.from(text), [text]);\n // 상태는 \"보여 줄 글자 수\"만 두고 화면 문자열은 text 에서 잘라 만든다.\n // effect 가 정리 후 다시 실행돼도(StrictMode · 부모 재렌더) 이 위치에서 이어 간다.\n const [visibleLength, setVisibleLength] = React.useState(0);\n const [prevText, setPrevText] = React.useState(text);\n // text 변경: 이어 붙이기면 진행 위치 유지, 통째 교체면 처음부터(렌더 중 상태 조정)\n if (text !== prevText) {\n setPrevText(text);\n if (!text.startsWith(prevText)) setVisibleLength(0);\n }\n\n const targetLength = chars.length;\n const isStreaming = !isComplete && visibleLength < targetLength;\n\n // onComplete 는 ref 로 읽어 effect 의존성에서 뺀다(부모가 매 렌더 새 함수를 넘겨도 진행 유지)\n const onCompleteRef = React.useRef(onComplete);\n React.useEffect(() => {\n onCompleteRef.current = onComplete;\n });\n\n React.useEffect(() => {\n if (!isStreaming) return;\n const interval = setInterval(() => {\n setVisibleLength((n) => Math.min(n + 1, targetLength));\n }, speed);\n return () => clearInterval(interval);\n }, [isStreaming, targetLength, speed]);\n\n // 화면이 text 끝까지 따라잡은 순간 한 번 onComplete 를 부른다(isComplete 로 즉시 표시한 경우는 제외)\n const wasStreamingRef = React.useRef(false);\n React.useEffect(() => {\n if (isStreaming) {\n wasStreamingRef.current = true;\n return;\n }\n if (wasStreamingRef.current) {\n wasStreamingRef.current = false;\n if (!isComplete) onCompleteRef.current?.();\n }\n }, [isStreaming, isComplete]);\n\n const displayedText = isComplete ? text : chars.slice(0, visibleLength).join(\"\");\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel || \"AI 응답\"}\n aria-live={ariaLive ?? \"polite\"}\n aria-busy={isStreaming}\n className={cn(\"relative\", className)}\n >\n <span className=\"text-[var(--axis-text-primary)] whitespace-pre-wrap\">\n {displayedText}\n </span>\n {showCursor && isStreaming && (\n <span className=\"inline-block w-0.5 h-4 ml-0.5 bg-[var(--axis-text-primary)] animate-pulse motion-reduce:!animate-none\" aria-hidden=\"true\" />\n )}\n </div>\n );\n}\n"
16
16
  }
17
17
  ]
18
18
  }
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "components/agentic/surface-renderer.tsx",
14
14
  "type": "registry:ui",
15
- "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nexport type SurfaceType = \"text\" | \"code\" | \"table\" | \"chart\" | \"form\" | \"custom\";\n\nexport interface Surface {\n id: string;\n type: SurfaceType;\n title?: string;\n content: unknown;\n metadata?: Record<string, unknown>;\n}\n\nexport interface SurfaceRendererProps {\n surface: Surface;\n renderCustom?: (surface: Surface) => React.ReactNode;\n className?: string;\n}\n\n/**\n * 다양한 타입의 Surface를 동적으로 렌더링하는 컴포넌트\n */\nexport function SurfaceRenderer({\n surface,\n renderCustom,\n className,\n}: SurfaceRendererProps) {\n const renderContent = () => {\n switch (surface.type) {\n case \"text\":\n return (\n <div className=\"prose prose-sm max-w-none text-[var(--axis-text-primary)]\">\n {String(surface.content)}\n </div>\n );\n\n case \"code\":\n return (\n <pre className=\"p-3 rounded-md bg-[var(--axis-surface-secondary)] overflow-x-auto\">\n <code className=\"text-sm font-mono text-[var(--axis-text-primary)]\">\n {String(surface.content)}\n </code>\n </pre>\n );\n\n case \"table\": {\n const tableData = surface.content as { headers: string[]; rows: string[][] };\n return (\n <div className=\"overflow-x-auto\">\n <table role=\"table\" className=\"w-full text-sm\">\n {surface.title && (\n <caption className=\"sr-only\">{surface.title}</caption>\n )}\n <thead>\n <tr className=\"border-b border-[var(--axis-border-default)]\">\n {tableData.headers.map((header, i) => (\n <th\n key={i}\n scope=\"col\"\n className=\"px-3 py-2 text-left font-medium text-[var(--axis-text-secondary)]\"\n >\n {header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {tableData.rows.map((row, i) => (\n <tr\n key={i}\n className=\"border-b border-[var(--axis-border-default)] last:border-0\"\n >\n {row.map((cell, j) => (\n <td\n key={j}\n className=\"px-3 py-2 text-[var(--axis-text-primary)]\"\n >\n {cell}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n }\n\n case \"chart\":\n return (\n <div className=\"p-4 text-center text-[var(--axis-text-secondary)]\">\n [차트: {surface.title || \"데이터 시각화\"}]\n </div>\n );\n\n case \"form\":\n return (\n <div className=\"p-4 text-center text-[var(--axis-text-secondary)]\">\n [폼: {surface.title || \"입력 양식\"}]\n </div>\n );\n\n case \"custom\":\n if (renderCustom) {\n return renderCustom(surface);\n }\n return (\n <div className=\"p-4 text-center text-[var(--axis-text-secondary)]\">\n [커스텀 Surface]\n </div>\n );\n\n default:\n return null;\n }\n };\n\n return (\n <div\n className={cn(\n \"rounded-lg border p-4\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n className\n )}\n >\n {surface.title && (\n <h4 className=\"text-sm font-medium text-[var(--axis-text-primary)] mb-3\">\n {surface.title}\n </h4>\n )}\n {renderContent()}\n </div>\n );\n}\n"
15
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nexport type SurfaceType = \"text\" | \"code\" | \"table\" | \"chart\" | \"form\" | \"custom\";\n\nexport interface Surface {\n id: string;\n type: SurfaceType;\n title?: string;\n content: unknown;\n metadata?: Record<string, unknown>;\n}\n\nexport interface SurfaceRendererProps {\n surface: Surface;\n renderCustom?: (surface: Surface) => React.ReactNode;\n className?: string;\n}\n\n/**\n * 다양한 타입의 Surface를 동적으로 렌더링하는 컴포넌트\n */\nexport function SurfaceRenderer({\n surface,\n renderCustom,\n className,\n}: SurfaceRendererProps) {\n const renderContent = () => {\n switch (surface.type) {\n case \"text\":\n return (\n <div className=\"text-sm text-[var(--axis-text-primary)]\">\n {String(surface.content)}\n </div>\n );\n\n case \"code\":\n return (\n <pre className=\"p-3 rounded-md bg-[var(--axis-surface-secondary)] overflow-x-auto\">\n <code className=\"text-sm font-mono text-[var(--axis-text-primary)]\">\n {String(surface.content)}\n </code>\n </pre>\n );\n\n case \"table\": {\n const tableData = surface.content as { headers: string[]; rows: string[][] };\n return (\n <div className=\"overflow-x-auto\">\n <table role=\"table\" className=\"w-full text-sm\">\n {surface.title && (\n <caption className=\"sr-only\">{surface.title}</caption>\n )}\n <thead>\n <tr className=\"border-b border-[var(--axis-border-default)]\">\n {tableData.headers.map((header, i) => (\n <th\n key={i}\n scope=\"col\"\n className=\"px-3 py-2 text-left font-medium text-[var(--axis-text-secondary)]\"\n >\n {header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {tableData.rows.map((row, i) => (\n <tr\n key={i}\n className=\"border-b border-[var(--axis-border-default)] last:border-0\"\n >\n {row.map((cell, j) => (\n <td\n key={j}\n className=\"px-3 py-2 text-[var(--axis-text-primary)]\"\n >\n {cell}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n }\n\n case \"chart\":\n return (\n <div className=\"p-4 text-center text-[var(--axis-text-secondary)]\">\n [차트: {surface.title || \"데이터 시각화\"}]\n </div>\n );\n\n case \"form\":\n return (\n <div className=\"p-4 text-center text-[var(--axis-text-secondary)]\">\n [폼: {surface.title || \"입력 양식\"}]\n </div>\n );\n\n case \"custom\":\n if (renderCustom) {\n return renderCustom(surface);\n }\n return (\n <div className=\"p-4 text-center text-[var(--axis-text-secondary)]\">\n [커스텀 Surface]\n </div>\n );\n\n default:\n return null;\n }\n };\n\n return (\n <div\n className={cn(\n \"rounded-lg border p-4\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n className\n )}\n >\n {surface.title && (\n <h4 className=\"text-sm font-medium text-[var(--axis-text-primary)] mb-3\">\n {surface.title}\n </h4>\n )}\n {renderContent()}\n </div>\n );\n}\n"
16
16
  }
17
17
  ]
18
18
  }
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "components/agentic/thinking-indicator.tsx",
14
14
  "type": "registry:ui",
15
- "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nexport interface ThinkingIndicatorProps {\n /** 표시할 텍스트 */\n text?: string;\n /** 크기 */\n size?: \"sm\" | \"md\" | \"lg\";\n /** 추가 클래스 */\n className?: string;\n}\n\nconst sizeStyles = {\n sm: \"text-xs gap-1\",\n md: \"text-sm gap-1.5\",\n lg: \"text-base gap-2\",\n};\n\nconst dotSizes = {\n sm: \"w-1 h-1\",\n md: \"w-1.5 h-1.5\",\n lg: \"w-2 h-2\",\n};\n\n/**\n * AI가 생각 중임을 표시하는 인디케이터\n */\nexport function ThinkingIndicator({\n text = \"생각 중\",\n size = \"md\",\n className,\n}: ThinkingIndicatorProps) {\n return (\n <div\n role=\"status\"\n aria-live=\"polite\"\n aria-label={text}\n className={cn(\n \"flex items-center text-[var(--axis-text-secondary)]\",\n sizeStyles[size],\n className\n )}\n >\n <span>{text}</span>\n <div className=\"flex gap-0.5\" aria-hidden=\"true\">\n {[0, 1, 2].map((i) => (\n <div\n key={i}\n className={cn(\n \"rounded-full bg-current animate-bounce\",\n dotSizes[size]\n )}\n style={{ animationDelay: `${i * 150}ms` }}\n />\n ))}\n </div>\n </div>\n );\n}\n"
15
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nexport interface ThinkingIndicatorProps {\n /** 표시할 텍스트 */\n text?: string;\n /** 크기 */\n size?: \"sm\" | \"md\" | \"lg\";\n /** 추가 클래스 */\n className?: string;\n}\n\nconst sizeStyles = {\n sm: \"text-xs gap-1\",\n md: \"text-sm gap-1.5\",\n lg: \"text-base gap-2\",\n};\n\nconst dotSizes = {\n sm: \"w-1 h-1\",\n md: \"w-1.5 h-1.5\",\n lg: \"w-2 h-2\",\n};\n\n/**\n * AI가 생각 중임을 표시하는 인디케이터\n */\nexport function ThinkingIndicator({\n text = \"생각 중\",\n size = \"md\",\n className,\n}: ThinkingIndicatorProps) {\n return (\n <div\n role=\"status\"\n aria-live=\"polite\"\n aria-label={text}\n className={cn(\n \"flex items-center text-[var(--axis-text-secondary)]\",\n sizeStyles[size],\n className\n )}\n >\n <span>{text}</span>\n <div className=\"flex gap-0.5\" aria-hidden=\"true\">\n {[0, 1, 2].map((i) => (\n <div\n key={i}\n className={cn(\n \"rounded-full bg-current animate-bounce motion-reduce:!animate-none\",\n dotSizes[size]\n )}\n style={{ animationDelay: `${i * 150}ms` }}\n />\n ))}\n </div>\n </div>\n );\n}\n"
16
16
  }
17
17
  ]
18
18
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/tool-call-card.tsx",
15
15
  "type": "registry:ui",
16
- "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { STATUS_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type ToolStatus = \"pending\" | \"running\" | \"success\" | \"error\";\n\nexport interface ToolCallCardProps {\n /** 도구 이름 */\n toolName: string;\n /** 도구 설명 */\n description?: string;\n /** 실행 상태 */\n status: ToolStatus;\n /** 입력 파라미터 */\n input?: Record<string, unknown>;\n /** 출력 결과 */\n output?: unknown;\n /** 에러 메시지 */\n error?: string;\n /** 실행 시간 (ms) */\n duration?: number;\n /** 펼침 상태 */\n defaultExpanded?: boolean;\n /** 접근성 레이블 */\n \"aria-label\"?: string;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst statusConfig: Record<ToolStatus, { color: string; icon: React.ReactNode }> = {\n pending: {\n color: \"text-[var(--axis-status-neutral)]\",\n icon: (\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" strokeWidth={2} />\n </svg>\n ),\n },\n running: {\n color: \"text-[var(--axis-status-running)]\",\n icon: (\n <svg className=\"w-4 h-4 animate-spin\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n ),\n },\n success: {\n color: \"text-[var(--axis-status-success)]\",\n icon: (\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ),\n },\n error: {\n color: \"text-[var(--axis-status-error)]\",\n icon: (\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n ),\n },\n};\n\n/**\n * AI 도구 호출 정보를 표시하는 카드 컴포넌트\n */\nexport function ToolCallCard({\n toolName,\n description,\n status,\n input,\n output,\n error,\n duration,\n defaultExpanded = false,\n \"aria-label\": ariaLabel,\n className,\n}: ToolCallCardProps) {\n const [isExpanded, setIsExpanded] = React.useState(defaultExpanded);\n const config = statusConfig[status];\n const contentId = React.useId();\n const statusLabel = STATUS_LABELS[status] || status;\n\n return (\n <div\n className={cn(\n \"rounded-lg border overflow-hidden\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n className\n )}\n >\n {/* 헤더 */}\n <button\n onClick={() => setIsExpanded(!isExpanded)}\n aria-expanded={isExpanded}\n aria-controls={contentId}\n aria-label={ariaLabel || `${toolName} - ${statusLabel}`}\n className=\"w-full flex items-center justify-between p-3 hover:bg-[var(--axis-surface-secondary)] transition-colors\"\n >\n <div className=\"flex items-center gap-2\">\n <span className={config.color} aria-hidden=\"true\">{config.icon}</span>\n <code className=\"text-sm font-mono text-[var(--axis-text-primary)]\">\n {toolName}\n </code>\n {description && (\n <span className=\"text-xs text-[var(--axis-text-secondary)]\">\n — {description}\n </span>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n {duration !== undefined && (\n <span className=\"text-xs text-[var(--axis-text-tertiary)]\">\n {duration}ms\n </span>\n )}\n <svg\n className={cn(\n \"w-4 h-4 text-[var(--axis-text-secondary)] transition-transform\",\n isExpanded && \"rotate-180\"\n )}\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M19 9l-7 7-7-7\" />\n </svg>\n </div>\n </button>\n\n {/* 내용 */}\n {isExpanded && (\n <div id={contentId} className=\"border-t border-[var(--axis-border-default)] p-3 space-y-3\">\n {input && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">\n 입력\n </h5>\n <pre className=\"text-xs p-2 rounded bg-[var(--axis-surface-secondary)] overflow-x-auto\">\n <code className=\"text-[var(--axis-text-primary)]\">\n {JSON.stringify(input, null, 2)}\n </code>\n </pre>\n </div>\n )}\n {output !== undefined && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">\n 출력\n </h5>\n <pre className=\"text-xs p-2 rounded bg-[var(--axis-surface-secondary)] overflow-x-auto\">\n <code className=\"text-[var(--axis-text-primary)]\">\n {typeof output === \"string\" ? output : JSON.stringify(output, null, 2)}\n </code>\n </pre>\n </div>\n )}\n {error && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-error)] mb-1\">\n 에러\n </h5>\n <pre className=\"text-xs p-2 rounded bg-[var(--axis-surface-error)] overflow-x-auto\">\n <code className=\"text-[var(--axis-text-error)]\">{error}</code>\n </pre>\n </div>\n )}\n </div>\n )}\n </div>\n );\n}\n"
16
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { STATUS_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type ToolStatus = \"pending\" | \"running\" | \"success\" | \"error\";\n\nexport interface ToolCallCardProps {\n /** 도구 이름 */\n toolName: string;\n /** 도구 설명 */\n description?: string;\n /** 실행 상태 */\n status: ToolStatus;\n /** 입력 파라미터 */\n input?: Record<string, unknown>;\n /** 출력 결과 */\n output?: unknown;\n /** 에러 메시지 */\n error?: string;\n /** 실행 시간 (ms) */\n duration?: number;\n /** 펼침 상태 */\n defaultExpanded?: boolean;\n /** 접근성 레이블 */\n \"aria-label\"?: string;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst statusConfig: Record<ToolStatus, { color: string; icon: React.ReactNode }> = {\n pending: {\n color: \"text-[var(--axis-status-neutral)]\",\n icon: (\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" strokeWidth={2} />\n </svg>\n ),\n },\n running: {\n color: \"text-[var(--axis-status-running)]\",\n icon: (\n <svg className=\"w-4 h-4 animate-spin motion-reduce:!animate-none\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n ),\n },\n success: {\n color: \"text-[var(--axis-status-success)]\",\n icon: (\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ),\n },\n error: {\n color: \"text-[var(--axis-status-error)]\",\n icon: (\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n ),\n },\n};\n\n/**\n * AI 도구 호출 정보를 표시하는 카드 컴포넌트\n */\nexport function ToolCallCard({\n toolName,\n description,\n status,\n input,\n output,\n error,\n duration,\n defaultExpanded = false,\n \"aria-label\": ariaLabel,\n className,\n}: ToolCallCardProps) {\n const [isExpanded, setIsExpanded] = React.useState(defaultExpanded);\n const config = statusConfig[status];\n const contentId = React.useId();\n const statusLabel = STATUS_LABELS[status] || status;\n\n return (\n <div\n className={cn(\n \"rounded-lg border overflow-hidden\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n className\n )}\n >\n {/* 헤더 */}\n <button\n onClick={() => setIsExpanded(!isExpanded)}\n aria-expanded={isExpanded}\n aria-controls={contentId}\n aria-label={ariaLabel || `${toolName} - ${statusLabel}`}\n className=\"w-full flex items-center justify-between p-3 hover:bg-[var(--axis-surface-secondary)] transition-colors\"\n >\n <div className=\"flex items-center gap-2\">\n <span className={config.color} aria-hidden=\"true\">{config.icon}</span>\n <code className=\"text-sm font-mono text-[var(--axis-text-primary)]\">\n {toolName}\n </code>\n {description && (\n <span className=\"text-xs text-[var(--axis-text-secondary)]\">\n — {description}\n </span>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n {duration !== undefined && (\n <span className=\"text-xs text-[var(--axis-text-tertiary)]\">\n {duration}ms\n </span>\n )}\n <svg\n className={cn(\n \"w-4 h-4 text-[var(--axis-text-secondary)] transition-transform\",\n isExpanded && \"rotate-180\"\n )}\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M19 9l-7 7-7-7\" />\n </svg>\n </div>\n </button>\n\n {/* 내용 */}\n {isExpanded && (\n <div id={contentId} className=\"border-t border-[var(--axis-border-default)] p-3 space-y-3\">\n {input && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">\n 입력\n </h5>\n <pre className=\"text-xs p-2 rounded bg-[var(--axis-surface-secondary)] overflow-x-auto\">\n <code className=\"text-[var(--axis-text-primary)]\">\n {JSON.stringify(input, null, 2)}\n </code>\n </pre>\n </div>\n )}\n {output !== undefined && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">\n 출력\n </h5>\n <pre className=\"text-xs p-2 rounded bg-[var(--axis-surface-secondary)] overflow-x-auto\">\n <code className=\"text-[var(--axis-text-primary)]\">\n {typeof output === \"string\" ? output : JSON.stringify(output, null, 2)}\n </code>\n </pre>\n </div>\n )}\n {error && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-error)] mb-1\">\n 에러\n </h5>\n <pre className=\"text-xs p-2 rounded bg-[var(--axis-surface-error)] overflow-x-auto\">\n <code className=\"text-[var(--axis-text-error)]\">{error}</code>\n </pre>\n </div>\n )}\n </div>\n )}\n </div>\n );\n}\n"
17
17
  }
18
18
  ]
19
19
  }
@@ -14,7 +14,7 @@
14
14
  {
15
15
  "path": "components/ui/dialog.tsx",
16
16
  "type": "registry:ui",
17
- "content": "import * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\n/** 다이얼로그의 루트 컴포넌트. 열림/닫힘 상태를 관리한다. */\nconst Dialog = DialogPrimitive.Root;\n\n/** 다이얼로그를 여는 트리거 버튼 컴포넌트 */\nconst DialogTrigger = DialogPrimitive.Trigger;\n\n/** 다이얼로그 콘텐츠를 포털로 렌더링하는 컴포넌트 */\nconst DialogPortal = DialogPrimitive.Portal;\n\n/** 다이얼로그를 닫는 버튼 컴포넌트 */\nconst DialogClose = DialogPrimitive.Close;\n\n/** 다이얼로그가 열릴 때 배경을 덮는 오버레이 컴포넌트. 페이드 인/아웃 애니메이션을 포함한다. */\nconst DialogOverlay = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n className={cn(\n \"fixed inset-0 z-50 bg-[var(--axis-dialog-overlay-bg)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n className\n )}\n {...props}\n />\n));\nDialogOverlay.displayName = \"DialogOverlay\";\n\n/** 다이얼로그의 본문 콘텐츠 컴포넌트. 화면 중앙에 위치하며, 오버레이와 함께 포털로 렌더링된다. */\nconst DialogContent = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n ref={ref}\n className={cn(\n \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-[shadow:var(--axis-shadow-lg)] duration-200\",\n \"bg-[var(--axis-dialog-content-bg)] border-[var(--axis-dialog-content-border)]\",\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]\",\n \"sm:rounded-lg\",\n className\n )}\n {...props}\n >\n {children}\n </DialogPrimitive.Content>\n </DialogPortal>\n));\nDialogContent.displayName = \"DialogContent\";\n\n/** 다이얼로그 상단 헤더 영역. 제목과 설명을 포함하는 레이아웃 컴포넌트이다. */\nconst DialogHeader = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col space-y-1.5 text-center sm:text-left\",\n className\n )}\n {...props}\n />\n);\nDialogHeader.displayName = \"DialogHeader\";\n\n/** 다이얼로그 하단 푸터 영역. 액션 버튼들을 배치하는 레이아웃 컴포넌트이다. */\nconst DialogFooter = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n className\n )}\n {...props}\n />\n);\nDialogFooter.displayName = \"DialogFooter\";\n\n/** 다이얼로그의 제목 컴포넌트. 접근성을 위해 aria-labelledby로 연결된다. */\nconst DialogTitle = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn(\n \"text-lg font-semibold leading-none tracking-tight text-[var(--axis-text-primary)]\",\n className\n )}\n {...props}\n />\n));\nDialogTitle.displayName = \"DialogTitle\";\n\n/** 다이얼로그의 설명 텍스트 컴포넌트. 접근성을 위해 aria-describedby로 연결된다. */\nconst DialogDescription = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn(\"text-sm text-[var(--axis-text-secondary)]\", className)}\n {...props}\n />\n));\nDialogDescription.displayName = \"DialogDescription\";\n\nexport {\n Dialog,\n DialogPortal,\n DialogOverlay,\n DialogClose,\n DialogTrigger,\n DialogContent,\n DialogHeader,\n DialogFooter,\n DialogTitle,\n DialogDescription,\n};\n"
17
+ "content": "import * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\n/** 다이얼로그의 루트 컴포넌트. 열림/닫힘 상태를 관리한다. */\nconst Dialog = DialogPrimitive.Root;\n\n/** 다이얼로그를 여는 트리거 버튼 컴포넌트 */\nconst DialogTrigger = DialogPrimitive.Trigger;\n\n/** 다이얼로그 콘텐츠를 포털로 렌더링하는 컴포넌트 */\nconst DialogPortal = DialogPrimitive.Portal;\n\n/** 다이얼로그를 닫는 버튼 컴포넌트 */\nconst DialogClose = DialogPrimitive.Close;\n\n/** 다이얼로그가 열릴 때 배경을 덮는 오버레이 컴포넌트. 페이드 인/아웃 애니메이션을 포함한다. */\nconst DialogOverlay = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n className={cn(\n \"fixed inset-0 z-50 bg-[var(--axis-dialog-overlay-bg)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 motion-reduce:!animate-none\",\n className\n )}\n {...props}\n />\n));\nDialogOverlay.displayName = \"DialogOverlay\";\n\n/** 다이얼로그의 본문 콘텐츠 컴포넌트. 화면 중앙에 위치하며, 오버레이와 함께 포털로 렌더링된다. */\nconst DialogContent = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n ref={ref}\n className={cn(\n \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-[shadow:var(--axis-shadow-lg)] duration-200\",\n \"bg-[var(--axis-dialog-content-bg)] border-[var(--axis-dialog-content-border)]\",\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] motion-reduce:!animate-none\",\n \"sm:rounded-lg\",\n className\n )}\n {...props}\n >\n {children}\n </DialogPrimitive.Content>\n </DialogPortal>\n));\nDialogContent.displayName = \"DialogContent\";\n\n/** 다이얼로그 상단 헤더 영역. 제목과 설명을 포함하는 레이아웃 컴포넌트이다. */\nconst DialogHeader = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col space-y-1.5 text-center sm:text-left\",\n className\n )}\n {...props}\n />\n);\nDialogHeader.displayName = \"DialogHeader\";\n\n/** 다이얼로그 하단 푸터 영역. 액션 버튼들을 배치하는 레이아웃 컴포넌트이다. */\nconst DialogFooter = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n className\n )}\n {...props}\n />\n);\nDialogFooter.displayName = \"DialogFooter\";\n\n/** 다이얼로그의 제목 컴포넌트. 접근성을 위해 aria-labelledby로 연결된다. */\nconst DialogTitle = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn(\n \"text-lg font-semibold leading-none tracking-tight text-[var(--axis-text-primary)]\",\n className\n )}\n {...props}\n />\n));\nDialogTitle.displayName = \"DialogTitle\";\n\n/** 다이얼로그의 설명 텍스트 컴포넌트. 접근성을 위해 aria-describedby로 연결된다. */\nconst DialogDescription = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn(\"text-sm text-[var(--axis-text-secondary)]\", className)}\n {...props}\n />\n));\nDialogDescription.displayName = \"DialogDescription\";\n\nexport {\n Dialog,\n DialogPortal,\n DialogOverlay,\n DialogClose,\n DialogTrigger,\n DialogContent,\n DialogHeader,\n DialogFooter,\n DialogTitle,\n DialogDescription,\n};\n"
18
18
  }
19
19
  ]
20
20
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "axis",
3
3
  "homepage": "https://axis-design-system.pages.dev",
4
- "version": "1.5.2",
4
+ "version": "1.6.2",
5
5
  "items": [
6
6
  {
7
7
  "name": "utils",
@@ -57,14 +57,14 @@
57
57
  "type": "registry:ui",
58
58
  "title": "Dialog",
59
59
  "description": "AXIS 다이얼로그 컴포넌트 - 모달 UI",
60
- "sha256": "75276f96133c560975559f776895bff594b4c3511b303f79ff3517a72d9e86ef"
60
+ "sha256": "cd2307dca13b830e0f34cfbe6ce800f1d4ee3a5a373bf9bf9dd7087758ea82b3"
61
61
  },
62
62
  {
63
63
  "name": "select",
64
64
  "type": "registry:ui",
65
65
  "title": "Select",
66
66
  "description": "AXIS 셀렉트 컴포넌트 - 드롭다운 선택",
67
- "sha256": "52dc633bc401b11da0a68b08c45c475292a1250aadff89357a25bd985542ff72"
67
+ "sha256": "3680a718874d05f153d8b46f8a9affbee9fe9bcf0509e20f4d6250c4e3307f81"
68
68
  },
69
69
  {
70
70
  "name": "tabs",
@@ -78,7 +78,7 @@
78
78
  "type": "registry:ui",
79
79
  "title": "Toast",
80
80
  "description": "AXIS 토스트 컴포넌트 - 알림 메시지",
81
- "sha256": "7be5ff031c88b39b955d8d574adb04a031ac01fdb606bd5ca6d38d730047201d"
81
+ "sha256": "0d5168f10be4223f9d31bf815045c88abbd57a96ab932b05388d43a26e82546c"
82
82
  },
83
83
  {
84
84
  "name": "agentic-a11y-labels",
@@ -92,14 +92,14 @@
92
92
  "type": "registry:ui",
93
93
  "title": "Run Progress",
94
94
  "description": "AI 에이전트 실행 진행률 표시 컴포넌트",
95
- "sha256": "7e9440054119958145eff6b3f82c814453ec80f42c23a47f83dcdf0de418df24"
95
+ "sha256": "bd197371ac791da7319dc72551d7fe86e020ae7f756852b40ede9123aa5d2186"
96
96
  },
97
97
  {
98
98
  "name": "agentic-streaming-text",
99
99
  "type": "registry:ui",
100
100
  "title": "Streaming Text",
101
101
  "description": "LLM 스트리밍 텍스트 출력 컴포넌트",
102
- "sha256": "e39ef3ef19e7ce5d0a22ba4c9d168c7cd219ea0234b5df75bf74cfa70c6a4056"
102
+ "sha256": "c40ac5afe7fd49b22bcc66aeebb77d23d7d8dd6b4b36f8fe4472750eaeae14d9"
103
103
  },
104
104
  {
105
105
  "name": "agentic-approval-card",
@@ -113,14 +113,14 @@
113
113
  "type": "registry:ui",
114
114
  "title": "Step Timeline",
115
115
  "description": "에이전트 실행 단계 타임라인 컴포넌트",
116
- "sha256": "536df739e23e098f57a3e8f5093edf46a55fba2dfa2525622dd73fc81b83a9ce"
116
+ "sha256": "2c93015b0ae012b517781c555297a79cc4c5a19842ab55ef7a847db5002c6875"
117
117
  },
118
118
  {
119
119
  "name": "agentic-thinking-indicator",
120
120
  "type": "registry:ui",
121
121
  "title": "Thinking Indicator",
122
122
  "description": "AI 사고 과정 표시 컴포넌트",
123
- "sha256": "5e495686fb1dfdb1f3a9b669269a41a0031fbb47cd75380dce294dc6aecafbbc"
123
+ "sha256": "0878b1f0b83b199239f4159beafc9a477ea1ad6c27a0587395e0e05dae85b3e6"
124
124
  },
125
125
  {
126
126
  "name": "agentic-source-panel",
@@ -148,21 +148,21 @@
148
148
  "type": "registry:ui",
149
149
  "title": "Surface Renderer",
150
150
  "description": "동적 UI Surface 렌더러 컴포넌트",
151
- "sha256": "3dcc39db1ee4f0538725ff9a543e428880c1dba9d45e02dcecd951f92b9f998a"
151
+ "sha256": "b822136654ec9e6da8c8fc88773bbbca5b0db6e93de7367cd0b6439cea1bc1ac"
152
152
  },
153
153
  {
154
154
  "name": "agentic-tool-call-card",
155
155
  "type": "registry:ui",
156
156
  "title": "Tool Call Card",
157
157
  "description": "도구 호출 결과 표시 카드 컴포넌트",
158
- "sha256": "4e387dbf052d5cddb90693d0eb2d1fcba6395e77c05ef8dad794ccce190f70ac"
158
+ "sha256": "b31a198cbb8aa9a84dd7475724abb1c75cfc57e98ce27ab6835b09f320f1f4a2"
159
159
  },
160
160
  {
161
161
  "name": "agentic-message-bubble",
162
162
  "type": "registry:ui",
163
163
  "title": "Message Bubble",
164
164
  "description": "대화 메시지 버블 컴포넌트 (user/assistant/system)",
165
- "sha256": "3bed0ec76f145b3aa01fe1efd2de9a35cfc4608429cc92968b639d8740081216"
165
+ "sha256": "9d5bebd9102dabc7afa8eb80c326abdae30ad174d1afd35cdccc9ce321ec90b9"
166
166
  },
167
167
  {
168
168
  "name": "agentic-feedback-buttons",
@@ -197,7 +197,7 @@
197
197
  "type": "registry:ui",
198
198
  "title": "Plan Card",
199
199
  "description": "AI 실행 계획 표시/승인 카드 컴포넌트",
200
- "sha256": "a702e9a023276fd63188eb38a86149f3e222f5224d938208fb0ae8fdb8e3a038"
200
+ "sha256": "c6d2f255b90b302966abd50091190b2af7e979bfe95e54141c538915b4b645e9"
201
201
  },
202
202
  {
203
203
  "name": "agentic-attachment-card",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://ui.shadcn.com/schema/registry.json",
3
3
  "name": "axis",
4
4
  "homepage": "https://axis-design-system.pages.dev",
5
- "version": "1.5.2",
5
+ "version": "1.6.2",
6
6
  "items": [
7
7
  {
8
8
  "name": "utils",
@@ -58,14 +58,14 @@
58
58
  "type": "registry:ui",
59
59
  "title": "Dialog",
60
60
  "description": "AXIS 다이얼로그 컴포넌트 - 모달 UI",
61
- "sha256": "75276f96133c560975559f776895bff594b4c3511b303f79ff3517a72d9e86ef"
61
+ "sha256": "cd2307dca13b830e0f34cfbe6ce800f1d4ee3a5a373bf9bf9dd7087758ea82b3"
62
62
  },
63
63
  {
64
64
  "name": "select",
65
65
  "type": "registry:ui",
66
66
  "title": "Select",
67
67
  "description": "AXIS 셀렉트 컴포넌트 - 드롭다운 선택",
68
- "sha256": "52dc633bc401b11da0a68b08c45c475292a1250aadff89357a25bd985542ff72"
68
+ "sha256": "3680a718874d05f153d8b46f8a9affbee9fe9bcf0509e20f4d6250c4e3307f81"
69
69
  },
70
70
  {
71
71
  "name": "tabs",
@@ -79,7 +79,7 @@
79
79
  "type": "registry:ui",
80
80
  "title": "Toast",
81
81
  "description": "AXIS 토스트 컴포넌트 - 알림 메시지",
82
- "sha256": "7be5ff031c88b39b955d8d574adb04a031ac01fdb606bd5ca6d38d730047201d"
82
+ "sha256": "0d5168f10be4223f9d31bf815045c88abbd57a96ab932b05388d43a26e82546c"
83
83
  },
84
84
  {
85
85
  "name": "agentic-a11y-labels",
@@ -93,14 +93,14 @@
93
93
  "type": "registry:ui",
94
94
  "title": "Run Progress",
95
95
  "description": "AI 에이전트 실행 진행률 표시 컴포넌트",
96
- "sha256": "7e9440054119958145eff6b3f82c814453ec80f42c23a47f83dcdf0de418df24"
96
+ "sha256": "bd197371ac791da7319dc72551d7fe86e020ae7f756852b40ede9123aa5d2186"
97
97
  },
98
98
  {
99
99
  "name": "agentic-streaming-text",
100
100
  "type": "registry:ui",
101
101
  "title": "Streaming Text",
102
102
  "description": "LLM 스트리밍 텍스트 출력 컴포넌트",
103
- "sha256": "e39ef3ef19e7ce5d0a22ba4c9d168c7cd219ea0234b5df75bf74cfa70c6a4056"
103
+ "sha256": "c40ac5afe7fd49b22bcc66aeebb77d23d7d8dd6b4b36f8fe4472750eaeae14d9"
104
104
  },
105
105
  {
106
106
  "name": "agentic-approval-card",
@@ -114,14 +114,14 @@
114
114
  "type": "registry:ui",
115
115
  "title": "Step Timeline",
116
116
  "description": "에이전트 실행 단계 타임라인 컴포넌트",
117
- "sha256": "536df739e23e098f57a3e8f5093edf46a55fba2dfa2525622dd73fc81b83a9ce"
117
+ "sha256": "2c93015b0ae012b517781c555297a79cc4c5a19842ab55ef7a847db5002c6875"
118
118
  },
119
119
  {
120
120
  "name": "agentic-thinking-indicator",
121
121
  "type": "registry:ui",
122
122
  "title": "Thinking Indicator",
123
123
  "description": "AI 사고 과정 표시 컴포넌트",
124
- "sha256": "5e495686fb1dfdb1f3a9b669269a41a0031fbb47cd75380dce294dc6aecafbbc"
124
+ "sha256": "0878b1f0b83b199239f4159beafc9a477ea1ad6c27a0587395e0e05dae85b3e6"
125
125
  },
126
126
  {
127
127
  "name": "agentic-source-panel",
@@ -149,21 +149,21 @@
149
149
  "type": "registry:ui",
150
150
  "title": "Surface Renderer",
151
151
  "description": "동적 UI Surface 렌더러 컴포넌트",
152
- "sha256": "3dcc39db1ee4f0538725ff9a543e428880c1dba9d45e02dcecd951f92b9f998a"
152
+ "sha256": "b822136654ec9e6da8c8fc88773bbbca5b0db6e93de7367cd0b6439cea1bc1ac"
153
153
  },
154
154
  {
155
155
  "name": "agentic-tool-call-card",
156
156
  "type": "registry:ui",
157
157
  "title": "Tool Call Card",
158
158
  "description": "도구 호출 결과 표시 카드 컴포넌트",
159
- "sha256": "4e387dbf052d5cddb90693d0eb2d1fcba6395e77c05ef8dad794ccce190f70ac"
159
+ "sha256": "b31a198cbb8aa9a84dd7475724abb1c75cfc57e98ce27ab6835b09f320f1f4a2"
160
160
  },
161
161
  {
162
162
  "name": "agentic-message-bubble",
163
163
  "type": "registry:ui",
164
164
  "title": "Message Bubble",
165
165
  "description": "대화 메시지 버블 컴포넌트 (user/assistant/system)",
166
- "sha256": "3bed0ec76f145b3aa01fe1efd2de9a35cfc4608429cc92968b639d8740081216"
166
+ "sha256": "9d5bebd9102dabc7afa8eb80c326abdae30ad174d1afd35cdccc9ce321ec90b9"
167
167
  },
168
168
  {
169
169
  "name": "agentic-feedback-buttons",
@@ -198,7 +198,7 @@
198
198
  "type": "registry:ui",
199
199
  "title": "Plan Card",
200
200
  "description": "AI 실행 계획 표시/승인 카드 컴포넌트",
201
- "sha256": "a702e9a023276fd63188eb38a86149f3e222f5224d938208fb0ae8fdb8e3a038"
201
+ "sha256": "c6d2f255b90b302966abd50091190b2af7e979bfe95e54141c538915b4b645e9"
202
202
  },
203
203
  {
204
204
  "name": "agentic-attachment-card",
@@ -14,7 +14,7 @@
14
14
  {
15
15
  "path": "components/ui/select.tsx",
16
16
  "type": "registry:ui",
17
- "content": "import * as React from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\n/** 셀렉트의 루트 컴포넌트. 열림/닫힘 및 선택 상태를 관리한다. */\nconst Select = SelectPrimitive.Root;\n\n/** 셀렉트 옵션들을 논리적으로 그룹화하는 컴포넌트 */\nconst SelectGroup = SelectPrimitive.Group;\n\n/** 셀렉트 트리거 내에서 현재 선택된 값을 표시하는 컴포넌트 */\nconst SelectValue = SelectPrimitive.Value;\n\n/** 셀렉트 드롭다운을 여는 트리거 버튼 컴포넌트. 선택된 값과 하향 화살표 아이콘을 표시한다. */\nconst SelectTrigger = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n <SelectPrimitive.Trigger\n ref={ref}\n className={cn(\n \"flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm\",\n \"bg-[var(--axis-input-bg-default)] border-[var(--axis-input-border-default)]\",\n \"placeholder:text-[var(--axis-input-text-placeholder)]\",\n \"hover:border-[var(--axis-input-border-hover)]\",\n \"focus:outline-none focus:ring-2 focus:ring-[var(--axis-input-border-focus)] focus:ring-offset-2\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"[&>span]:line-clamp-1\",\n className\n )}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <svg\n className=\"h-4 w-4 opacity-50\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 9l-7 7-7-7\"\n />\n </svg>\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = \"SelectTrigger\";\n\n/** 셀렉트 드롭다운의 본문 콘텐츠 컴포넌트. 포털을 통해 렌더링되며 진입/퇴장 애니메이션을 포함한다. */\nconst SelectContent = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n ref={ref}\n className={cn(\n \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\",\n position === \"popper\" &&\n \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n className\n )}\n position={position}\n {...props}\n >\n <SelectPrimitive.Viewport\n className={cn(\n \"p-1\",\n position === \"popper\" &&\n \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\"\n )}\n >\n {children}\n </SelectPrimitive.Viewport>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n));\nSelectContent.displayName = \"SelectContent\";\n\n/** 셀렉트 옵션 그룹의 레이블 컴포넌트. 비선택 텍스트로 그룹 제목을 표시한다. */\nconst SelectLabel = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Label>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n <SelectPrimitive.Label\n ref={ref}\n className={cn(\n \"py-1.5 pl-8 pr-2 text-sm font-semibold text-[var(--axis-text-secondary)]\",\n className\n )}\n {...props}\n />\n));\nSelectLabel.displayName = \"SelectLabel\";\n\n/** 셀렉트 드롭다운의 개별 선택 항목 컴포넌트. 선택 시 좌측에 체크 아이콘이 표시된다. */\nconst SelectItem = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n <SelectPrimitive.Item\n ref={ref}\n className={cn(\n \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none\",\n \"text-[var(--axis-text-primary)]\",\n \"focus:bg-[var(--axis-surface-secondary)] focus:text-[var(--axis-text-primary)]\",\n \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n className\n )}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <SelectPrimitive.ItemIndicator>\n <svg\n className=\"h-4 w-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </SelectPrimitive.ItemIndicator>\n </span>\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n </SelectPrimitive.Item>\n));\nSelectItem.displayName = \"SelectItem\";\n\n/** 셀렉트 옵션 사이의 구분선 컴포넌트 */\nconst SelectSeparator = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n <SelectPrimitive.Separator\n ref={ref}\n className={cn(\"-mx-1 my-1 h-px bg-[var(--axis-border-default)]\", className)}\n {...props}\n />\n));\nSelectSeparator.displayName = \"SelectSeparator\";\n\nexport {\n Select,\n SelectGroup,\n SelectValue,\n SelectTrigger,\n SelectContent,\n SelectLabel,\n SelectItem,\n SelectSeparator,\n};\n"
17
+ "content": "import * as React from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\n/** 셀렉트의 루트 컴포넌트. 열림/닫힘 및 선택 상태를 관리한다. */\nconst Select = SelectPrimitive.Root;\n\n/** 셀렉트 옵션들을 논리적으로 그룹화하는 컴포넌트 */\nconst SelectGroup = SelectPrimitive.Group;\n\n/** 셀렉트 트리거 내에서 현재 선택된 값을 표시하는 컴포넌트 */\nconst SelectValue = SelectPrimitive.Value;\n\n/** 셀렉트 드롭다운을 여는 트리거 버튼 컴포넌트. 선택된 값과 하향 화살표 아이콘을 표시한다. */\nconst SelectTrigger = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n <SelectPrimitive.Trigger\n ref={ref}\n className={cn(\n \"flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm\",\n \"bg-[var(--axis-input-bg-default)] border-[var(--axis-input-border-default)]\",\n \"placeholder:text-[var(--axis-input-text-placeholder)]\",\n \"hover:border-[var(--axis-input-border-hover)]\",\n \"focus:outline-none focus:ring-2 focus:ring-[var(--axis-input-border-focus)] focus:ring-offset-2\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"[&>span]:line-clamp-1\",\n className\n )}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <svg\n className=\"h-4 w-4 opacity-50\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 9l-7 7-7-7\"\n />\n </svg>\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = \"SelectTrigger\";\n\n/** 셀렉트 드롭다운의 본문 콘텐츠 컴포넌트. 포털을 통해 렌더링되며 진입/퇴장 애니메이션을 포함한다. */\nconst SelectContent = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n ref={ref}\n className={cn(\n \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 motion-reduce:!animate-none\",\n position === \"popper\" &&\n \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n className\n )}\n position={position}\n {...props}\n >\n <SelectPrimitive.Viewport\n className={cn(\n \"p-1\",\n position === \"popper\" &&\n \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\"\n )}\n >\n {children}\n </SelectPrimitive.Viewport>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n));\nSelectContent.displayName = \"SelectContent\";\n\n/** 셀렉트 옵션 그룹의 레이블 컴포넌트. 비선택 텍스트로 그룹 제목을 표시한다. */\nconst SelectLabel = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Label>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n <SelectPrimitive.Label\n ref={ref}\n className={cn(\n \"py-1.5 pl-8 pr-2 text-sm font-semibold text-[var(--axis-text-secondary)]\",\n className\n )}\n {...props}\n />\n));\nSelectLabel.displayName = \"SelectLabel\";\n\n/** 셀렉트 드롭다운의 개별 선택 항목 컴포넌트. 선택 시 좌측에 체크 아이콘이 표시된다. */\nconst SelectItem = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n <SelectPrimitive.Item\n ref={ref}\n className={cn(\n \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none\",\n \"text-[var(--axis-text-primary)]\",\n \"focus:bg-[var(--axis-surface-secondary)] focus:text-[var(--axis-text-primary)]\",\n \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n className\n )}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <SelectPrimitive.ItemIndicator>\n <svg\n className=\"h-4 w-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </SelectPrimitive.ItemIndicator>\n </span>\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n </SelectPrimitive.Item>\n));\nSelectItem.displayName = \"SelectItem\";\n\n/** 셀렉트 옵션 사이의 구분선 컴포넌트 */\nconst SelectSeparator = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n <SelectPrimitive.Separator\n ref={ref}\n className={cn(\"-mx-1 my-1 h-px bg-[var(--axis-border-default)]\", className)}\n {...props}\n />\n));\nSelectSeparator.displayName = \"SelectSeparator\";\n\nexport {\n Select,\n SelectGroup,\n SelectValue,\n SelectTrigger,\n SelectContent,\n SelectLabel,\n SelectItem,\n SelectSeparator,\n};\n"
18
18
  }
19
19
  ]
20
20
  }
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "path": "components/ui/toast.tsx",
17
17
  "type": "registry:ui",
18
- "content": "import * as React from \"react\";\nimport * as ToastPrimitives from \"@radix-ui/react-toast\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\n/** 토스트 알림 시스템의 프로바이더 컴포넌트. 앱 루트에 배치하여 토스트 기능을 활성화한다. */\nconst ToastProvider = ToastPrimitives.Provider;\n\n/** 토스트 알림이 표시되는 뷰포트 영역. 화면 하단 우측에 고정 위치로 렌더링된다. */\nconst ToastViewport = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Viewport>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Viewport\n ref={ref}\n className={cn(\n \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]\",\n className\n )}\n {...props}\n />\n));\nToastViewport.displayName = ToastPrimitives.Viewport.displayName;\n\nconst toastVariants = cva(\n \"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full\",\n {\n variants: {\n variant: {\n default:\n \"border-[var(--axis-border-default)] bg-[var(--axis-surface-default)] text-[var(--axis-text-primary)]\",\n success:\n \"border-[var(--axis-border-success)] bg-[var(--axis-surface-success)] text-[var(--axis-text-success)]\",\n error:\n \"border-[var(--axis-border-error)] bg-[var(--axis-surface-error)] text-[var(--axis-text-error)]\",\n warning:\n \"border-transparent bg-[var(--axis-surface-warning)] text-[var(--axis-text-warning)]\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n);\n\n/** 토스트 알림의 루트 컴포넌트. default, success, error, warning 변형을 지원하며 스와이프로 닫을 수 있다. */\nconst Toast = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Root>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => (\n <ToastPrimitives.Root\n ref={ref}\n className={cn(toastVariants({ variant }), className)}\n {...props}\n />\n));\nToast.displayName = ToastPrimitives.Root.displayName;\n\n/** 토스트 내에 표시되는 액션 버튼 컴포넌트. 사용자 상호작용을 위한 CTA를 제공한다. */\nconst ToastAction = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Action>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Action\n ref={ref}\n className={cn(\n \"inline-flex h-8 shrink-0 items-center justify-center rounded-md border px-3 text-sm font-medium transition-colors\",\n \"border-[var(--axis-border-default)] bg-transparent\",\n \"hover:bg-[var(--axis-surface-secondary)]\",\n \"focus:outline-none focus:ring-2 focus:ring-[var(--axis-border-focus)]\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n className\n )}\n {...props}\n />\n));\nToastAction.displayName = ToastPrimitives.Action.displayName;\n\n/** 토스트를 닫는 닫기 버튼 컴포넌트. 호버 시 표시되며 X 아이콘을 포함한다. */\nconst ToastClose = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Close>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Close\n ref={ref}\n className={cn(\n \"absolute right-2 top-2 rounded-md p-1 text-[var(--axis-text-secondary)] opacity-0 transition-opacity\",\n \"hover:text-[var(--axis-text-primary)]\",\n \"focus:opacity-100 focus:outline-none focus:ring-2\",\n \"group-hover:opacity-100\",\n className\n )}\n toast-close=\"\"\n {...props}\n >\n <svg className=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </ToastPrimitives.Close>\n));\nToastClose.displayName = ToastPrimitives.Close.displayName;\n\n/** 토스트의 제목을 표시하는 컴포넌트 */\nconst ToastTitle = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Title>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Title\n ref={ref}\n className={cn(\"text-sm font-semibold\", className)}\n {...props}\n />\n));\nToastTitle.displayName = ToastPrimitives.Title.displayName;\n\n/** 토스트의 상세 설명을 표시하는 컴포넌트 */\nconst ToastDescription = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Description>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Description\n ref={ref}\n className={cn(\"text-sm opacity-90\", className)}\n {...props}\n />\n));\nToastDescription.displayName = ToastPrimitives.Description.displayName;\n\nexport {\n ToastProvider,\n ToastViewport,\n Toast,\n ToastTitle,\n ToastDescription,\n ToastClose,\n ToastAction,\n};\n"
18
+ "content": "import * as React from \"react\";\nimport * as ToastPrimitives from \"@radix-ui/react-toast\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\n/** 토스트 알림 시스템의 프로바이더 컴포넌트. 앱 루트에 배치하여 토스트 기능을 활성화한다. */\nconst ToastProvider = ToastPrimitives.Provider;\n\n/** 토스트 알림이 표시되는 뷰포트 영역. 화면 하단 우측에 고정 위치로 렌더링된다. */\nconst ToastViewport = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Viewport>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Viewport\n ref={ref}\n className={cn(\n \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]\",\n className\n )}\n {...props}\n />\n));\nToastViewport.displayName = ToastPrimitives.Viewport.displayName;\n\nconst toastVariants = cva(\n \"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full motion-reduce:!animate-none\",\n {\n variants: {\n variant: {\n default:\n \"border-[var(--axis-border-default)] bg-[var(--axis-surface-default)] text-[var(--axis-text-primary)]\",\n success:\n \"border-[var(--axis-border-success)] bg-[var(--axis-surface-success)] text-[var(--axis-text-success)]\",\n error:\n \"border-[var(--axis-border-error)] bg-[var(--axis-surface-error)] text-[var(--axis-text-error)]\",\n warning:\n \"border-transparent bg-[var(--axis-surface-warning)] text-[var(--axis-text-warning)]\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n);\n\n/** 토스트 알림의 루트 컴포넌트. default, success, error, warning 변형을 지원하며 스와이프로 닫을 수 있다. */\nconst Toast = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Root>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => (\n <ToastPrimitives.Root\n ref={ref}\n className={cn(toastVariants({ variant }), className)}\n {...props}\n />\n));\nToast.displayName = ToastPrimitives.Root.displayName;\n\n/** 토스트 내에 표시되는 액션 버튼 컴포넌트. 사용자 상호작용을 위한 CTA를 제공한다. */\nconst ToastAction = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Action>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Action\n ref={ref}\n className={cn(\n \"inline-flex h-8 shrink-0 items-center justify-center rounded-md border px-3 text-sm font-medium transition-colors\",\n \"border-[var(--axis-border-default)] bg-transparent\",\n \"hover:bg-[var(--axis-surface-secondary)]\",\n \"focus:outline-none focus:ring-2 focus:ring-[var(--axis-border-focus)]\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n className\n )}\n {...props}\n />\n));\nToastAction.displayName = ToastPrimitives.Action.displayName;\n\n/** 토스트를 닫는 닫기 버튼 컴포넌트. 호버 시 표시되며 X 아이콘을 포함한다. */\nconst ToastClose = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Close>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Close\n ref={ref}\n className={cn(\n \"absolute right-2 top-2 rounded-md p-1 text-[var(--axis-text-secondary)] opacity-0 transition-opacity\",\n \"hover:text-[var(--axis-text-primary)]\",\n \"focus:opacity-100 focus:outline-none focus:ring-2\",\n \"group-hover:opacity-100\",\n className\n )}\n toast-close=\"\"\n {...props}\n >\n <svg className=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </ToastPrimitives.Close>\n));\nToastClose.displayName = ToastPrimitives.Close.displayName;\n\n/** 토스트의 제목을 표시하는 컴포넌트 */\nconst ToastTitle = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Title>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Title\n ref={ref}\n className={cn(\"text-sm font-semibold\", className)}\n {...props}\n />\n));\nToastTitle.displayName = ToastPrimitives.Title.displayName;\n\n/** 토스트의 상세 설명을 표시하는 컴포넌트 */\nconst ToastDescription = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Description>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Description\n ref={ref}\n className={cn(\"text-sm opacity-90\", className)}\n {...props}\n />\n));\nToastDescription.displayName = ToastPrimitives.Description.displayName;\n\nexport {\n ToastProvider,\n ToastViewport,\n Toast,\n ToastTitle,\n ToastDescription,\n ToastClose,\n ToastAction,\n};\n"
19
19
  }
20
20
  ]
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axis-ds/mcp",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "type": "module",
5
5
  "description": "AXIS Design System - MCP Server for AI-assisted component discovery",
6
6
  "bin": {
@@ -50,6 +50,7 @@
50
50
  "dev": "tsup src/index.ts --format esm --watch",
51
51
  "clean": "rm -rf dist",
52
52
  "test": "vitest run",
53
- "type-check": "tsc --noEmit"
53
+ "type-check": "tsc --noEmit",
54
+ "lint": "eslint ."
54
55
  }
55
56
  }