@axis-ds/mcp 1.3.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -10,61 +10,67 @@ import { z } from "zod";
10
10
  import { readFileSync } from "fs";
11
11
  import { join, dirname } from "path";
12
12
  import { fileURLToPath } from "url";
13
+
14
+ // src/types.ts
15
+ var TOKEN_CATEGORIES = [
16
+ "color",
17
+ "typography",
18
+ "spacing",
19
+ "radius",
20
+ "shadow",
21
+ "density",
22
+ "component"
23
+ ];
24
+
25
+ // src/registry/loader.ts
13
26
  var __filename = fileURLToPath(import.meta.url);
14
27
  var __dirname = dirname(__filename);
15
28
  var DATA_DIR = join(__dirname, "data");
16
- var cachedRegistry = null;
29
+ var cachedComponents = null;
30
+ var cachedTokens = null;
17
31
  function loadComponents() {
18
- const filePath = join(DATA_DIR, "components.json");
19
- const content = readFileSync(filePath, "utf-8");
20
- const data = JSON.parse(content);
21
- return data.components;
32
+ if (!cachedComponents) {
33
+ const filePath = join(DATA_DIR, "components.json");
34
+ const content = readFileSync(filePath, "utf-8");
35
+ const data = JSON.parse(content);
36
+ cachedComponents = data.components;
37
+ }
38
+ return cachedComponents;
22
39
  }
23
40
  function loadTokens() {
24
- const filePath = join(DATA_DIR, "tokens.json");
25
- const content = readFileSync(filePath, "utf-8");
26
- const data = JSON.parse(content);
27
- return data.tokens;
28
- }
29
- function getRegistry() {
30
- if (!cachedRegistry) {
31
- cachedRegistry = {
32
- components: loadComponents(),
33
- tokens: loadTokens()
34
- };
41
+ if (!cachedTokens) {
42
+ const filePath = join(DATA_DIR, "tokens.json");
43
+ const content = readFileSync(filePath, "utf-8");
44
+ const data = JSON.parse(content);
45
+ cachedTokens = { source: data.source, tokens: data.tokens };
35
46
  }
36
- return cachedRegistry;
47
+ return cachedTokens;
37
48
  }
38
49
  function getComponentMeta(name) {
39
- const registry = getRegistry();
40
- return registry.components.find(
50
+ return loadComponents().find(
41
51
  (c) => c.name.toLowerCase() === name.toLowerCase()
42
52
  );
43
53
  }
44
54
  function listTokens(category) {
45
- const registry = getRegistry();
55
+ const { tokens } = loadTokens();
46
56
  if (category) {
47
- return registry.tokens.filter((t) => t.category === category);
57
+ return tokens.filter((t) => t.category === category);
48
58
  }
49
- return registry.tokens;
59
+ return tokens;
50
60
  }
51
61
  function getToken(path11) {
52
- const registry = getRegistry();
53
- return registry.tokens.find(
54
- (t) => t.path.toLowerCase() === path11.toLowerCase()
62
+ const q = path11.trim().toLowerCase();
63
+ return loadTokens().tokens.find(
64
+ (t) => t.path.toLowerCase() === q || (t.aliases ?? []).some((a) => a.toLowerCase() === q) || t.cssVariable.toLowerCase() === q
55
65
  );
56
66
  }
67
+ function getTokenSource() {
68
+ return loadTokens().source;
69
+ }
57
70
  function getTokenStats() {
58
- const registry = getRegistry();
59
- const stats = {
60
- color: 0,
61
- typography: 0,
62
- spacing: 0,
63
- radius: 0,
64
- shadow: 0,
65
- animation: 0
66
- };
67
- for (const token of registry.tokens) {
71
+ const { tokens } = loadTokens();
72
+ const stats = Object.fromEntries(TOKEN_CATEGORIES.map((c) => [c, 0]));
73
+ for (const token of tokens) {
68
74
  stats[token.category]++;
69
75
  }
70
76
  return stats;
@@ -1052,22 +1058,17 @@ function majorFromVersion(version) {
1052
1058
  }
1053
1059
  function detectTailwind(baseDirInput, pkg) {
1054
1060
  const baseDir = resolve(baseDirInput);
1055
- const installedFile = join2(baseDir, "node_modules", "tailwindcss", "package.json");
1056
1061
  const declared = pkg ? hasDependency2(pkg, "tailwindcss") : void 0;
1057
1062
  const bounds = declared !== void 0 ? tailwindMajorBounds(declared) : void 0;
1058
1063
  const declaredMajor = bounds?.lower;
1059
- try {
1060
- const installed = JSON.parse(readFileSync4(installedFile, "utf8"));
1061
- const version = isPlainObject2(installed) ? installed.version : void 0;
1062
- const major = majorFromVersion(version);
1063
- if (major !== void 0) {
1064
- const found = { major, source: "installed", value: String(version) };
1065
- if (declared !== void 0 && bounds !== void 0 && !majorWithin(major, bounds)) {
1066
- found.mismatch = { declared, declaredMajor: bounds.lower };
1067
- }
1068
- return found;
1064
+ const version = isPnpProject(baseDir) ? void 0 : findInstalledPackage(baseDir, "tailwindcss")?.version;
1065
+ const major = majorFromVersion(version);
1066
+ if (version !== void 0 && major !== void 0) {
1067
+ const found = { major, source: "installed", value: version };
1068
+ if (declared !== void 0 && bounds !== void 0 && !majorWithin(major, bounds)) {
1069
+ found.mismatch = { declared, declaredMajor: bounds.lower };
1069
1070
  }
1070
- } catch {
1071
+ return found;
1071
1072
  }
1072
1073
  if (declared !== void 0) {
1073
1074
  if (declaredMajor !== void 0) return { major: declaredMajor, source: "declared", value: declared };
@@ -1210,19 +1211,9 @@ function rangeLowerAtLeast(range, min) {
1210
1211
  if (!lower || !target) return void 0;
1211
1212
  return compareLower(lower, target) >= 0;
1212
1213
  }
1213
- function exactVersion(version) {
1214
- if (typeof version !== "string") return void 0;
1215
- const v = version.trim();
1216
- if (!/^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(v) || /^v?0\.0\.0-/.test(v)) return void 0;
1217
- return v;
1218
- }
1219
- function installedVersion(baseDir, name) {
1220
- try {
1221
- const parsed = JSON.parse(readFileSync4(join2(baseDir, "node_modules", ...name.split("/"), "package.json"), "utf8"));
1222
- return isPlainObject2(parsed) ? exactVersion(parsed.version) : void 0;
1223
- } catch {
1224
- return void 0;
1225
- }
1214
+ function installedAxisVersion(baseDir, name) {
1215
+ if (isPnpProject(baseDir)) return void 0;
1216
+ return findInstalledPackage(baseDir, name)?.version;
1226
1217
  }
1227
1218
  function axisPackagesBelowTailwind4(pkg, baseDirInput) {
1228
1219
  if (!pkg) return [];
@@ -1232,7 +1223,7 @@ function axisPackagesBelowTailwind4(pkg, baseDirInput) {
1232
1223
  for (const name of TAILWIND4_AXIS_PACKAGES) {
1233
1224
  const range = hasDependency2(pkg, name);
1234
1225
  if (range === void 0 || range.trim().startsWith("workspace:")) continue;
1235
- const installed = baseDir !== void 0 ? installedVersion(baseDir, name) : void 0;
1226
+ const installed = baseDir !== void 0 ? installedAxisVersion(baseDir, name) : void 0;
1236
1227
  const installedLower = installed !== void 0 ? lowerOf(installed) : void 0;
1237
1228
  if (installed !== void 0 && installedLower !== void 0) {
1238
1229
  if (compareLower(installedLower, min) < 0) out.push({ name, range, status: "below", source: "installed", installed });
@@ -2727,20 +2718,36 @@ function formatComponentDetail(result) {
2727
2718
  }
2728
2719
  return lines.join("\n");
2729
2720
  }
2730
- function formatTokenDetail(token) {
2721
+ function cssUsage(token) {
2722
+ const shadcnColor = !token.cssVariable.startsWith("--axis-") && token.value.startsWith("hsl(");
2723
+ return shadcnColor ? `hsl(var(${token.cssVariable}))` : `var(${token.cssVariable})`;
2724
+ }
2725
+ function formatTokenNotFound(path11) {
2726
+ return [
2727
+ `\uD1A0\uD070\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: \`${path11}\``,
2728
+ "",
2729
+ "- \uACBD\uB85C\uB294 `axis.list_tokens` \uB85C \uD655\uC778\uD558\uC138\uC694. \uACBD\uB85C \xB7 tokens \uC774\uB984 \uAE30\uBC18 \uACBD\uB85C(\uC608: `control.h.md`) \xB7 CSS \uBCC0\uC218 \uC774\uB984(\uC608: `--axis-control-h-md`)\uC744 \uBC1B\uC2B5\uB2C8\uB2E4.",
2730
+ "- \uBE0C\uB79C\uB4DC \uAC12\uC740 `axis.list_brands` \xB7 `axis.get_brand` \uB85C \uC870\uD68C\uD558\uC138\uC694."
2731
+ ].join("\n");
2732
+ }
2733
+ function formatTokenDetail(token, query = "") {
2731
2734
  if (!token) {
2732
- return "\uD1A0\uD070\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.";
2735
+ return formatTokenNotFound(query);
2733
2736
  }
2734
2737
  const lines = [];
2735
2738
  lines.push(`# \uD1A0\uD070: ${token.path}`);
2736
2739
  lines.push("");
2737
2740
  lines.push(`- **\uAC12**: \`${token.value}\``);
2741
+ if (token.dark !== void 0) {
2742
+ lines.push(`- **\uB2E4\uD06C \uAC12**: \`${token.dark}\`${token.dark === token.value ? " (\uB77C\uC774\uD2B8\uC640 \uAC19\uC74C)" : ""}`);
2743
+ }
2738
2744
  lines.push(`- **\uCE74\uD14C\uACE0\uB9AC**: ${token.category}`);
2739
2745
  if (token.description) {
2740
2746
  lines.push(`- **\uC124\uBA85**: ${token.description}`);
2741
2747
  }
2742
- if (token.cssVariable) {
2743
- lines.push(`- **CSS \uBCC0\uC218**: \`var(${token.cssVariable})\``);
2748
+ lines.push(`- **CSS \uBCC0\uC218**: \`${token.cssVariable}\` (\uC0AC\uC6A9: \`${cssUsage(token)}\`)`);
2749
+ if (token.aliases && token.aliases.length > 0) {
2750
+ lines.push(`- **\uD568\uAED8 \uBC1B\uB294 \uACBD\uB85C**: ${token.aliases.map((a) => `\`${a}\``).join(", ")}`);
2744
2751
  }
2745
2752
  return lines.join("\n");
2746
2753
  }
@@ -2776,7 +2783,8 @@ function handleListTokens(params) {
2776
2783
  tokens,
2777
2784
  totalCount: tokens.length,
2778
2785
  stats,
2779
- category
2786
+ category,
2787
+ sourceVersion: getTokenSource()?.version
2780
2788
  };
2781
2789
  }
2782
2790
  function formatComponentList(result) {
@@ -2821,41 +2829,51 @@ function formatComponentList(result) {
2821
2829
  lines.push("\uC124\uCE58: `axis.install_component` \uC5D0 `name`(\uB610\uB294 `names`)\uACFC \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8 `targetDir` \uB97C \uC8FC\uC138\uC694. \uC774\uB984\uC740 \uC704 \uD45C\uC758 \uC774\uB984 \uADF8\uB300\uB85C\uC785\uB2C8\uB2E4(agentic \uC740 `agentic-` \uC811\uB450 \uD3EC\uD568).");
2822
2830
  return lines.join("\n");
2823
2831
  }
2832
+ var TOKEN_CATEGORY_LABELS = {
2833
+ color: "Color",
2834
+ typography: "Typography",
2835
+ spacing: "Spacing",
2836
+ radius: "Radius",
2837
+ shadow: "Shadow",
2838
+ density: "Density (\uBC00\uB3C4)",
2839
+ component: "Component"
2840
+ };
2841
+ var cell = (v) => v.replace(/\|/g, "\\|");
2824
2842
  function formatTokenList(result) {
2825
2843
  const lines = [];
2844
+ const total = TOKEN_CATEGORIES.reduce((n, c) => n + result.stats[c], 0);
2826
2845
  lines.push("# AXIS Design System \uD1A0\uD070 \uBAA9\uB85D");
2827
2846
  lines.push("");
2847
+ if (result.sourceVersion) {
2848
+ lines.push(`- **\uCD9C\uCC98**: @axis-ds/tokens ${result.sourceVersion} \uBE4C\uB4DC \uC0B0\uCD9C\uBB3C\uC5D0\uC11C \uC0DD\uC131(\uB77C\uC774\uD2B8 :root \xB7 \uB2E4\uD06C .dark)`);
2849
+ }
2850
+ lines.push("- **\uBE0C\uB79C\uB4DC \uAC12**: `axis.list_brands` \xB7 `axis.get_brand` \uB85C \uC870\uD68C\uD558\uC138\uC694(\uB300\uC0C1 \uD504\uB85C\uC81D\uD2B8\uC5D0 \uC124\uCE58\uB41C tokens \uB97C \uC77D\uC74C)");
2851
+ lines.push("");
2828
2852
  lines.push("## \uCE74\uD14C\uACE0\uB9AC\uBCC4 \uD1B5\uACC4");
2829
2853
  lines.push("| \uCE74\uD14C\uACE0\uB9AC | \uAC1C\uC218 |");
2830
2854
  lines.push("|----------|------|");
2831
- lines.push(`| Color | ${result.stats.color} |`);
2832
- lines.push(`| Typography | ${result.stats.typography} |`);
2833
- lines.push(`| Spacing | ${result.stats.spacing} |`);
2834
- lines.push(`| Radius | ${result.stats.radius} |`);
2835
- lines.push(`| Shadow | ${result.stats.shadow} |`);
2836
- lines.push(`| Animation | ${result.stats.animation} |`);
2837
- lines.push(`| **\uD569\uACC4** | **${result.totalCount}** |`);
2855
+ for (const c of TOKEN_CATEGORIES) {
2856
+ lines.push(`| ${TOKEN_CATEGORY_LABELS[c]} | ${result.stats[c]} |`);
2857
+ }
2858
+ lines.push(`| **\uD569\uACC4** | **${total}** |`);
2838
2859
  lines.push("");
2839
2860
  if (result.category) {
2840
- lines.push(`## ${result.category} \uD1A0\uD070`);
2861
+ lines.push(`## ${result.category} \uD1A0\uD070 (${result.totalCount}\uAC1C)`);
2841
2862
  } else {
2842
2863
  lines.push("## \uC804\uCCB4 \uD1A0\uD070");
2843
2864
  }
2844
2865
  lines.push("");
2845
- const grouped = {};
2846
- for (const token of result.tokens) {
2847
- if (!grouped[token.category]) {
2848
- grouped[token.category] = [];
2849
- }
2850
- grouped[token.category].push(token);
2851
- }
2852
- for (const [category, tokens] of Object.entries(grouped)) {
2853
- lines.push(`### ${category.charAt(0).toUpperCase() + category.slice(1)}`);
2854
- lines.push("| \uACBD\uB85C | \uAC12 | \uC124\uBA85 |");
2855
- lines.push("|------|-----|------|");
2866
+ for (const c of TOKEN_CATEGORIES) {
2867
+ const tokens = result.tokens.filter((t) => t.category === c);
2868
+ if (tokens.length === 0) continue;
2869
+ lines.push(`### ${TOKEN_CATEGORY_LABELS[c]}`);
2870
+ lines.push("| \uACBD\uB85C | \uAC12 | \uB2E4\uD06C | CSS | \uC124\uBA85 |");
2871
+ lines.push("|------|-----|------|-----|------|");
2856
2872
  for (const t of tokens) {
2857
- const desc = t.description || "-";
2858
- lines.push(`| \`${t.path}\` | \`${t.value}\` | ${desc} |`);
2873
+ const dark = t.dark === void 0 ? "-" : t.dark === t.value ? "(\uAC19\uC74C)" : `\`${cell(t.dark)}\``;
2874
+ lines.push(
2875
+ `| \`${t.path}\` | \`${cell(t.value)}\` | ${dark} | \`${cssUsage(t)}\` | ${t.description ? cell(t.description) : "-"} |`
2876
+ );
2859
2877
  }
2860
2878
  lines.push("");
2861
2879
  }
@@ -5233,7 +5251,7 @@ var server = new McpServer({
5233
5251
  version: SERVER_VERSION
5234
5252
  });
5235
5253
  var ComponentCategorySchema = z.enum(["core", "agentic", "form", "layout", "ui"]).optional().describe("\uCEF4\uD3EC\uB10C\uD2B8 \uCE74\uD14C\uACE0\uB9AC \uD544\uD130 (ui = Core UI \uC804\uCCB4, agentic = Agentic UI)");
5236
- var TokenCategorySchema = z.enum(["color", "typography", "spacing", "radius", "shadow", "animation"]).optional().describe("\uD1A0\uD070 \uCE74\uD14C\uACE0\uB9AC \uD544\uD130");
5254
+ var TokenCategorySchema = z.enum(TOKEN_CATEGORIES).optional().describe("\uD1A0\uD070 \uCE74\uD14C\uACE0\uB9AC \uD544\uD130 (density = \uCEE8\uD2B8\uB864 \uB192\uC774 \xB7 \uAE00\uC790 \uD06C\uAE30 \uB4F1 \uBC00\uB3C4 \uD1A0\uD070, component = \uCEF4\uD3EC\uB10C\uD2B8 \uD1A0\uD070)");
5237
5255
  server.tool(
5238
5256
  "axis.search_components",
5239
5257
  "AXIS \uB514\uC790\uC778 \uC2DC\uC2A4\uD15C\uC5D0\uC11C \uCEF4\uD3EC\uB10C\uD2B8\uB97C \uAC80\uC0C9\uD569\uB2C8\uB2E4.",
@@ -5338,7 +5356,7 @@ server.tool(
5338
5356
  );
5339
5357
  server.tool(
5340
5358
  "axis.list_tokens",
5341
- "AXIS \uB514\uC790\uC778 \uC2DC\uC2A4\uD15C\uC758 \uB514\uC790\uC778 \uD1A0\uD070 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4.",
5359
+ "AXIS \uB514\uC790\uC778 \uC2DC\uC2A4\uD15C\uC758 \uB514\uC790\uC778 \uD1A0\uD070 \uBAA9\uB85D(\uB77C\uC774\uD2B8 \xB7 \uB2E4\uD06C \uAC12, CSS \uBCC0\uC218)\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. \uAC12\uC740 @axis-ds/tokens \uBE4C\uB4DC \uC0B0\uCD9C\uBB3C\uC5D0\uC11C \uC0DD\uC131\uD569\uB2C8\uB2E4.",
5342
5360
  {
5343
5361
  category: TokenCategorySchema
5344
5362
  },
@@ -5372,21 +5390,23 @@ server.tool(
5372
5390
  );
5373
5391
  server.tool(
5374
5392
  "axis.get_token",
5375
- "AXIS \uB514\uC790\uC778 \uC2DC\uC2A4\uD15C \uD1A0\uD070\uC758 \uC0C1\uC138 \uC815\uBCF4\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.",
5393
+ "AXIS \uB514\uC790\uC778 \uC2DC\uC2A4\uD15C \uD1A0\uD070\uC758 \uC0C1\uC138 \uC815\uBCF4(\uB77C\uC774\uD2B8 \xB7 \uB2E4\uD06C \uAC12, CSS \uBCC0\uC218)\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. \uC5C6\uB294 \uACBD\uB85C\uB294 isError \uB85C \uB2F5\uD569\uB2C8\uB2E4.",
5376
5394
  {
5377
- path: z.string().describe("\uD1A0\uD070 \uACBD\uB85C (\uC608: colors.primary, spacing.4)")
5395
+ path: z.string().describe("\uD1A0\uD070 \uACBD\uB85C (\uC608: colors.primary, spacing.4, control.h.md) \uB610\uB294 CSS \uBCC0\uC218 \uC774\uB984 (\uC608: --axis-control-h-md)")
5378
5396
  },
5379
5397
  async ({ path: path11 }) => {
5380
5398
  try {
5381
5399
  const token = handleGetToken({ path: path11 });
5382
- const formatted = formatTokenDetail(token);
5400
+ const formatted = formatTokenDetail(token, path11);
5383
5401
  return {
5384
5402
  content: [
5385
5403
  {
5386
5404
  type: "text",
5387
5405
  text: formatted
5388
5406
  }
5389
- ]
5407
+ ],
5408
+ // WI-0076: 없는 토큰은 일반 텍스트가 아니라 오류로 답합니다(응답 계약 변경)
5409
+ ...token ? {} : { isError: true }
5390
5410
  };
5391
5411
  } catch (error) {
5392
5412
  const message = error instanceof Error ? error.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958";
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/agent-avatar.tsx",
15
15
  "type": "registry:ui",
16
- "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { AGENT_STATUS_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport interface AgentAvatarProps {\n /** 에이전트 이름 */\n name: string;\n /** 이미지 URL */\n src?: string;\n /** 크기 */\n size?: \"sm\" | \"md\" | \"lg\" | \"xl\";\n /** 상태 표시 */\n status?: \"online\" | \"busy\" | \"offline\";\n /** 에이전트 타입 */\n type?: \"assistant\" | \"tool\" | \"system\";\n /** 추가 클래스 */\n className?: string;\n}\n\nconst sizeStyles = {\n sm: \"w-6 h-6 text-xs\",\n md: \"w-8 h-8 text-sm\",\n lg: \"w-10 h-10 text-base\",\n xl: \"w-12 h-12 text-lg\",\n};\n\nconst statusStyles = {\n online: \"bg-[var(--axis-status-success)]\",\n busy: \"bg-[var(--axis-status-warning)]\",\n offline: \"bg-[var(--axis-status-neutral)]\",\n};\n\nconst typeColors = {\n assistant: \"bg-[var(--axis-agent-avatar-assistant)]\",\n tool: \"bg-[var(--axis-agent-avatar-tool)]\",\n system: \"bg-[var(--axis-agent-avatar-system)]\",\n};\n\n/**\n * 에이전트 아바타 컴포넌트\n */\nexport function AgentAvatar({\n name,\n src,\n size = \"md\",\n status,\n type = \"assistant\",\n className,\n}: AgentAvatarProps) {\n const initials = name\n .split(\" \")\n .map((n) => n[0])\n .join(\"\")\n .slice(0, 2)\n .toUpperCase();\n\n const statusLabel = status ? AGENT_STATUS_LABELS[status] : \"\";\n const ariaLabel = `${name}${statusLabel ? `, ${statusLabel}` : \"\"}`;\n\n return (\n <div\n role=\"img\"\n aria-label={ariaLabel}\n className={cn(\"relative inline-flex\", className)}\n >\n {src ? (\n <img\n src={src}\n alt=\"\"\n aria-hidden=\"true\"\n className={cn(\n \"rounded-full object-cover\",\n sizeStyles[size]\n )}\n />\n ) : (\n <div\n aria-hidden=\"true\"\n className={cn(\n \"rounded-full flex items-center justify-center font-medium text-[var(--axis-agent-avatar-text)]\",\n sizeStyles[size],\n typeColors[type]\n )}\n >\n {initials}\n </div>\n )}\n {status && (\n <span\n aria-hidden=\"true\"\n className={cn(\n \"absolute bottom-0 right-0 rounded-full border-2 border-[var(--axis-surface-default)]\",\n size === \"sm\" ? \"w-2 h-2\" : \"w-3 h-3\",\n statusStyles[status]\n )}\n />\n )}\n </div>\n );\n}\n"
16
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { AGENT_STATUS_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport interface AgentAvatarProps {\n /** 에이전트 이름 */\n name: string;\n /** 이미지 URL */\n src?: string;\n /** 크기 */\n size?: \"sm\" | \"md\" | \"lg\" | \"xl\";\n /** 상태 표시 */\n status?: \"online\" | \"busy\" | \"offline\";\n /** 에이전트 타입 */\n type?: \"assistant\" | \"tool\" | \"system\";\n /** 추가 클래스 */\n className?: string;\n}\n\nconst sizeStyles = {\n sm: \"w-6 h-6 text-xs\",\n md: \"w-8 h-8 text-sm\",\n lg: \"w-10 h-10 text-base\",\n xl: \"w-12 h-12 text-lg\",\n};\n\nconst statusStyles = {\n online: \"bg-[var(--axis-text-success)]\",\n busy: \"bg-[var(--axis-text-warning)]\",\n offline: \"bg-[var(--axis-text-tertiary)]\",\n};\n\nconst typeColors = {\n assistant: \"bg-[var(--axis-agent-avatar-assistant)]\",\n tool: \"bg-[var(--axis-agent-avatar-tool)]\",\n system: \"bg-[var(--axis-agent-avatar-system)]\",\n};\n\n/**\n * 에이전트 아바타 컴포넌트\n */\nexport function AgentAvatar({\n name,\n src,\n size = \"md\",\n status,\n type = \"assistant\",\n className,\n}: AgentAvatarProps) {\n const initials = name\n .split(\" \")\n .map((n) => n[0])\n .join(\"\")\n .slice(0, 2)\n .toUpperCase();\n\n const statusLabel = status ? AGENT_STATUS_LABELS[status] : \"\";\n const ariaLabel = `${name}${statusLabel ? `, ${statusLabel}` : \"\"}`;\n\n return (\n <div\n role=\"img\"\n aria-label={ariaLabel}\n className={cn(\"relative inline-flex\", className)}\n >\n {src ? (\n <img\n src={src}\n alt=\"\"\n aria-hidden=\"true\"\n className={cn(\n \"rounded-full object-cover\",\n sizeStyles[size]\n )}\n />\n ) : (\n <div\n aria-hidden=\"true\"\n className={cn(\n \"rounded-full flex items-center justify-center font-medium text-[var(--axis-agent-avatar-text)]\",\n sizeStyles[size],\n typeColors[type]\n )}\n >\n {initials}\n </div>\n )}\n {status && (\n <span\n aria-hidden=\"true\"\n className={cn(\n \"absolute bottom-0 right-0 rounded-full border-2 border-[var(--axis-surface-default)]\",\n size === \"sm\" ? \"w-2 h-2\" : \"w-3 h-3\",\n statusStyles[status]\n )}\n />\n )}\n </div>\n );\n}\n"
17
17
  }
18
18
  ]
19
19
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/approval-card.tsx",
15
15
  "type": "registry:ui",
16
- "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { SEVERITY_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type Severity = \"info\" | \"warning\" | \"error\";\n\nexport interface ApprovalAction {\n label: string;\n variant?: \"default\" | \"secondary\" | \"ghost\" | \"destructive\";\n onClick: () => void;\n}\n\nexport interface ApprovalCardProps {\n /** 고유 ID (접근성용) */\n id?: string;\n /** 제목 */\n title: string;\n /** 설명 */\n description?: string;\n /** 심각도 (색상 결정) */\n severity?: Severity;\n /** 액션 버튼 목록 */\n actions: ApprovalAction[];\n /** 추가 내용 */\n children?: React.ReactNode;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst severityStyles: Record<Severity, { border: string; icon: string; bg: string }> = {\n info: {\n border: \"border-l-[var(--axis-status-info)]\",\n icon: \"text-[var(--axis-status-info)]\",\n bg: \"bg-[var(--axis-surface-info)]\",\n },\n warning: {\n border: \"border-l-[var(--axis-status-warning)]\",\n icon: \"text-[var(--axis-status-warning)]\",\n bg: \"bg-[var(--axis-surface-warning)]\",\n },\n error: {\n border: \"border-l-[var(--axis-status-error)]\",\n icon: \"text-[var(--axis-status-error)]\",\n bg: \"bg-[var(--axis-surface-error)]\",\n },\n};\n\nconst severityIcons: Record<Severity, React.ReactNode> = {\n info: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n ),\n warning: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\" />\n </svg>\n ),\n error: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n ),\n};\n\nconst actionVariantStyles: Record<string, string> = {\n default: \"bg-[var(--axis-button-bg-default)] text-[var(--axis-button-text-default)] hover:bg-[var(--axis-button-bg-hover)]\",\n secondary: \"bg-[var(--axis-button-secondary-bg-default)] text-[var(--axis-text-primary)] hover:bg-[var(--axis-button-secondary-bg-hover)]\",\n ghost: \"bg-transparent text-[var(--axis-text-primary)] hover:bg-[var(--axis-surface-secondary)]\",\n destructive: \"bg-[var(--axis-button-destructive-bg-default)] text-[var(--axis-button-destructive-text-default)] hover:bg-[var(--axis-button-destructive-bg-hover)]\",\n};\n\n/**\n * 사용자 승인이 필요한 작업을 표시하는 카드 컴포넌트\n */\nexport function ApprovalCard({\n id,\n title,\n description,\n severity = \"info\",\n actions,\n children,\n className,\n}: ApprovalCardProps) {\n const styles = severityStyles[severity];\n const generatedId = React.useId();\n const baseId = id || generatedId;\n const titleId = `${baseId}-title`;\n const descId = `${baseId}-desc`;\n const severityLabel = SEVERITY_LABELS[severity];\n\n return (\n <div\n role=\"region\"\n aria-labelledby={titleId}\n aria-describedby={description ? descId : undefined}\n className={cn(\n \"rounded-lg border border-l-4 p-4\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n styles.border,\n className\n )}\n >\n <div className=\"flex items-start gap-3\">\n <div className={cn(\"flex-shrink-0 mt-0.5\", styles.icon)} aria-hidden=\"true\">\n {severityIcons[severity]}\n </div>\n <div className=\"flex-1 min-w-0\">\n <span className=\"sr-only\">{severityLabel} 알림:</span>\n <h4 id={titleId} className=\"text-sm font-semibold text-[var(--axis-text-primary)]\">\n {title}\n </h4>\n {description && (\n <p id={descId} className=\"mt-1 text-sm text-[var(--axis-text-secondary)]\">\n {description}\n </p>\n )}\n {children && <div className=\"mt-3\">{children}</div>}\n <div className=\"flex flex-wrap gap-2 mt-4\">\n {actions.map((action, index) => (\n <button\n key={index}\n onClick={action.onClick}\n className={cn(\n \"px-3 py-1.5 text-sm font-medium rounded-md transition-colors\",\n actionVariantStyles[action.variant || \"default\"]\n )}\n >\n {action.label}\n </button>\n ))}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"
16
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { SEVERITY_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type Severity = \"info\" | \"warning\" | \"error\";\n\nexport interface ApprovalAction {\n label: string;\n variant?: \"default\" | \"secondary\" | \"ghost\" | \"destructive\";\n onClick: () => void;\n}\n\nexport interface ApprovalCardProps {\n /** 고유 ID (접근성용) */\n id?: string;\n /** 제목 */\n title: string;\n /** 설명 */\n description?: string;\n /** 심각도 (색상 결정) */\n severity?: Severity;\n /** 액션 버튼 목록 */\n actions: ApprovalAction[];\n /** 추가 내용 */\n children?: React.ReactNode;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst severityStyles: Record<Severity, { border: string; icon: string; bg: string }> = {\n info: {\n border: \"border-l-[var(--axis-status-info)]\",\n icon: \"text-[var(--axis-status-info)]\",\n bg: \"bg-[var(--axis-surface-info)]\",\n },\n warning: {\n border: \"border-l-[var(--axis-status-warning)]\",\n icon: \"text-[var(--axis-text-warning)]\",\n bg: \"bg-[var(--axis-surface-warning)]\",\n },\n error: {\n border: \"border-l-[var(--axis-status-error)]\",\n icon: \"text-[var(--axis-text-error)]\",\n bg: \"bg-[var(--axis-surface-error)]\",\n },\n};\n\nconst severityIcons: Record<Severity, React.ReactNode> = {\n info: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n ),\n warning: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\" />\n </svg>\n ),\n error: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n ),\n};\n\nconst actionVariantStyles: Record<string, string> = {\n default: \"bg-[var(--axis-button-bg-default)] text-[var(--axis-button-text-default)] hover:bg-[var(--axis-button-bg-hover)]\",\n secondary: \"bg-[var(--axis-button-secondary-bg-default)] text-[var(--axis-text-primary)] hover:bg-[var(--axis-button-secondary-bg-hover)]\",\n ghost: \"bg-transparent text-[var(--axis-text-primary)] hover:bg-[var(--axis-surface-secondary)]\",\n destructive: \"bg-[var(--axis-button-destructive-bg-default)] text-[var(--axis-button-destructive-text-default)] hover:bg-[var(--axis-button-destructive-bg-hover)]\",\n};\n\n/**\n * 사용자 승인이 필요한 작업을 표시하는 카드 컴포넌트\n */\nexport function ApprovalCard({\n id,\n title,\n description,\n severity = \"info\",\n actions,\n children,\n className,\n}: ApprovalCardProps) {\n const styles = severityStyles[severity];\n const generatedId = React.useId();\n const baseId = id || generatedId;\n const titleId = `${baseId}-title`;\n const descId = `${baseId}-desc`;\n const severityLabel = SEVERITY_LABELS[severity];\n\n return (\n <div\n role=\"region\"\n aria-labelledby={titleId}\n aria-describedby={description ? descId : undefined}\n className={cn(\n \"rounded-lg border border-l-4 p-4\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n styles.border,\n className\n )}\n >\n <div className=\"flex items-start gap-3\">\n <div className={cn(\"flex-shrink-0 mt-0.5\", styles.icon)} aria-hidden=\"true\">\n {severityIcons[severity]}\n </div>\n <div className=\"flex-1 min-w-0\">\n <span className=\"sr-only\">{severityLabel} 알림:</span>\n <h4 id={titleId} className=\"text-sm font-semibold text-[var(--axis-text-primary)]\">\n {title}\n </h4>\n {description && (\n <p id={descId} className=\"mt-1 text-sm text-[var(--axis-text-secondary)]\">\n {description}\n </p>\n )}\n {children && <div className=\"mt-3\">{children}</div>}\n <div className=\"flex flex-wrap gap-2 mt-4\">\n {actions.map((action, index) => (\n <button\n key={index}\n onClick={action.onClick}\n className={cn(\n \"px-3 py-1.5 text-sm font-medium rounded-md transition-colors\",\n actionVariantStyles[action.variant || \"default\"]\n )}\n >\n {action.label}\n </button>\n ))}\n </div>\n </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/attachment-card.tsx",
15
15
  "type": "registry:ui",
16
- "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { ATTACHMENT_TYPE_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type AttachmentType = \"image\" | \"document\" | \"code\" | \"archive\" | \"other\";\nexport type AttachmentStatus = \"uploading\" | \"complete\" | \"error\";\n\nexport interface AttachmentCardProps {\n /** 파일 타입 */\n type: AttachmentType;\n /** 파일명 */\n name: string;\n /** 파일 크기 (bytes) */\n size?: number;\n /** 파일 URL */\n url?: string;\n /** 썸네일 URL (이미지인 경우) */\n thumbnail?: string;\n /** 업로드 상태 */\n status?: AttachmentStatus;\n /** 업로드 진행률 (0-100) */\n progress?: number;\n /** 삭제 콜백 */\n onRemove?: () => void;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst typeIcons: Record<AttachmentType, React.ReactNode> = {\n image: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z\" />\n </svg>\n ),\n document: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\" />\n </svg>\n ),\n code: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4\" />\n </svg>\n ),\n archive: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4\" />\n </svg>\n ),\n other: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z\" />\n </svg>\n ),\n};\n\nconst formatFileSize = (bytes: number): string => {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n};\n\n/**\n * 파일/이미지 첨부를 표시하는 카드 컴포넌트\n */\nexport function AttachmentCard({\n type,\n name,\n size,\n url,\n thumbnail,\n status = \"complete\",\n progress,\n onRemove,\n className,\n}: AttachmentCardProps) {\n const typeLabel = ATTACHMENT_TYPE_LABELS[type];\n\n return (\n <div\n role=\"article\"\n aria-label={`${typeLabel}: ${name}`}\n className={cn(\n \"flex items-center gap-3 rounded-lg border p-3\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n status === \"error\" && \"border-[var(--axis-status-error-border)]\",\n className\n )}\n >\n {/* 썸네일 또는 아이콘 */}\n {thumbnail ? (\n <img\n src={thumbnail}\n alt=\"\"\n aria-hidden=\"true\"\n className=\"w-10 h-10 rounded object-cover flex-shrink-0\"\n />\n ) : (\n <div\n className=\"w-10 h-10 rounded flex items-center justify-center flex-shrink-0 bg-[var(--axis-surface-secondary)] text-[var(--axis-icon-secondary)]\"\n aria-hidden=\"true\"\n >\n {typeIcons[type]}\n </div>\n )}\n\n {/* 정보 */}\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium text-[var(--axis-text-primary)] truncate\">\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:underline\"\n aria-label={`${name} (새 창에서 열림)`}\n >\n {name}\n </a>\n ) : (\n name\n )}\n </p>\n <div className=\"flex items-center gap-2 text-xs text-[var(--axis-text-tertiary)]\">\n <span>{typeLabel}</span>\n {size !== undefined && (\n <>\n <span aria-hidden=\"true\">·</span>\n <span>{formatFileSize(size)}</span>\n </>\n )}\n {status === \"error\" && (\n <span className=\"text-[var(--axis-text-error)]\">업로드 실패</span>\n )}\n </div>\n\n {/* 업로드 진행률 */}\n {status === \"uploading\" && progress !== undefined && (\n <div className=\"mt-1.5 h-1 rounded-full bg-[var(--axis-surface-secondary)] overflow-hidden\">\n <div\n className=\"h-full rounded-full bg-[var(--axis-status-running)] transition-all\"\n style={{ width: `${progress}%` }}\n role=\"progressbar\"\n aria-valuenow={progress}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label=\"업로드 진행률\"\n />\n </div>\n )}\n </div>\n\n {/* 삭제 버튼 */}\n {onRemove && (\n <button\n onClick={onRemove}\n aria-label={`${name} 삭제`}\n className=\"flex-shrink-0 p-1 rounded-md text-[var(--axis-text-tertiary)] hover:text-[var(--axis-text-secondary)] hover:bg-[var(--axis-surface-secondary)] transition-colors\"\n >\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 </button>\n )}\n </div>\n );\n}\n"
16
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { ATTACHMENT_TYPE_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type AttachmentType = \"image\" | \"document\" | \"code\" | \"archive\" | \"other\";\nexport type AttachmentStatus = \"uploading\" | \"complete\" | \"error\";\n\nexport interface AttachmentCardProps {\n /** 파일 타입 */\n type: AttachmentType;\n /** 파일명 */\n name: string;\n /** 파일 크기 (bytes) */\n size?: number;\n /** 파일 URL */\n url?: string;\n /** 썸네일 URL (이미지인 경우) */\n thumbnail?: string;\n /** 업로드 상태 */\n status?: AttachmentStatus;\n /** 업로드 진행률 (0-100) */\n progress?: number;\n /** 삭제 콜백 */\n onRemove?: () => void;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst typeIcons: Record<AttachmentType, React.ReactNode> = {\n image: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z\" />\n </svg>\n ),\n document: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\" />\n </svg>\n ),\n code: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4\" />\n </svg>\n ),\n archive: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4\" />\n </svg>\n ),\n other: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z\" />\n </svg>\n ),\n};\n\nconst formatFileSize = (bytes: number): string => {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n};\n\n/**\n * 파일/이미지 첨부를 표시하는 카드 컴포넌트\n */\nexport function AttachmentCard({\n type,\n name,\n size,\n url,\n thumbnail,\n status = \"complete\",\n progress,\n onRemove,\n className,\n}: AttachmentCardProps) {\n const typeLabel = ATTACHMENT_TYPE_LABELS[type];\n\n return (\n <div\n role=\"article\"\n aria-label={`${typeLabel}: ${name}`}\n className={cn(\n \"flex items-center gap-3 rounded-lg border p-3\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n status === \"error\" && \"border-[var(--axis-border-error)]\",\n className\n )}\n >\n {/* 썸네일 또는 아이콘 */}\n {thumbnail ? (\n <img\n src={thumbnail}\n alt=\"\"\n aria-hidden=\"true\"\n className=\"w-10 h-10 rounded object-cover flex-shrink-0\"\n />\n ) : (\n <div\n className=\"w-10 h-10 rounded flex items-center justify-center flex-shrink-0 bg-[var(--axis-surface-secondary)] text-[var(--axis-icon-secondary)]\"\n aria-hidden=\"true\"\n >\n {typeIcons[type]}\n </div>\n )}\n\n {/* 정보 */}\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium text-[var(--axis-text-primary)] truncate\">\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:underline\"\n aria-label={`${name} (새 창에서 열림)`}\n >\n {name}\n </a>\n ) : (\n name\n )}\n </p>\n <div className=\"flex items-center gap-2 text-xs text-[var(--axis-text-tertiary)]\">\n <span>{typeLabel}</span>\n {size !== undefined && (\n <>\n <span aria-hidden=\"true\">·</span>\n <span>{formatFileSize(size)}</span>\n </>\n )}\n {status === \"error\" && (\n <span className=\"text-[var(--axis-text-error)]\">업로드 실패</span>\n )}\n </div>\n\n {/* 업로드 진행률 */}\n {status === \"uploading\" && progress !== undefined && (\n <div className=\"mt-1.5 h-1 rounded-full bg-[var(--axis-surface-secondary)] overflow-hidden\">\n <div\n className=\"h-full rounded-full bg-[var(--axis-status-running)] transition-all\"\n style={{ width: `${progress}%` }}\n role=\"progressbar\"\n aria-valuenow={progress}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label=\"업로드 진행률\"\n />\n </div>\n )}\n </div>\n\n {/* 삭제 버튼 */}\n {onRemove && (\n <button\n onClick={onRemove}\n aria-label={`${name} 삭제`}\n className=\"flex-shrink-0 p-1 rounded-md text-[var(--axis-text-tertiary)] hover:text-[var(--axis-text-secondary)] hover:bg-[var(--axis-surface-secondary)] transition-colors\"\n >\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 </button>\n )}\n </div>\n );\n}\n"
17
17
  }
18
18
  ]
19
19
  }
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "components/agentic/code-block.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 CodeBlockProps {\n /** 코드 문자열 */\n code: string;\n /** 언어 (구문 강조용 힌트) */\n language?: string;\n /** 파일명 */\n filename?: string;\n /** 줄 번호 표시 */\n showLineNumbers?: boolean;\n /** 복사 완료 콜백 */\n onCopy?: (code: string) => void;\n /** 최대 높이 (스크롤) */\n maxHeight?: string;\n /** 추가 클래스 */\n className?: string;\n}\n\n/**\n * 코드 표시 + 복사 버튼 컴포넌트\n */\nexport function CodeBlock({\n code,\n language,\n filename,\n showLineNumbers = false,\n onCopy,\n maxHeight,\n className,\n}: CodeBlockProps) {\n const [copied, setCopied] = React.useState(false);\n\n const handleCopy = async () => {\n try {\n await navigator.clipboard.writeText(code);\n setCopied(true);\n onCopy?.(code);\n setTimeout(() => setCopied(false), 2000);\n } catch {\n // clipboard API 미지원 시 fallback 없음\n }\n };\n\n const lines = code.split(\"\\n\");\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 <div className=\"flex items-center justify-between px-3 py-2 border-b border-[var(--axis-border-default)] bg-[var(--axis-surface-secondary)]\">\n <div className=\"flex items-center gap-2 min-w-0\">\n {filename && (\n <code className=\"text-xs font-mono text-[var(--axis-text-primary)] truncate\">\n {filename}\n </code>\n )}\n {language && (\n <span className=\"text-xs px-1.5 py-0.5 rounded bg-[var(--axis-surface-tertiary)] text-[var(--axis-text-tertiary)]\">\n {language}\n </span>\n )}\n </div>\n <button\n onClick={handleCopy}\n aria-label={copied ? \"복사됨\" : \"코드 복사\"}\n className=\"flex items-center gap-1 px-2 py-1 text-xs rounded-md text-[var(--axis-text-secondary)] hover:bg-[var(--axis-surface-tertiary)] transition-colors\"\n >\n {copied ? (\n <>\n <svg className=\"w-3.5 h-3.5 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 <span>복사됨</span>\n </>\n ) : (\n <>\n <svg className=\"w-3.5 h-3.5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\" />\n </svg>\n <span>복사</span>\n </>\n )}\n </button>\n </div>\n\n {/* 코드 본문 */}\n <div\n className=\"overflow-auto\"\n style={maxHeight ? { maxHeight } : undefined}\n >\n <pre className=\"p-3 text-sm leading-relaxed\">\n <code className=\"text-[var(--axis-text-primary)] font-mono\">\n {showLineNumbers ? (\n <table className=\"border-collapse w-full\" role=\"presentation\">\n <tbody>\n {lines.map((line, i) => (\n <tr key={i}>\n <td\n className=\"select-none text-right pr-4 text-[var(--axis-text-tertiary)] w-8\"\n aria-hidden=\"true\"\n >\n {i + 1}\n </td>\n <td className=\"whitespace-pre\">{line}</td>\n </tr>\n ))}\n </tbody>\n </table>\n ) : (\n code\n )}\n </code>\n </pre>\n </div>\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 CodeBlockProps {\n /** 코드 문자열 */\n code: string;\n /** 언어 (구문 강조용 힌트) */\n language?: string;\n /** 파일명 */\n filename?: string;\n /** 줄 번호 표시 */\n showLineNumbers?: boolean;\n /** 복사 완료 콜백 */\n onCopy?: (code: string) => void;\n /** 최대 높이 (스크롤) */\n maxHeight?: string;\n /** 추가 클래스 */\n className?: string;\n}\n\n/**\n * 코드 표시 + 복사 버튼 컴포넌트\n */\nexport function CodeBlock({\n code,\n language,\n filename,\n showLineNumbers = false,\n onCopy,\n maxHeight,\n className,\n}: CodeBlockProps) {\n const [copied, setCopied] = React.useState(false);\n\n const handleCopy = async () => {\n try {\n await navigator.clipboard.writeText(code);\n setCopied(true);\n onCopy?.(code);\n setTimeout(() => setCopied(false), 2000);\n } catch {\n // clipboard API 미지원 시 fallback 없음\n }\n };\n\n const lines = code.split(\"\\n\");\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 <div className=\"flex items-center justify-between px-3 py-2 border-b border-[var(--axis-border-default)] bg-[var(--axis-surface-secondary)]\">\n <div className=\"flex items-center gap-2 min-w-0\">\n {filename && (\n <code className=\"text-xs font-mono text-[var(--axis-text-primary)] truncate\">\n {filename}\n </code>\n )}\n {language && (\n <span className=\"text-xs px-1.5 py-0.5 rounded bg-[var(--axis-surface-tertiary)] text-[var(--axis-text-secondary)]\">\n {language}\n </span>\n )}\n </div>\n <button\n onClick={handleCopy}\n aria-label={copied ? \"복사됨\" : \"코드 복사\"}\n className=\"flex items-center gap-1 px-2 py-1 text-xs rounded-md text-[var(--axis-text-secondary)] hover:bg-[var(--axis-surface-tertiary)] transition-colors\"\n >\n {copied ? (\n <>\n <svg className=\"w-3.5 h-3.5 text-[var(--axis-text-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 <span>복사됨</span>\n </>\n ) : (\n <>\n <svg className=\"w-3.5 h-3.5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\" />\n </svg>\n <span>복사</span>\n </>\n )}\n </button>\n </div>\n\n {/* 코드 본문: 스크롤 영역이라 키보드로 포커스해 스크롤할 수 있게 한다(WCAG 2.1.1, axe scrollable-region-focusable, WI-0069) */}\n <div\n role=\"region\"\n tabIndex={0}\n aria-label={filename ? `${filename} 코드` : language ? `${language} 코드` : \"코드\"}\n className=\"overflow-auto rounded-b-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--axis-border-focus)]\"\n style={maxHeight ? { maxHeight } : undefined}\n >\n <pre className=\"p-3 text-sm leading-relaxed\">\n <code className=\"text-[var(--axis-text-primary)] font-mono\">\n {showLineNumbers ? (\n <table className=\"border-collapse w-full\" role=\"presentation\">\n <tbody>\n {lines.map((line, i) => (\n <tr key={i}>\n <td\n className=\"select-none text-right pr-4 text-[var(--axis-text-tertiary)] w-8\"\n aria-hidden=\"true\"\n >\n {i + 1}\n </td>\n <td className=\"whitespace-pre\">{line}</td>\n </tr>\n ))}\n </tbody>\n </table>\n ) : (\n code\n )}\n </code>\n </pre>\n </div>\n </div>\n );\n}\n"
16
16
  }
17
17
  ]
18
18
  }
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "components/agentic/context-panel.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 ModelInfo {\n /** 모델 이름 */\n name: string;\n /** 모델 제공자 */\n provider?: string;\n /** 모델 버전 */\n version?: string;\n}\n\nexport interface ContextPanelProps {\n /** 모델 정보 */\n modelInfo?: ModelInfo;\n /** 시스템 프롬프트 */\n systemPrompt?: string;\n /** 첨부 파일 목록 */\n attachedFiles?: string[];\n /** 추가 설정 키-값 */\n settings?: Record<string, string>;\n /** 접힘 가능 여부 */\n collapsible?: boolean;\n /** 기본 펼침 상태 */\n defaultExpanded?: boolean;\n /** 추가 클래스 */\n className?: string;\n}\n\n/**\n * 모델/설정 메타 정보를 표시하는 패널 컴포넌트\n */\nexport function ContextPanel({\n modelInfo,\n systemPrompt,\n attachedFiles,\n settings,\n collapsible = true,\n defaultExpanded = false,\n className,\n}: ContextPanelProps) {\n const [isExpanded, setIsExpanded] = React.useState(defaultExpanded);\n const contentId = React.useId();\n\n const hasContent = modelInfo || systemPrompt || (attachedFiles && attachedFiles.length > 0) || (settings && Object.keys(settings).length > 0);\n if (!hasContent) return null;\n\n const header = (\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=\"M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z\" />\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M15 12a3 3 0 11-6 0 3 3 0 016 0z\" />\n </svg>\n <span className=\"text-xs font-medium text-[var(--axis-text-secondary)]\">컨텍스트</span>\n </div>\n );\n\n const content = (\n <div id={contentId} className=\"space-y-3 mt-2\">\n {modelInfo && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">모델</h5>\n <div className=\"flex items-center gap-2\">\n <code className=\"text-sm font-mono text-[var(--axis-text-primary)]\">{modelInfo.name}</code>\n {modelInfo.provider && (\n <span className=\"text-xs text-[var(--axis-text-tertiary)]\">{modelInfo.provider}</span>\n )}\n {modelInfo.version && (\n <span className=\"text-xs px-1.5 py-0.5 rounded bg-[var(--axis-surface-tertiary)] text-[var(--axis-text-tertiary)]\">\n {modelInfo.version}\n </span>\n )}\n </div>\n </div>\n )}\n\n {systemPrompt && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">시스템 프롬프트</h5>\n <p className=\"text-xs text-[var(--axis-text-primary)] p-2 rounded bg-[var(--axis-surface-secondary)] line-clamp-3 whitespace-pre-wrap\">\n {systemPrompt}\n </p>\n </div>\n )}\n\n {attachedFiles && attachedFiles.length > 0 && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">\n 첨부 파일 ({attachedFiles.length})\n </h5>\n <div className=\"space-y-1\">\n {attachedFiles.map((file, i) => (\n <div key={i} className=\"flex items-center gap-1.5 text-xs text-[var(--axis-text-primary)]\">\n <svg className=\"w-3 h-3 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=\"M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13\" />\n </svg>\n <span className=\"truncate\">{file}</span>\n </div>\n ))}\n </div>\n </div>\n )}\n\n {settings && Object.keys(settings).length > 0 && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">설정</h5>\n <div className=\"space-y-1\">\n {Object.entries(settings).map(([key, value]) => (\n <div key={key} className=\"flex items-center justify-between text-xs\">\n <span className=\"text-[var(--axis-text-secondary)]\">{key}</span>\n <code className=\"font-mono text-[var(--axis-text-primary)]\">{value}</code>\n </div>\n ))}\n </div>\n </div>\n )}\n </div>\n );\n\n return (\n <div\n className={cn(\n \"rounded-lg border p-3\",\n \"bg-[var(--axis-surface-secondary)] border-[var(--axis-border-default)]\",\n className\n )}\n >\n {collapsible ? (\n <>\n <button\n onClick={() => setIsExpanded(!isExpanded)}\n aria-expanded={isExpanded}\n aria-controls={contentId}\n className=\"w-full flex items-center justify-between hover:opacity-80 transition-opacity\"\n >\n {header}\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 aria-hidden=\"true\"\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M19 9l-7 7-7-7\" />\n </svg>\n </button>\n {isExpanded && content}\n </>\n ) : (\n <>\n {header}\n {content}\n </>\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 ModelInfo {\n /** 모델 이름 */\n name: string;\n /** 모델 제공자 */\n provider?: string;\n /** 모델 버전 */\n version?: string;\n}\n\nexport interface ContextPanelProps {\n /** 모델 정보 */\n modelInfo?: ModelInfo;\n /** 시스템 프롬프트 */\n systemPrompt?: string;\n /** 첨부 파일 목록 */\n attachedFiles?: string[];\n /** 추가 설정 키-값 */\n settings?: Record<string, string>;\n /** 접힘 가능 여부 */\n collapsible?: boolean;\n /** 기본 펼침 상태 */\n defaultExpanded?: boolean;\n /** 추가 클래스 */\n className?: string;\n}\n\n/**\n * 모델/설정 메타 정보를 표시하는 패널 컴포넌트\n */\nexport function ContextPanel({\n modelInfo,\n systemPrompt,\n attachedFiles,\n settings,\n collapsible = true,\n defaultExpanded = false,\n className,\n}: ContextPanelProps) {\n const [isExpanded, setIsExpanded] = React.useState(defaultExpanded);\n const contentId = React.useId();\n\n const hasContent = modelInfo || systemPrompt || (attachedFiles && attachedFiles.length > 0) || (settings && Object.keys(settings).length > 0);\n if (!hasContent) return null;\n\n const header = (\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=\"M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z\" />\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M15 12a3 3 0 11-6 0 3 3 0 016 0z\" />\n </svg>\n <span className=\"text-xs font-medium text-[var(--axis-text-secondary)]\">컨텍스트</span>\n </div>\n );\n\n const content = (\n <div id={contentId} className=\"space-y-3 mt-2\">\n {modelInfo && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">모델</h5>\n <div className=\"flex items-center gap-2\">\n <code className=\"text-sm font-mono text-[var(--axis-text-primary)]\">{modelInfo.name}</code>\n {modelInfo.provider && (\n <span className=\"text-xs text-[var(--axis-text-secondary)]\">{modelInfo.provider}</span>\n )}\n {modelInfo.version && (\n <span className=\"text-xs px-1.5 py-0.5 rounded bg-[var(--axis-surface-tertiary)] text-[var(--axis-text-secondary)]\">\n {modelInfo.version}\n </span>\n )}\n </div>\n </div>\n )}\n\n {systemPrompt && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">시스템 프롬프트</h5>\n <p className=\"text-xs text-[var(--axis-text-primary)] p-2 rounded bg-[var(--axis-surface-secondary)] line-clamp-3 whitespace-pre-wrap\">\n {systemPrompt}\n </p>\n </div>\n )}\n\n {attachedFiles && attachedFiles.length > 0 && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">\n 첨부 파일 ({attachedFiles.length})\n </h5>\n <div className=\"space-y-1\">\n {attachedFiles.map((file, i) => (\n <div key={i} className=\"flex items-center gap-1.5 text-xs text-[var(--axis-text-primary)]\">\n <svg className=\"w-3 h-3 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=\"M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13\" />\n </svg>\n <span className=\"truncate\">{file}</span>\n </div>\n ))}\n </div>\n </div>\n )}\n\n {settings && Object.keys(settings).length > 0 && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">설정</h5>\n <div className=\"space-y-1\">\n {Object.entries(settings).map(([key, value]) => (\n <div key={key} className=\"flex items-center justify-between text-xs\">\n <span className=\"text-[var(--axis-text-secondary)]\">{key}</span>\n <code className=\"font-mono text-[var(--axis-text-primary)]\">{value}</code>\n </div>\n ))}\n </div>\n </div>\n )}\n </div>\n );\n\n return (\n <div\n className={cn(\n \"rounded-lg border p-3\",\n \"bg-[var(--axis-surface-secondary)] border-[var(--axis-border-default)]\",\n className\n )}\n >\n {collapsible ? (\n <>\n <button\n onClick={() => setIsExpanded(!isExpanded)}\n aria-expanded={isExpanded}\n aria-controls={contentId}\n className=\"w-full flex items-center justify-between hover:opacity-80 transition-opacity\"\n >\n {header}\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 aria-hidden=\"true\"\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M19 9l-7 7-7-7\" />\n </svg>\n </button>\n {isExpanded && content}\n </>\n ) : (\n <>\n {header}\n {content}\n </>\n )}\n </div>\n );\n}\n"
16
16
  }
17
17
  ]
18
18
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/diff-viewer.tsx",
15
15
  "type": "registry:ui",
16
- "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { DIFF_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type DiffViewMode = \"unified\" | \"split\";\n\nexport interface DiffViewerProps {\n /** 변경 전 코드 */\n before: string;\n /** 변경 후 코드 */\n after: string;\n /** 파일명 */\n filename?: string;\n /** 표시 모드 */\n viewMode?: DiffViewMode;\n /** 추가 클래스 */\n className?: string;\n}\n\ntype DiffLineType = \"added\" | \"removed\" | \"unchanged\";\n\ninterface DiffLine {\n type: DiffLineType;\n content: string;\n oldLineNumber?: number;\n newLineNumber?: number;\n}\n\n/** LCS 기반 diff 알고리즘 */\nfunction computeDiff(before: string, after: string): DiffLine[] {\n const oldLines = before.split(\"\\n\");\n const newLines = after.split(\"\\n\");\n const m = oldLines.length;\n const n = newLines.length;\n\n // LCS 테이블 구축\n const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n if (oldLines[i - 1] === newLines[j - 1]) {\n dp[i][j] = dp[i - 1][j - 1] + 1;\n } else {\n dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);\n }\n }\n }\n\n // 역추적으로 diff 생성\n const result: DiffLine[] = [];\n let i = m;\n let j = n;\n\n while (i > 0 || j > 0) {\n if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {\n result.unshift({ type: \"unchanged\", content: oldLines[i - 1], oldLineNumber: i, newLineNumber: j });\n i--;\n j--;\n } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {\n result.unshift({ type: \"added\", content: newLines[j - 1], newLineNumber: j });\n j--;\n } else if (i > 0) {\n result.unshift({ type: \"removed\", content: oldLines[i - 1], oldLineNumber: i });\n i--;\n }\n }\n\n return result;\n}\n\nconst lineTypeStyles: Record<DiffLineType, string> = {\n added: \"bg-[color:color-mix(in_srgb,var(--axis-diff-added)_10%,transparent)]\",\n removed: \"bg-[color:color-mix(in_srgb,var(--axis-diff-removed)_10%,transparent)]\",\n unchanged: \"\",\n};\n\nconst lineTypeTextStyles: Record<DiffLineType, string> = {\n added: \"text-[var(--axis-diff-added-text)]\",\n removed: \"text-[var(--axis-diff-removed-text)]\",\n unchanged: \"text-[var(--axis-text-primary)]\",\n};\n\nconst lineTypeSymbols: Record<DiffLineType, string> = {\n added: \"+\",\n removed: \"-\",\n unchanged: \" \",\n};\n\n/**\n * 코드 변경사항 비교 표시 컴포넌트\n */\nexport function DiffViewer({\n before,\n after,\n filename,\n viewMode = \"unified\",\n className,\n}: DiffViewerProps) {\n const [mode, setMode] = React.useState<DiffViewMode>(viewMode);\n const diffLines = React.useMemo(() => computeDiff(before, after), [before, after]);\n\n const addedCount = diffLines.filter((l) => l.type === \"added\").length;\n const removedCount = diffLines.filter((l) => l.type === \"removed\").length;\n\n return (\n <div\n role=\"region\"\n aria-label={filename ? `${filename} 변경사항` : \"코드 변경사항\"}\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 <div className=\"flex items-center justify-between px-3 py-2 border-b border-[var(--axis-border-default)] bg-[var(--axis-surface-secondary)]\">\n <div className=\"flex items-center gap-2 min-w-0\">\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=\"M4 6h16M4 12h16M4 18h7\" />\n </svg>\n {filename && (\n <code className=\"text-xs font-mono text-[var(--axis-text-primary)] truncate\">{filename}</code>\n )}\n <span className=\"text-xs text-[var(--axis-diff-added)]\" aria-label={`${addedCount}줄 ${DIFF_LABELS.added}`}>\n +{addedCount}\n </span>\n <span className=\"text-xs text-[var(--axis-diff-removed)]\" aria-label={`${removedCount}줄 ${DIFF_LABELS.removed}`}>\n -{removedCount}\n </span>\n </div>\n <div className=\"flex items-center gap-1 bg-[var(--axis-surface-tertiary)] rounded-md p-0.5\">\n <button\n onClick={() => setMode(\"unified\")}\n aria-pressed={mode === \"unified\"}\n className={cn(\n \"px-2 py-0.5 text-xs rounded transition-colors\",\n mode === \"unified\"\n ? \"bg-[var(--axis-surface-default)] text-[var(--axis-text-primary)] shadow-sm\"\n : \"text-[var(--axis-text-tertiary)] hover:text-[var(--axis-text-secondary)]\"\n )}\n >\n 통합\n </button>\n <button\n onClick={() => setMode(\"split\")}\n aria-pressed={mode === \"split\"}\n className={cn(\n \"px-2 py-0.5 text-xs rounded transition-colors\",\n mode === \"split\"\n ? \"bg-[var(--axis-surface-default)] text-[var(--axis-text-primary)] shadow-sm\"\n : \"text-[var(--axis-text-tertiary)] hover:text-[var(--axis-text-secondary)]\"\n )}\n >\n 분할\n </button>\n </div>\n </div>\n\n {/* Diff 본문 */}\n <div className=\"overflow-auto\">\n {mode === \"unified\" ? (\n <UnifiedView lines={diffLines} />\n ) : (\n <SplitView lines={diffLines} />\n )}\n </div>\n </div>\n );\n}\n\nfunction UnifiedView({ lines }: { lines: DiffLine[] }) {\n return (\n <table className=\"w-full text-xs font-mono border-collapse\" role=\"presentation\">\n <tbody>\n {lines.map((line, i) => (\n <tr key={i} className={lineTypeStyles[line.type]}>\n <td className=\"select-none text-right px-2 py-0.5 text-[var(--axis-text-tertiary)] w-10 border-r border-[var(--axis-border-default)]\">\n {line.oldLineNumber ?? \"\"}\n </td>\n <td className=\"select-none text-right px-2 py-0.5 text-[var(--axis-text-tertiary)] w-10 border-r border-[var(--axis-border-default)]\">\n {line.newLineNumber ?? \"\"}\n </td>\n <td className={cn(\"select-none w-4 text-center py-0.5\", lineTypeTextStyles[line.type])}>\n {lineTypeSymbols[line.type]}\n </td>\n <td className={cn(\"px-2 py-0.5 whitespace-pre\", lineTypeTextStyles[line.type])}>\n {line.content}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n );\n}\n\nfunction SplitView({ lines }: { lines: DiffLine[] }) {\n // 왼쪽: removed + unchanged, 오른쪽: added + unchanged\n const leftLines: (DiffLine | null)[] = [];\n const rightLines: (DiffLine | null)[] = [];\n\n for (const line of lines) {\n if (line.type === \"unchanged\") {\n leftLines.push(line);\n rightLines.push(line);\n } else if (line.type === \"removed\") {\n leftLines.push(line);\n rightLines.push(null);\n } else {\n leftLines.push(null);\n rightLines.push(line);\n }\n }\n\n return (\n <table className=\"w-full text-xs font-mono border-collapse\" role=\"presentation\">\n <tbody>\n {leftLines.map((leftLine, i) => {\n const rightLine = rightLines[i];\n return (\n <tr key={i}>\n {/* 왼쪽 (before) */}\n <td className={cn(\"select-none text-right px-2 py-0.5 text-[var(--axis-text-tertiary)] w-10 border-r border-[var(--axis-border-default)]\", leftLine ? lineTypeStyles[leftLine.type] : \"\")}>\n {leftLine?.oldLineNumber ?? \"\"}\n </td>\n <td className={cn(\"select-none w-4 text-center py-0.5\", leftLine ? lineTypeTextStyles[leftLine.type] : \"\")}>\n {leftLine ? lineTypeSymbols[leftLine.type] : \"\"}\n </td>\n <td className={cn(\"px-2 py-0.5 whitespace-pre w-1/2 border-r border-[var(--axis-border-default)]\", leftLine ? cn(lineTypeStyles[leftLine.type], lineTypeTextStyles[leftLine.type]) : \"\")}>\n {leftLine?.content ?? \"\"}\n </td>\n {/* 오른쪽 (after) */}\n <td className={cn(\"select-none text-right px-2 py-0.5 text-[var(--axis-text-tertiary)] w-10 border-r border-[var(--axis-border-default)]\", rightLine ? lineTypeStyles[rightLine.type] : \"\")}>\n {rightLine?.newLineNumber ?? \"\"}\n </td>\n <td className={cn(\"select-none w-4 text-center py-0.5\", rightLine ? lineTypeTextStyles[rightLine.type] : \"\")}>\n {rightLine ? lineTypeSymbols[rightLine.type] : \"\"}\n </td>\n <td className={cn(\"px-2 py-0.5 whitespace-pre w-1/2\", rightLine ? cn(lineTypeStyles[rightLine.type], lineTypeTextStyles[rightLine.type]) : \"\")}>\n {rightLine?.content ?? \"\"}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n );\n}\n"
16
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { DIFF_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type DiffViewMode = \"unified\" | \"split\";\n\nexport interface DiffViewerProps {\n /** 변경 전 코드 */\n before: string;\n /** 변경 후 코드 */\n after: string;\n /** 파일명 */\n filename?: string;\n /** 표시 모드 */\n viewMode?: DiffViewMode;\n /** 추가 클래스 */\n className?: string;\n}\n\ntype DiffLineType = \"added\" | \"removed\" | \"unchanged\";\n\ninterface DiffLine {\n type: DiffLineType;\n content: string;\n oldLineNumber?: number;\n newLineNumber?: number;\n}\n\n/** LCS 기반 diff 알고리즘 */\nfunction computeDiff(before: string, after: string): DiffLine[] {\n const oldLines = before.split(\"\\n\");\n const newLines = after.split(\"\\n\");\n const m = oldLines.length;\n const n = newLines.length;\n\n // LCS 테이블 구축\n const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n if (oldLines[i - 1] === newLines[j - 1]) {\n dp[i][j] = dp[i - 1][j - 1] + 1;\n } else {\n dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);\n }\n }\n }\n\n // 역추적으로 diff 생성\n const result: DiffLine[] = [];\n let i = m;\n let j = n;\n\n while (i > 0 || j > 0) {\n if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {\n result.unshift({ type: \"unchanged\", content: oldLines[i - 1], oldLineNumber: i, newLineNumber: j });\n i--;\n j--;\n } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {\n result.unshift({ type: \"added\", content: newLines[j - 1], newLineNumber: j });\n j--;\n } else if (i > 0) {\n result.unshift({ type: \"removed\", content: oldLines[i - 1], oldLineNumber: i });\n i--;\n }\n }\n\n return result;\n}\n\nconst lineTypeStyles: Record<DiffLineType, string> = {\n added: \"bg-[color:color-mix(in_srgb,var(--axis-diff-added)_10%,transparent)]\",\n removed: \"bg-[color:color-mix(in_srgb,var(--axis-diff-removed)_10%,transparent)]\",\n unchanged: \"\",\n};\n\nconst lineTypeTextStyles: Record<DiffLineType, string> = {\n added: \"text-[var(--axis-diff-added-text)]\",\n removed: \"text-[var(--axis-diff-removed-text)]\",\n unchanged: \"text-[var(--axis-text-primary)]\",\n};\n\nconst lineTypeSymbols: Record<DiffLineType, string> = {\n added: \"+\",\n removed: \"-\",\n unchanged: \" \",\n};\n\n/**\n * 코드 변경사항 비교 표시 컴포넌트\n */\nexport function DiffViewer({\n before,\n after,\n filename,\n viewMode = \"unified\",\n className,\n}: DiffViewerProps) {\n const [mode, setMode] = React.useState<DiffViewMode>(viewMode);\n const diffLines = React.useMemo(() => computeDiff(before, after), [before, after]);\n\n const addedCount = diffLines.filter((l) => l.type === \"added\").length;\n const removedCount = diffLines.filter((l) => l.type === \"removed\").length;\n\n return (\n <div\n role=\"region\"\n aria-label={filename ? `${filename} 변경사항` : \"코드 변경사항\"}\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 <div className=\"flex items-center justify-between px-3 py-2 border-b border-[var(--axis-border-default)] bg-[var(--axis-surface-secondary)]\">\n <div className=\"flex items-center gap-2 min-w-0\">\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=\"M4 6h16M4 12h16M4 18h7\" />\n </svg>\n {filename && (\n <code className=\"text-xs font-mono text-[var(--axis-text-primary)] truncate\">{filename}</code>\n )}\n {/* 역할 없는 span 의 aria-label 은 보조기술이 읽지 않는다(axe aria-prohibited-attr, WI-0069).\n 보이는 기호는 숨기고 읽을 문장은 sr-only 로 둔다 */}\n <span className=\"text-xs text-[var(--axis-text-secondary)]\">\n <span aria-hidden=\"true\">+{addedCount}</span>\n <span className=\"sr-only\">{`${addedCount}줄 ${DIFF_LABELS.added}`}</span>\n </span>\n <span className=\"text-xs text-[var(--axis-text-error)]\">\n <span aria-hidden=\"true\">-{removedCount}</span>\n <span className=\"sr-only\">{`${removedCount}줄 ${DIFF_LABELS.removed}`}</span>\n </span>\n </div>\n <div className=\"flex items-center gap-1 bg-[var(--axis-surface-tertiary)] rounded-md p-0.5\">\n <button\n onClick={() => setMode(\"unified\")}\n aria-pressed={mode === \"unified\"}\n className={cn(\n \"px-2 py-0.5 text-xs rounded transition-colors\",\n mode === \"unified\"\n ? \"bg-[var(--axis-surface-default)] text-[var(--axis-text-primary)] shadow-sm\"\n : \"text-[var(--axis-text-secondary)] hover:text-[var(--axis-text-primary)]\"\n )}\n >\n 통합\n </button>\n <button\n onClick={() => setMode(\"split\")}\n aria-pressed={mode === \"split\"}\n className={cn(\n \"px-2 py-0.5 text-xs rounded transition-colors\",\n mode === \"split\"\n ? \"bg-[var(--axis-surface-default)] text-[var(--axis-text-primary)] shadow-sm\"\n : \"text-[var(--axis-text-secondary)] hover:text-[var(--axis-text-primary)]\"\n )}\n >\n 분할\n </button>\n </div>\n </div>\n\n {/* Diff 본문 */}\n <div className=\"overflow-auto\">\n {mode === \"unified\" ? (\n <UnifiedView lines={diffLines} />\n ) : (\n <SplitView lines={diffLines} />\n )}\n </div>\n </div>\n );\n}\n\nfunction UnifiedView({ lines }: { lines: DiffLine[] }) {\n return (\n <table className=\"w-full text-xs font-mono border-collapse\" role=\"presentation\">\n <tbody>\n {lines.map((line, i) => (\n <tr key={i} className={lineTypeStyles[line.type]}>\n <td className=\"select-none text-right px-2 py-0.5 text-[var(--axis-text-secondary)] w-10 border-r border-[var(--axis-border-default)]\">\n {line.oldLineNumber ?? \"\"}\n </td>\n <td className=\"select-none text-right px-2 py-0.5 text-[var(--axis-text-secondary)] w-10 border-r border-[var(--axis-border-default)]\">\n {line.newLineNumber ?? \"\"}\n </td>\n <td className={cn(\"select-none w-4 text-center py-0.5\", lineTypeTextStyles[line.type])}>\n {lineTypeSymbols[line.type]}\n </td>\n <td className={cn(\"px-2 py-0.5 whitespace-pre\", lineTypeTextStyles[line.type])}>\n {line.content}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n );\n}\n\nfunction SplitView({ lines }: { lines: DiffLine[] }) {\n // 왼쪽: removed + unchanged, 오른쪽: added + unchanged\n const leftLines: (DiffLine | null)[] = [];\n const rightLines: (DiffLine | null)[] = [];\n\n for (const line of lines) {\n if (line.type === \"unchanged\") {\n leftLines.push(line);\n rightLines.push(line);\n } else if (line.type === \"removed\") {\n leftLines.push(line);\n rightLines.push(null);\n } else {\n leftLines.push(null);\n rightLines.push(line);\n }\n }\n\n return (\n <table className=\"w-full text-xs font-mono border-collapse\" role=\"presentation\">\n <tbody>\n {leftLines.map((leftLine, i) => {\n const rightLine = rightLines[i];\n return (\n <tr key={i}>\n {/* 왼쪽 (before) */}\n <td className={cn(\"select-none text-right px-2 py-0.5 text-[var(--axis-text-secondary)] w-10 border-r border-[var(--axis-border-default)]\", leftLine ? lineTypeStyles[leftLine.type] : \"\")}>\n {leftLine?.oldLineNumber ?? \"\"}\n </td>\n <td className={cn(\"select-none w-4 text-center py-0.5\", leftLine ? lineTypeTextStyles[leftLine.type] : \"\")}>\n {leftLine ? lineTypeSymbols[leftLine.type] : \"\"}\n </td>\n <td className={cn(\"px-2 py-0.5 whitespace-pre w-1/2 border-r border-[var(--axis-border-default)]\", leftLine ? cn(lineTypeStyles[leftLine.type], lineTypeTextStyles[leftLine.type]) : \"\")}>\n {leftLine?.content ?? \"\"}\n </td>\n {/* 오른쪽 (after) */}\n <td className={cn(\"select-none text-right px-2 py-0.5 text-[var(--axis-text-secondary)] w-10 border-r border-[var(--axis-border-default)]\", rightLine ? lineTypeStyles[rightLine.type] : \"\")}>\n {rightLine?.newLineNumber ?? \"\"}\n </td>\n <td className={cn(\"select-none w-4 text-center py-0.5\", rightLine ? lineTypeTextStyles[rightLine.type] : \"\")}>\n {rightLine ? lineTypeSymbols[rightLine.type] : \"\"}\n </td>\n <td className={cn(\"px-2 py-0.5 whitespace-pre w-1/2\", rightLine ? cn(lineTypeStyles[rightLine.type], lineTypeTextStyles[rightLine.type]) : \"\")}>\n {rightLine?.content ?? \"\"}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n );\n}\n"
17
17
  }
18
18
  ]
19
19
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/feedback-buttons.tsx",
15
15
  "type": "registry:ui",
16
- "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { FEEDBACK_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type FeedbackType = \"like\" | \"dislike\" | \"none\";\n\nexport interface FeedbackButtonsProps {\n /** 메시지 ID */\n messageId: string;\n /** 초기 피드백 상태 */\n initialFeedback?: FeedbackType;\n /** 피드백 변경 콜백 */\n onFeedback?: (messageId: string, feedback: FeedbackType) => void;\n /** 크기 */\n size?: \"sm\" | \"md\";\n /** 추가 클래스 */\n className?: string;\n}\n\nconst sizeStyles = {\n sm: \"p-1\",\n md: \"p-1.5\",\n};\n\nconst iconSizes = {\n sm: \"w-3.5 h-3.5\",\n md: \"w-4 h-4\",\n};\n\n/**\n * 좋아요/싫어요 피드백 버튼 컴포넌트\n */\nexport function FeedbackButtons({\n messageId,\n initialFeedback = \"none\",\n onFeedback,\n size = \"sm\",\n className,\n}: FeedbackButtonsProps) {\n const [feedback, setFeedback] = React.useState<FeedbackType>(initialFeedback);\n\n const handleFeedback = (type: FeedbackType) => {\n const newFeedback = feedback === type ? \"none\" : type;\n setFeedback(newFeedback);\n onFeedback?.(messageId, newFeedback);\n };\n\n return (\n <div\n role=\"group\"\n aria-label=\"메시지 피드백\"\n className={cn(\"inline-flex items-center gap-1\", className)}\n >\n <button\n onClick={() => handleFeedback(\"like\")}\n aria-pressed={feedback === \"like\"}\n aria-label={FEEDBACK_LABELS.like}\n className={cn(\n \"rounded-md transition-colors\",\n sizeStyles[size],\n feedback === \"like\"\n ? \"text-[var(--axis-status-success)] bg-[color:color-mix(in_srgb,var(--axis-status-success)_10%,transparent)]\"\n : \"text-[var(--axis-text-tertiary)] hover:text-[var(--axis-text-secondary)] hover:bg-[var(--axis-surface-secondary)]\"\n )}\n >\n <svg className={iconSizes[size]} fill={feedback === \"like\" ? \"currentColor\" : \"none\"} stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M14 9V5a3 3 0 00-3-3l-4 9v11h11.28a2 2 0 002-1.7l1.38-9a2 2 0 00-2-2.3H14zm-9 11H3a2 2 0 01-2-2v-7a2 2 0 012-2h2\" />\n </svg>\n </button>\n <button\n onClick={() => handleFeedback(\"dislike\")}\n aria-pressed={feedback === \"dislike\"}\n aria-label={FEEDBACK_LABELS.dislike}\n className={cn(\n \"rounded-md transition-colors\",\n sizeStyles[size],\n feedback === \"dislike\"\n ? \"text-[var(--axis-status-error)] bg-[color:color-mix(in_srgb,var(--axis-status-error)_10%,transparent)]\"\n : \"text-[var(--axis-text-tertiary)] hover:text-[var(--axis-text-secondary)] hover:bg-[var(--axis-surface-secondary)]\"\n )}\n >\n <svg className={iconSizes[size]} fill={feedback === \"dislike\" ? \"currentColor\" : \"none\"} stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M10 15V19a3 3 0 003 3l4-9V2H5.72a2 2 0 00-2 1.7l-1.38 9a2 2 0 002 2.3H10zm9-13h2a2 2 0 012 2v7a2 2 0 01-2 2h-2\" />\n </svg>\n </button>\n </div>\n );\n}\n"
16
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { FEEDBACK_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type FeedbackType = \"like\" | \"dislike\" | \"none\";\n\nexport interface FeedbackButtonsProps {\n /** 메시지 ID */\n messageId: string;\n /** 초기 피드백 상태 */\n initialFeedback?: FeedbackType;\n /** 피드백 변경 콜백 */\n onFeedback?: (messageId: string, feedback: FeedbackType) => void;\n /** 크기 */\n size?: \"sm\" | \"md\";\n /** 추가 클래스 */\n className?: string;\n}\n\nconst sizeStyles = {\n sm: \"p-1\",\n md: \"p-1.5\",\n};\n\nconst iconSizes = {\n sm: \"w-3.5 h-3.5\",\n md: \"w-4 h-4\",\n};\n\n/**\n * 좋아요/싫어요 피드백 버튼 컴포넌트\n */\nexport function FeedbackButtons({\n messageId,\n initialFeedback = \"none\",\n onFeedback,\n size = \"sm\",\n className,\n}: FeedbackButtonsProps) {\n const [feedback, setFeedback] = React.useState<FeedbackType>(initialFeedback);\n\n const handleFeedback = (type: FeedbackType) => {\n const newFeedback = feedback === type ? \"none\" : type;\n setFeedback(newFeedback);\n onFeedback?.(messageId, newFeedback);\n };\n\n return (\n <div\n role=\"group\"\n aria-label=\"메시지 피드백\"\n className={cn(\"inline-flex items-center gap-1\", className)}\n >\n <button\n onClick={() => handleFeedback(\"like\")}\n aria-pressed={feedback === \"like\"}\n aria-label={FEEDBACK_LABELS.like}\n className={cn(\n \"rounded-md transition-colors\",\n sizeStyles[size],\n feedback === \"like\"\n ? \"text-[var(--axis-text-success)] bg-[color:color-mix(in_srgb,var(--axis-status-success)_10%,transparent)]\"\n : \"text-[var(--axis-text-tertiary)] hover:text-[var(--axis-text-secondary)] hover:bg-[var(--axis-surface-secondary)]\"\n )}\n >\n <svg className={iconSizes[size]} fill={feedback === \"like\" ? \"currentColor\" : \"none\"} stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M14 9V5a3 3 0 00-3-3l-4 9v11h11.28a2 2 0 002-1.7l1.38-9a2 2 0 00-2-2.3H14zm-9 11H3a2 2 0 01-2-2v-7a2 2 0 012-2h2\" />\n </svg>\n </button>\n <button\n onClick={() => handleFeedback(\"dislike\")}\n aria-pressed={feedback === \"dislike\"}\n aria-label={FEEDBACK_LABELS.dislike}\n className={cn(\n \"rounded-md transition-colors\",\n sizeStyles[size],\n feedback === \"dislike\"\n ? \"text-[var(--axis-text-error)] bg-[color:color-mix(in_srgb,var(--axis-status-error)_10%,transparent)]\"\n : \"text-[var(--axis-text-tertiary)] hover:text-[var(--axis-text-secondary)] hover:bg-[var(--axis-surface-secondary)]\"\n )}\n >\n <svg className={iconSizes[size]} fill={feedback === \"dislike\" ? \"currentColor\" : \"none\"} stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M10 15V19a3 3 0 003 3l4-9V2H5.72a2 2 0 00-2 1.7l-1.38 9a2 2 0 002 2.3H10zm9-13h2a2 2 0 012 2v7a2 2 0 01-2 2h-2\" />\n </svg>\n </button>\n </div>\n );\n}\n"
17
17
  }
18
18
  ]
19
19
  }