@codrstudio/openclaude-chat 0.1.0 → 0.2.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.
Files changed (62) hide show
  1. package/dist/components/StreamingIndicator.js +5 -5
  2. package/dist/display/DisplayReactRenderer.js +12 -12
  3. package/dist/display/react-sandbox/bootstrap.js +150 -150
  4. package/dist/styles.css +1 -2
  5. package/package.json +64 -61
  6. package/src/components/Chat.tsx +107 -107
  7. package/src/components/ErrorNote.tsx +35 -35
  8. package/src/components/LazyRender.tsx +42 -42
  9. package/src/components/Markdown.tsx +114 -114
  10. package/src/components/MessageBubble.tsx +107 -107
  11. package/src/components/MessageInput.tsx +421 -421
  12. package/src/components/MessageList.tsx +153 -153
  13. package/src/components/StreamingIndicator.tsx +19 -19
  14. package/src/display/AlertRenderer.tsx +23 -23
  15. package/src/display/CarouselRenderer.tsx +141 -141
  16. package/src/display/ChartRenderer.tsx +195 -195
  17. package/src/display/ChoiceButtonsRenderer.tsx +114 -114
  18. package/src/display/CodeBlockRenderer.tsx +49 -49
  19. package/src/display/ComparisonTableRenderer.tsx +132 -132
  20. package/src/display/DataTableRenderer.tsx +144 -144
  21. package/src/display/DisplayReactRenderer.tsx +269 -269
  22. package/src/display/FileCardRenderer.tsx +55 -55
  23. package/src/display/GalleryRenderer.tsx +65 -65
  24. package/src/display/ImageViewerRenderer.tsx +114 -114
  25. package/src/display/LinkPreviewRenderer.tsx +74 -74
  26. package/src/display/MapViewRenderer.tsx +75 -75
  27. package/src/display/MetricCardRenderer.tsx +29 -29
  28. package/src/display/PriceHighlightRenderer.tsx +62 -62
  29. package/src/display/ProductCardRenderer.tsx +112 -112
  30. package/src/display/ProgressStepsRenderer.tsx +59 -59
  31. package/src/display/SourcesListRenderer.tsx +47 -47
  32. package/src/display/SpreadsheetRenderer.tsx +86 -86
  33. package/src/display/StepTimelineRenderer.tsx +75 -75
  34. package/src/display/index.ts +21 -21
  35. package/src/display/react-sandbox/bootstrap.ts +155 -155
  36. package/src/display/registry.ts +84 -84
  37. package/src/display/sdk-types.ts +217 -217
  38. package/src/hooks/ChatProvider.tsx +21 -21
  39. package/src/hooks/useIsMobile.ts +15 -15
  40. package/src/hooks/useOpenClaudeChat.ts +476 -476
  41. package/src/index.ts +76 -76
  42. package/src/lib/utils.ts +6 -6
  43. package/src/parts/PartErrorBoundary.tsx +51 -51
  44. package/src/parts/PartRenderer.tsx +145 -145
  45. package/src/parts/ReasoningBlock.tsx +41 -41
  46. package/src/parts/ToolActivity.tsx +78 -78
  47. package/src/parts/ToolResult.tsx +79 -79
  48. package/src/styles.css +2 -2
  49. package/src/types.ts +41 -41
  50. package/src/ui/alert.tsx +77 -77
  51. package/src/ui/badge.tsx +36 -36
  52. package/src/ui/button.tsx +54 -54
  53. package/src/ui/card.tsx +68 -68
  54. package/src/ui/collapsible.tsx +7 -7
  55. package/src/ui/dialog.tsx +122 -122
  56. package/src/ui/dropdown-menu.tsx +76 -76
  57. package/src/ui/input.tsx +24 -24
  58. package/src/ui/progress.tsx +36 -36
  59. package/src/ui/scroll-area.tsx +48 -48
  60. package/src/ui/separator.tsx +31 -31
  61. package/src/ui/skeleton.tsx +9 -9
  62. package/src/ui/table.tsx +114 -114
@@ -1,195 +1,195 @@
1
- import type { DisplayChart } from "./sdk-types.js";
2
- import {
3
- AreaChart,
4
- Area,
5
- BarChart,
6
- Bar,
7
- LineChart,
8
- Line,
9
- PieChart,
10
- Pie,
11
- Cell,
12
- CartesianGrid,
13
- Tooltip,
14
- ResponsiveContainer,
15
- XAxis,
16
- YAxis,
17
- } from "recharts";
18
- import { Card } from "../ui/card";
19
-
20
- const CHART_COLORS = [
21
- "var(--chart-1)",
22
- "var(--chart-2)",
23
- "var(--chart-3)",
24
- "var(--chart-4)",
25
- "var(--chart-5)",
26
- ];
27
-
28
- const AXIS_PROPS = {
29
- stroke: "var(--border)",
30
- tick: { fill: "var(--muted-foreground)", fontSize: 12 },
31
- };
32
-
33
- const TOOLTIP_STYLE = {
34
- contentStyle: {
35
- background: "var(--card)",
36
- border: "1px solid var(--border)",
37
- color: "var(--card-foreground)",
38
- borderRadius: "0.375rem",
39
- fontSize: "0.75rem",
40
- },
41
- };
42
-
43
- function formatValue(value: number, format?: DisplayChart["format"]): string {
44
- if (!format) return String(value);
45
- const locale = format.locale ?? "pt-BR";
46
- const prefix = format.prefix ?? "";
47
- const suffix = format.suffix ?? "";
48
- if (prefix.includes("R$") || prefix.includes("$") || prefix.includes("€")) {
49
- const currencyMap: Record<string, string> = {
50
- "R$": "BRL",
51
- "$": "USD",
52
- "€": "EUR",
53
- };
54
- const currency = Object.keys(currencyMap).find(k => prefix.includes(k));
55
- if (currency) {
56
- return new Intl.NumberFormat(locale, { style: "currency", currency: currencyMap[currency] }).format(value);
57
- }
58
- }
59
- if (suffix.includes("%")) {
60
- return new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 }).format(value / 100);
61
- }
62
- const formatted = new Intl.NumberFormat(locale).format(value);
63
- return `${prefix}${formatted}${suffix}`;
64
- }
65
-
66
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
- function makeTooltipFormatter(format?: DisplayChart["format"]): any {
68
- return (value: number | string | undefined): [string, string] => [
69
- typeof value === "number" ? formatValue(value, format) : String(value ?? ""),
70
- "",
71
- ];
72
- }
73
-
74
- export function ChartRenderer({ type, title, data, format }: DisplayChart) {
75
- const chartData = data.map((d) => ({ name: d.label, value: d.value, color: d.color }));
76
- const tooltipFormatter = makeTooltipFormatter(format);
77
-
78
- const sharedProps = {
79
- data: chartData,
80
- margin: { top: 4, right: 8, left: 8, bottom: 4 },
81
- };
82
-
83
- return (
84
- <Card className="p-4">
85
- {title && <p className="text-sm font-medium text-foreground mb-4">{title}</p>}
86
- <div className="min-h-[200px]">
87
- <ResponsiveContainer width="100%" height={200}>
88
- {renderChart(type, chartData, sharedProps, tooltipFormatter)}
89
- </ResponsiveContainer>
90
- </div>
91
- </Card>
92
- );
93
- }
94
-
95
- function renderChart(
96
- type: DisplayChart["type"],
97
- data: Array<{ name: string; value: number; color?: string }>,
98
- sharedProps: { data: typeof data; margin: { top: number; right: number; left: number; bottom: number } },
99
- tooltipFormatter: (v: number | string | undefined) => [string, string]
100
- ) {
101
- switch (type) {
102
- case "bar":
103
- return (
104
- <BarChart {...sharedProps}>
105
- <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
106
- <XAxis dataKey="name" {...AXIS_PROPS} />
107
- <YAxis width={48} {...AXIS_PROPS} />
108
- <Tooltip formatter={tooltipFormatter} {...TOOLTIP_STYLE} />
109
- <Bar dataKey="value" radius={[4, 4, 0, 0]}>
110
- {data.map((entry, index) => (
111
- <Cell key={index} fill={entry.color ?? CHART_COLORS[index % CHART_COLORS.length]} />
112
- ))}
113
- </Bar>
114
- </BarChart>
115
- );
116
-
117
- case "line":
118
- return (
119
- <LineChart {...sharedProps}>
120
- <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
121
- <XAxis dataKey="name" {...AXIS_PROPS} />
122
- <YAxis width={48} {...AXIS_PROPS} />
123
- <Tooltip formatter={tooltipFormatter} {...TOOLTIP_STYLE} />
124
- <Line
125
- type="monotone"
126
- dataKey="value"
127
- stroke={CHART_COLORS[0]}
128
- strokeWidth={2}
129
- dot={{ r: 4, fill: CHART_COLORS[0] }}
130
- />
131
- </LineChart>
132
- );
133
-
134
- case "area":
135
- return (
136
- <AreaChart {...sharedProps}>
137
- <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
138
- <XAxis dataKey="name" {...AXIS_PROPS} />
139
- <YAxis width={48} {...AXIS_PROPS} />
140
- <Tooltip formatter={tooltipFormatter} {...TOOLTIP_STYLE} />
141
- <Area
142
- type="monotone"
143
- dataKey="value"
144
- stroke={CHART_COLORS[0]}
145
- fill={CHART_COLORS[0]}
146
- fillOpacity={0.2}
147
- strokeWidth={2}
148
- />
149
- </AreaChart>
150
- );
151
-
152
- case "pie":
153
- return (
154
- <PieChart>
155
- <Tooltip formatter={tooltipFormatter} {...TOOLTIP_STYLE} />
156
- <Pie
157
- data={data}
158
- dataKey="value"
159
- nameKey="name"
160
- cx="50%"
161
- cy="50%"
162
- outerRadius="70%"
163
- label={({ name }) => name}
164
- labelLine={{ stroke: "var(--muted-foreground)" }}
165
- >
166
- {data.map((entry, index) => (
167
- <Cell key={index} fill={entry.color ?? CHART_COLORS[index % CHART_COLORS.length]} />
168
- ))}
169
- </Pie>
170
- </PieChart>
171
- );
172
-
173
- case "donut":
174
- return (
175
- <PieChart>
176
- <Tooltip formatter={tooltipFormatter} {...TOOLTIP_STYLE} />
177
- <Pie
178
- data={data}
179
- dataKey="value"
180
- nameKey="name"
181
- cx="50%"
182
- cy="50%"
183
- innerRadius="40%"
184
- outerRadius="70%"
185
- label={({ name }) => name}
186
- labelLine={{ stroke: "var(--muted-foreground)" }}
187
- >
188
- {data.map((entry, index) => (
189
- <Cell key={index} fill={entry.color ?? CHART_COLORS[index % CHART_COLORS.length]} />
190
- ))}
191
- </Pie>
192
- </PieChart>
193
- );
194
- }
195
- }
1
+ import type { DisplayChart } from "./sdk-types.js";
2
+ import {
3
+ AreaChart,
4
+ Area,
5
+ BarChart,
6
+ Bar,
7
+ LineChart,
8
+ Line,
9
+ PieChart,
10
+ Pie,
11
+ Cell,
12
+ CartesianGrid,
13
+ Tooltip,
14
+ ResponsiveContainer,
15
+ XAxis,
16
+ YAxis,
17
+ } from "recharts";
18
+ import { Card } from "../ui/card";
19
+
20
+ const CHART_COLORS = [
21
+ "var(--chart-1)",
22
+ "var(--chart-2)",
23
+ "var(--chart-3)",
24
+ "var(--chart-4)",
25
+ "var(--chart-5)",
26
+ ];
27
+
28
+ const AXIS_PROPS = {
29
+ stroke: "var(--border)",
30
+ tick: { fill: "var(--muted-foreground)", fontSize: 12 },
31
+ };
32
+
33
+ const TOOLTIP_STYLE = {
34
+ contentStyle: {
35
+ background: "var(--card)",
36
+ border: "1px solid var(--border)",
37
+ color: "var(--card-foreground)",
38
+ borderRadius: "0.375rem",
39
+ fontSize: "0.75rem",
40
+ },
41
+ };
42
+
43
+ function formatValue(value: number, format?: DisplayChart["format"]): string {
44
+ if (!format) return String(value);
45
+ const locale = format.locale ?? "pt-BR";
46
+ const prefix = format.prefix ?? "";
47
+ const suffix = format.suffix ?? "";
48
+ if (prefix.includes("R$") || prefix.includes("$") || prefix.includes("€")) {
49
+ const currencyMap: Record<string, string> = {
50
+ "R$": "BRL",
51
+ "$": "USD",
52
+ "€": "EUR",
53
+ };
54
+ const currency = Object.keys(currencyMap).find(k => prefix.includes(k));
55
+ if (currency) {
56
+ return new Intl.NumberFormat(locale, { style: "currency", currency: currencyMap[currency] }).format(value);
57
+ }
58
+ }
59
+ if (suffix.includes("%")) {
60
+ return new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 }).format(value / 100);
61
+ }
62
+ const formatted = new Intl.NumberFormat(locale).format(value);
63
+ return `${prefix}${formatted}${suffix}`;
64
+ }
65
+
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ function makeTooltipFormatter(format?: DisplayChart["format"]): any {
68
+ return (value: number | string | undefined): [string, string] => [
69
+ typeof value === "number" ? formatValue(value, format) : String(value ?? ""),
70
+ "",
71
+ ];
72
+ }
73
+
74
+ export function ChartRenderer({ type, title, data, format }: DisplayChart) {
75
+ const chartData = data.map((d) => ({ name: d.label, value: d.value, color: d.color }));
76
+ const tooltipFormatter = makeTooltipFormatter(format);
77
+
78
+ const sharedProps = {
79
+ data: chartData,
80
+ margin: { top: 4, right: 8, left: 8, bottom: 4 },
81
+ };
82
+
83
+ return (
84
+ <Card className="p-4">
85
+ {title && <p className="text-sm font-medium text-foreground mb-4">{title}</p>}
86
+ <div className="min-h-[200px]">
87
+ <ResponsiveContainer width="100%" height={200}>
88
+ {renderChart(type, chartData, sharedProps, tooltipFormatter)}
89
+ </ResponsiveContainer>
90
+ </div>
91
+ </Card>
92
+ );
93
+ }
94
+
95
+ function renderChart(
96
+ type: DisplayChart["type"],
97
+ data: Array<{ name: string; value: number; color?: string }>,
98
+ sharedProps: { data: typeof data; margin: { top: number; right: number; left: number; bottom: number } },
99
+ tooltipFormatter: (v: number | string | undefined) => [string, string]
100
+ ) {
101
+ switch (type) {
102
+ case "bar":
103
+ return (
104
+ <BarChart {...sharedProps}>
105
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
106
+ <XAxis dataKey="name" {...AXIS_PROPS} />
107
+ <YAxis width={48} {...AXIS_PROPS} />
108
+ <Tooltip formatter={tooltipFormatter} {...TOOLTIP_STYLE} />
109
+ <Bar dataKey="value" radius={[4, 4, 0, 0]}>
110
+ {data.map((entry, index) => (
111
+ <Cell key={index} fill={entry.color ?? CHART_COLORS[index % CHART_COLORS.length]} />
112
+ ))}
113
+ </Bar>
114
+ </BarChart>
115
+ );
116
+
117
+ case "line":
118
+ return (
119
+ <LineChart {...sharedProps}>
120
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
121
+ <XAxis dataKey="name" {...AXIS_PROPS} />
122
+ <YAxis width={48} {...AXIS_PROPS} />
123
+ <Tooltip formatter={tooltipFormatter} {...TOOLTIP_STYLE} />
124
+ <Line
125
+ type="monotone"
126
+ dataKey="value"
127
+ stroke={CHART_COLORS[0]}
128
+ strokeWidth={2}
129
+ dot={{ r: 4, fill: CHART_COLORS[0] }}
130
+ />
131
+ </LineChart>
132
+ );
133
+
134
+ case "area":
135
+ return (
136
+ <AreaChart {...sharedProps}>
137
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
138
+ <XAxis dataKey="name" {...AXIS_PROPS} />
139
+ <YAxis width={48} {...AXIS_PROPS} />
140
+ <Tooltip formatter={tooltipFormatter} {...TOOLTIP_STYLE} />
141
+ <Area
142
+ type="monotone"
143
+ dataKey="value"
144
+ stroke={CHART_COLORS[0]}
145
+ fill={CHART_COLORS[0]}
146
+ fillOpacity={0.2}
147
+ strokeWidth={2}
148
+ />
149
+ </AreaChart>
150
+ );
151
+
152
+ case "pie":
153
+ return (
154
+ <PieChart>
155
+ <Tooltip formatter={tooltipFormatter} {...TOOLTIP_STYLE} />
156
+ <Pie
157
+ data={data}
158
+ dataKey="value"
159
+ nameKey="name"
160
+ cx="50%"
161
+ cy="50%"
162
+ outerRadius="70%"
163
+ label={({ name }) => name}
164
+ labelLine={{ stroke: "var(--muted-foreground)" }}
165
+ >
166
+ {data.map((entry, index) => (
167
+ <Cell key={index} fill={entry.color ?? CHART_COLORS[index % CHART_COLORS.length]} />
168
+ ))}
169
+ </Pie>
170
+ </PieChart>
171
+ );
172
+
173
+ case "donut":
174
+ return (
175
+ <PieChart>
176
+ <Tooltip formatter={tooltipFormatter} {...TOOLTIP_STYLE} />
177
+ <Pie
178
+ data={data}
179
+ dataKey="value"
180
+ nameKey="name"
181
+ cx="50%"
182
+ cy="50%"
183
+ innerRadius="40%"
184
+ outerRadius="70%"
185
+ label={({ name }) => name}
186
+ labelLine={{ stroke: "var(--muted-foreground)" }}
187
+ >
188
+ {data.map((entry, index) => (
189
+ <Cell key={index} fill={entry.color ?? CHART_COLORS[index % CHART_COLORS.length]} />
190
+ ))}
191
+ </Pie>
192
+ </PieChart>
193
+ );
194
+ }
195
+ }
@@ -1,114 +1,114 @@
1
- import type { DisplayChoices } from "./sdk-types.js";
2
- import { useState } from "react";
3
- import { Button } from "../ui/button.js";
4
- import { Card } from "../ui/card.js";
5
- import { cn } from "../lib/utils.js";
6
-
7
- type ChoiceButtonsProps = DisplayChoices & {
8
- onChoiceSelect?: (value: string) => void;
9
- };
10
-
11
- export function ChoiceButtonsRenderer({
12
- question,
13
- choices,
14
- layout,
15
- onChoiceSelect,
16
- }: ChoiceButtonsProps) {
17
- const [selected, setSelected] = useState<string | null>(null);
18
-
19
- function handleSelect(id: string) {
20
- setSelected(id);
21
- onChoiceSelect?.(id);
22
- }
23
-
24
- return (
25
- <div className="space-y-3 w-fit">
26
- {question && <p className="text-sm font-medium text-foreground">{question}</p>}
27
-
28
- <div
29
- className={cn(
30
- layout === "buttons" && "flex flex-wrap gap-2",
31
- layout === "cards" && "grid grid-cols-1 sm:grid-cols-2 gap-2",
32
- layout === "list" && "flex flex-col gap-1"
33
- )}
34
- role="group"
35
- aria-label={question ?? "Escolha uma opção"}
36
- >
37
- {choices.map((choice) => {
38
- const isSelected = selected === choice.id;
39
-
40
- if (layout === "buttons") {
41
- return (
42
- <Button
43
- key={choice.id}
44
- variant="outline"
45
- size="sm"
46
- onClick={() => handleSelect(choice.id)}
47
- aria-pressed={isSelected}
48
- className={cn(isSelected && "ring-2 ring-ring")}
49
- >
50
- {choice.icon && (
51
- <span aria-hidden="true">{choice.icon}</span>
52
- )}
53
- {choice.label}
54
- </Button>
55
- );
56
- }
57
-
58
- if (layout === "cards") {
59
- return (
60
- <Card
61
- key={choice.id}
62
- className={cn(
63
- "p-3 cursor-pointer hover:bg-muted/50 transition-colors",
64
- isSelected && "ring-2 ring-ring"
65
- )}
66
- onClick={() => handleSelect(choice.id)}
67
- role="button"
68
- aria-pressed={isSelected}
69
- >
70
- {choice.icon && (
71
- <span className="text-base" aria-hidden="true">{choice.icon}</span>
72
- )}
73
- <span className="block font-medium text-sm text-foreground">{choice.label}</span>
74
- {choice.description && (
75
- <span className="block text-xs text-muted-foreground mt-1">{choice.description}</span>
76
- )}
77
- </Card>
78
- );
79
- }
80
-
81
- // list
82
- return (
83
- <button
84
- key={choice.id}
85
- className={cn(
86
- "flex items-center gap-3 w-full rounded-md px-3 py-2 text-sm text-left hover:bg-muted transition-colors",
87
- isSelected && "bg-muted ring-1 ring-ring"
88
- )}
89
- onClick={() => handleSelect(choice.id)}
90
- aria-pressed={isSelected}
91
- >
92
- <span
93
- className={cn(
94
- "w-4 h-4 rounded-full border-2 border-input flex-shrink-0",
95
- isSelected && "border-primary bg-primary"
96
- )}
97
- aria-hidden="true"
98
- />
99
- <span className="flex items-center gap-2 flex-1 min-w-0">
100
- {choice.icon && (
101
- <span aria-hidden="true">{choice.icon}</span>
102
- )}
103
- <span className="font-medium text-foreground">{choice.label}</span>
104
- {choice.description && (
105
- <span className="text-xs text-muted-foreground ml-auto">{choice.description}</span>
106
- )}
107
- </span>
108
- </button>
109
- );
110
- })}
111
- </div>
112
- </div>
113
- );
114
- }
1
+ import type { DisplayChoices } from "./sdk-types.js";
2
+ import { useState } from "react";
3
+ import { Button } from "../ui/button.js";
4
+ import { Card } from "../ui/card.js";
5
+ import { cn } from "../lib/utils.js";
6
+
7
+ type ChoiceButtonsProps = DisplayChoices & {
8
+ onChoiceSelect?: (value: string) => void;
9
+ };
10
+
11
+ export function ChoiceButtonsRenderer({
12
+ question,
13
+ choices,
14
+ layout,
15
+ onChoiceSelect,
16
+ }: ChoiceButtonsProps) {
17
+ const [selected, setSelected] = useState<string | null>(null);
18
+
19
+ function handleSelect(id: string) {
20
+ setSelected(id);
21
+ onChoiceSelect?.(id);
22
+ }
23
+
24
+ return (
25
+ <div className="space-y-3 w-fit">
26
+ {question && <p className="text-sm font-medium text-foreground">{question}</p>}
27
+
28
+ <div
29
+ className={cn(
30
+ layout === "buttons" && "flex flex-wrap gap-2",
31
+ layout === "cards" && "grid grid-cols-1 sm:grid-cols-2 gap-2",
32
+ layout === "list" && "flex flex-col gap-1"
33
+ )}
34
+ role="group"
35
+ aria-label={question ?? "Escolha uma opção"}
36
+ >
37
+ {choices.map((choice) => {
38
+ const isSelected = selected === choice.id;
39
+
40
+ if (layout === "buttons") {
41
+ return (
42
+ <Button
43
+ key={choice.id}
44
+ variant="outline"
45
+ size="sm"
46
+ onClick={() => handleSelect(choice.id)}
47
+ aria-pressed={isSelected}
48
+ className={cn(isSelected && "ring-2 ring-ring")}
49
+ >
50
+ {choice.icon && (
51
+ <span aria-hidden="true">{choice.icon}</span>
52
+ )}
53
+ {choice.label}
54
+ </Button>
55
+ );
56
+ }
57
+
58
+ if (layout === "cards") {
59
+ return (
60
+ <Card
61
+ key={choice.id}
62
+ className={cn(
63
+ "p-3 cursor-pointer hover:bg-muted/50 transition-colors",
64
+ isSelected && "ring-2 ring-ring"
65
+ )}
66
+ onClick={() => handleSelect(choice.id)}
67
+ role="button"
68
+ aria-pressed={isSelected}
69
+ >
70
+ {choice.icon && (
71
+ <span className="text-base" aria-hidden="true">{choice.icon}</span>
72
+ )}
73
+ <span className="block font-medium text-sm text-foreground">{choice.label}</span>
74
+ {choice.description && (
75
+ <span className="block text-xs text-muted-foreground mt-1">{choice.description}</span>
76
+ )}
77
+ </Card>
78
+ );
79
+ }
80
+
81
+ // list
82
+ return (
83
+ <button
84
+ key={choice.id}
85
+ className={cn(
86
+ "flex items-center gap-3 w-full rounded-md px-3 py-2 text-sm text-left hover:bg-muted transition-colors",
87
+ isSelected && "bg-muted ring-1 ring-ring"
88
+ )}
89
+ onClick={() => handleSelect(choice.id)}
90
+ aria-pressed={isSelected}
91
+ >
92
+ <span
93
+ className={cn(
94
+ "w-4 h-4 rounded-full border-2 border-input flex-shrink-0",
95
+ isSelected && "border-primary bg-primary"
96
+ )}
97
+ aria-hidden="true"
98
+ />
99
+ <span className="flex items-center gap-2 flex-1 min-w-0">
100
+ {choice.icon && (
101
+ <span aria-hidden="true">{choice.icon}</span>
102
+ )}
103
+ <span className="font-medium text-foreground">{choice.label}</span>
104
+ {choice.description && (
105
+ <span className="text-xs text-muted-foreground ml-auto">{choice.description}</span>
106
+ )}
107
+ </span>
108
+ </button>
109
+ );
110
+ })}
111
+ </div>
112
+ </div>
113
+ );
114
+ }