@12-apps/mcp 1.0.0 → 1.3.1

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.
@@ -0,0 +1,125 @@
1
+ "use client";
2
+
3
+ import BoltOutlinedIcon from "@mui/icons-material/BoltOutlined";
4
+ import LanguageOutlinedIcon from "@mui/icons-material/LanguageOutlined";
5
+ import LockOpenOutlinedIcon from "@mui/icons-material/LockOpenOutlined";
6
+ import VerifiedUserOutlinedIcon from "@mui/icons-material/VerifiedUserOutlined";
7
+
8
+ import { Button } from "@12-apps/ui/form/Button";
9
+ import { Box } from "@12-apps/ui/mui/Box";
10
+ import { Stack } from "@12-apps/ui/mui/Stack";
11
+ import { Text } from "@12-apps/ui/typography/Text";
12
+
13
+ import { AI_CAPABILITIES, AI_PERMISSION_MODEL, type AiCapability } from "../guide";
14
+ import { AiCapabilities } from "./ai-capabilities";
15
+ import { FeatureBadge, type FeatureBadgeItem } from "./feature-badge";
16
+
17
+ const FEATURES: FeatureBadgeItem[] = [
18
+ { icon: <LockOpenOutlinedIcon />, label: "Usa o seu próprio login", caption: "Sem chaves ou credenciais extras" },
19
+ { icon: <BoltOutlinedIcon />, label: "Sem instalar nada", caption: "Conecta em poucos minutos" },
20
+ { icon: <VerifiedUserOutlinedIcon />, label: "Só o que você pode", caption: "As suas permissões, nada além" },
21
+ { icon: <LanguageOutlinedIcon />, label: "Navegador ou app", caption: "Claude, ChatGPT, Codex" },
22
+ ];
23
+
24
+ /** The single-column marketing hero: eyebrow + headline + description + CTA. */
25
+ function Hero({ onStart }: { onStart: () => void }): React.JSX.Element {
26
+ return (
27
+ <Stack spacing={2.5} sx={{ maxWidth: 680 }}>
28
+ <Box
29
+ sx={{
30
+ color: "primary.main",
31
+ fontSize: 12,
32
+ fontWeight: 700,
33
+ letterSpacing: 1.2,
34
+ textTransform: "uppercase",
35
+ }}
36
+ >
37
+ Integração com IA
38
+ </Box>
39
+ <Text variant="heading" size="xl" weight="bold" as="h1">
40
+ Conecte{" "}
41
+ <Box component="span" sx={{ color: "primary.main" }}>
42
+ assistentes de IA
43
+ </Box>{" "}
44
+ à sua loja
45
+ </Text>
46
+ <Text variant="body" color="secondary" as="p">
47
+ Deixe o Claude, o ChatGPT e outros assistentes responderem sobre o seu cardápio, estoque e
48
+ pedidos — e executarem ações por você, direto na conversa. Com segurança e sem instalar
49
+ nada.
50
+ </Text>
51
+ <Box sx={{ pt: 1 }}>
52
+ <Button onClick={onStart} data-testid="ai-landing-start">
53
+ Ver como conectar
54
+ </Button>
55
+ </Box>
56
+ </Stack>
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Homepage-style marketing landing for the AI integration (shown before the
62
+ * owner starts): a hero, the permission reassurance, a trust/feature strip, and
63
+ * the capability highlights. `onStart` begins the guided flow. `permissionModel`
64
+ * and `capabilities` default to the shared copy; apps can override.
65
+ */
66
+ export function AiLanding({
67
+ onStart,
68
+ permissionModel = AI_PERMISSION_MODEL,
69
+ capabilities = AI_CAPABILITIES,
70
+ }: {
71
+ onStart: () => void;
72
+ permissionModel?: string;
73
+ capabilities?: readonly AiCapability[];
74
+ }): React.JSX.Element {
75
+ return (
76
+ <Stack spacing={6} data-testid="ai-landing">
77
+ <Hero onStart={onStart} />
78
+
79
+ <Stack
80
+ direction="row"
81
+ spacing={1.5}
82
+ alignItems="flex-start"
83
+ data-testid="ai-permission-callout"
84
+ sx={{
85
+ p: 2,
86
+ borderRadius: 2,
87
+ bgcolor: "action.hover",
88
+ border: 1,
89
+ borderColor: "divider",
90
+ }}
91
+ >
92
+ <VerifiedUserOutlinedIcon sx={{ color: "success.main", mt: 0.25 }} />
93
+ <Text variant="body" as="p">
94
+ {permissionModel}
95
+ </Text>
96
+ </Stack>
97
+
98
+ <Box
99
+ sx={{
100
+ p: { xs: 2.5, md: 3 },
101
+ borderRadius: 3,
102
+ bgcolor: "action.hover",
103
+ border: 1,
104
+ borderColor: "divider",
105
+ }}
106
+ >
107
+ <Box
108
+ sx={{
109
+ display: "grid",
110
+ gap: 3,
111
+ gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr", md: "repeat(4, 1fr)" },
112
+ }}
113
+ >
114
+ {FEATURES.map((f) => (
115
+ <FeatureBadge key={f.label} icon={f.icon} label={f.label} caption={f.caption} />
116
+ ))}
117
+ </Box>
118
+ </Box>
119
+
120
+ <Box>
121
+ <AiCapabilities capabilities={capabilities} />
122
+ </Box>
123
+ </Stack>
124
+ );
125
+ }
@@ -0,0 +1,147 @@
1
+ "use client";
2
+
3
+ import {
4
+ GuidedSection,
5
+ OnboardingProvider,
6
+ useOnboarding,
7
+ type OnboardingStateSnapshot,
8
+ type OnboardingStore,
9
+ } from "@12-apps/onboarding";
10
+
11
+ import {
12
+ AI_CAPABILITIES,
13
+ AI_CONNECT_PROMPT,
14
+ AI_HOST_GUIDES,
15
+ AI_PERMISSION_MODEL,
16
+ type AiCapability,
17
+ type AiHostGuide,
18
+ } from "../guide";
19
+ import { AiLanding } from "./ai-landing";
20
+ import {
21
+ buildFlowSteps,
22
+ connectedSummary,
23
+ connectedTitle,
24
+ StatusBoard,
25
+ type AiConnection,
26
+ } from "./ai-steps";
27
+
28
+ export type { AiConnection } from "./ai-steps";
29
+
30
+ const DEFAULT_FEATURE_KEY = "ai_integration";
31
+
32
+ /** Props for the reusable AI-connect onboarding flow. */
33
+ export interface AiIntegrationOnboardingProps {
34
+ /** Persistence seam — the app wires this to its own backend (server actions). */
35
+ store: OnboardingStore;
36
+ /** The MCP endpoint URL to paste (derived by the app from its public origin). */
37
+ endpointUrl: string;
38
+ /** The owner's saved progress (null → first run / landing). */
39
+ initialState: OnboardingStateSnapshot | null;
40
+ /** The live MCP connections (one per connected assistant; empty when none). */
41
+ connections: readonly AiConnection[];
42
+ /** Onboarding feature key (persistence namespace). @default "ai_integration" */
43
+ featureKey?: string;
44
+ /** Show the dev-only "reset onboarding" button. @default false */
45
+ devReset?: boolean;
46
+ /** Assistants offered in the flow. @default the shared AI_HOST_GUIDES */
47
+ hosts?: readonly AiHostGuide[];
48
+ /** Capability cards on the landing. @default the shared AI_CAPABILITIES */
49
+ capabilities?: readonly AiCapability[];
50
+ /** Permission reassurance copy on the landing. @default AI_PERMISSION_MODEL */
51
+ permissionModel?: string;
52
+ /** Message pasted into the assistant on the Conectar step. @default AI_CONNECT_PROMPT */
53
+ connectPrompt?: string;
54
+ /**
55
+ * Re-check the live connection on the verify step's "Testar conexão" button —
56
+ * apps pass a router refresh (e.g. Next's `router.refresh`). @default a full
57
+ * `window.location.reload()`.
58
+ */
59
+ onRetest?: () => void;
60
+ }
61
+
62
+ /** Resolved (defaults-applied) props threaded to the in-provider flow body. */
63
+ interface FlowProps {
64
+ endpointUrl: string;
65
+ connections: readonly AiConnection[];
66
+ hosts: readonly AiHostGuide[];
67
+ capabilities: readonly AiCapability[];
68
+ permissionModel: string;
69
+ connectPrompt: string;
70
+ onRetest: () => void;
71
+ devReset: boolean;
72
+ }
73
+
74
+ /**
75
+ * The flow body — inside the OnboardingProvider so it can read the selected host
76
+ * and pick the path. A host with a published `pluginUrl` gets the simplified
77
+ * Escolher → Instalar → Confirmar flow (no URL to copy); otherwise the full
78
+ * Escolher → Copiar URL → Configurar → Conectar → Confirmar flow.
79
+ */
80
+ function AiOnboardingFlow(props: FlowProps): React.JSX.Element {
81
+ const { endpointUrl, connections, hosts, capabilities, permissionModel, connectPrompt, onRetest, devReset } =
82
+ props;
83
+ const { state } = useOnboarding();
84
+ const selectedHost = hosts.find((h) => h.id === state.data.selectedHost) ?? hosts[0]!;
85
+
86
+ const steps = buildFlowSteps({ host: selectedHost, hosts, endpointUrl, connectPrompt, connections, onRetest });
87
+ const connectedHostId = (state.data.connectedHost ?? state.data.selectedHost) as string | undefined;
88
+
89
+ return (
90
+ <GuidedSection
91
+ steps={steps}
92
+ title="Conecte assistentes de IA à sua loja"
93
+ startLabel="Ver como conectar"
94
+ renderLanding={(start) => (
95
+ <AiLanding onStart={start} permissionModel={permissionModel} capabilities={capabilities} />
96
+ )}
97
+ configuredTitle={connectedTitle(connections, hosts, connectedHostId)}
98
+ configuredSummary={connectedSummary(connections)}
99
+ editLabel="Conectar IA"
100
+ completedContent={(nav) => <StatusBoard nav={nav} connections={connections} hosts={hosts} />}
101
+ devReset={devReset}
102
+ dataTestId="ai-onboarding"
103
+ />
104
+ );
105
+ }
106
+
107
+ /**
108
+ * URL-free, persisted onboarding for the AI/MCP integration — a five-step wizard
109
+ * (pick an assistant, copy the store URL, configure the connector, connect, then
110
+ * verify) whose position + chosen assistant are saved via `@12-apps/onboarding`, so
111
+ * a refresh resumes exactly where the owner left off. The live MCP connection
112
+ * signal drives the verify step and the completed status board.
113
+ *
114
+ * App-agnostic: the app supplies the persistence `store`, the `endpointUrl`, and
115
+ * the live `connections` (one per connected assistant); content (hosts,
116
+ * capabilities, copy) defaults to the shared guide but can be overridden per app.
117
+ */
118
+ export function AiIntegrationOnboarding({
119
+ store,
120
+ endpointUrl,
121
+ initialState,
122
+ connections,
123
+ featureKey = DEFAULT_FEATURE_KEY,
124
+ devReset = false,
125
+ hosts = AI_HOST_GUIDES,
126
+ capabilities = AI_CAPABILITIES,
127
+ permissionModel = AI_PERMISSION_MODEL,
128
+ connectPrompt = AI_CONNECT_PROMPT,
129
+ onRetest = () => {
130
+ if (typeof window !== "undefined") window.location.reload();
131
+ },
132
+ }: AiIntegrationOnboardingProps): React.JSX.Element {
133
+ return (
134
+ <OnboardingProvider featureKey={featureKey} store={store} initialState={initialState}>
135
+ <AiOnboardingFlow
136
+ endpointUrl={endpointUrl}
137
+ connections={connections}
138
+ hosts={hosts}
139
+ capabilities={capabilities}
140
+ permissionModel={permissionModel}
141
+ connectPrompt={connectPrompt}
142
+ onRetest={onRetest}
143
+ devReset={devReset}
144
+ />
145
+ </OnboardingProvider>
146
+ );
147
+ }
@@ -0,0 +1,124 @@
1
+ "use client";
2
+
3
+ import AddLinkOutlinedIcon from "@mui/icons-material/AddLinkOutlined";
4
+ import CheckCircleIcon from "@mui/icons-material/CheckCircle";
5
+
6
+ import { Button } from "@12-apps/ui/form/Button";
7
+ import { Box } from "@12-apps/ui/mui/Box";
8
+ import { Stack } from "@12-apps/ui/mui/Stack";
9
+ import { Text } from "@12-apps/ui/typography/Text";
10
+
11
+ import type { AiHostGuide } from "../guide";
12
+ import { HostBrandAvatar } from "./ai-icons";
13
+
14
+ /** One assistant's connection state. */
15
+ export interface HostStatus {
16
+ host: AiHostGuide;
17
+ connected: boolean;
18
+ /** Freeform activity line for a connected host (e.g. "ativo há 3 min"). */
19
+ detail?: string;
20
+ }
21
+
22
+ /** A single green (connected) / red (to connect) status box for one assistant. */
23
+ function StatusBox({
24
+ status,
25
+ onConnect,
26
+ }: {
27
+ status: HostStatus;
28
+ onConnect: (hostId: string) => void;
29
+ }): React.JSX.Element {
30
+ const { host, connected, detail } = status;
31
+ return (
32
+ <Box
33
+ data-testid={`ai-status-${host.id}`}
34
+ data-connected={connected}
35
+ sx={{
36
+ display: "flex",
37
+ alignItems: "center",
38
+ gap: 1.5,
39
+ p: 2,
40
+ borderRadius: 3,
41
+ border: 1,
42
+ borderColor: connected ? "success.main" : "error.main",
43
+ bgcolor: "background.paper",
44
+ }}
45
+ >
46
+ <HostBrandAvatar brand={host.brand} size={36} />
47
+ <Box sx={{ minWidth: 0, flexGrow: 1 }}>
48
+ <Stack direction="row" spacing={0.75} alignItems="center">
49
+ <Text variant="body" weight="bold" as="span">
50
+ {host.label}
51
+ </Text>
52
+ {connected ? <CheckCircleIcon sx={{ fontSize: 18, color: "success.main" }} /> : null}
53
+ </Stack>
54
+ <Text variant="caption" as="p" color="secondary">
55
+ {connected ? (detail ?? "Conectado") : "Ainda não conectado"}
56
+ </Text>
57
+ </Box>
58
+ {connected ? (
59
+ <Box
60
+ sx={{
61
+ px: 1,
62
+ py: 0.25,
63
+ borderRadius: 999,
64
+ bgcolor: "success.main",
65
+ color: "success.contrastText",
66
+ }}
67
+ >
68
+ <Text variant="caption" weight="bold" as="span">
69
+ Conectado
70
+ </Text>
71
+ </Box>
72
+ ) : (
73
+ <Button
74
+ variant="outline"
75
+ size="sm"
76
+ onClick={() => onConnect(host.id)}
77
+ data-testid={`ai-status-connect-${host.id}`}
78
+ >
79
+ <AddLinkOutlinedIcon sx={{ fontSize: 16, mr: 0.5 }} />
80
+ Conectar
81
+ </Button>
82
+ )}
83
+ </Box>
84
+ );
85
+ }
86
+
87
+ /**
88
+ * The completed-state status board for the AI integration: every assistant as a
89
+ * box — green when connected, red when still to connect (with a "Conectar"
90
+ * button that re-enters the guided flow for that host). Each connection is
91
+ * attributed to its provider (derived from the OAuth client + confirmed by
92
+ * `announceAiConnection`), so several assistants can show connected at once.
93
+ */
94
+ export function AiStatusBoard({
95
+ statuses,
96
+ onConnect,
97
+ }: {
98
+ statuses: readonly HostStatus[];
99
+ onConnect: (hostId: string) => void;
100
+ }): React.JSX.Element {
101
+ return (
102
+ <Stack spacing={2} data-testid="ai-status-board">
103
+ <Box>
104
+ <Text variant="heading" size="sm" as="h2">
105
+ Assistentes conectados
106
+ </Text>
107
+ <Text variant="caption" as="p" color="secondary">
108
+ Em verde os que já operam a sua loja; em vermelho os que faltam conectar.
109
+ </Text>
110
+ </Box>
111
+ <Box
112
+ sx={{
113
+ display: "grid",
114
+ gap: 2,
115
+ gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
116
+ }}
117
+ >
118
+ {statuses.map((status) => (
119
+ <StatusBox key={status.host.id} status={status} onConnect={onConnect} />
120
+ ))}
121
+ </Box>
122
+ </Stack>
123
+ );
124
+ }
@@ -0,0 +1,167 @@
1
+ "use client";
2
+
3
+ import { type GuidedNav, type GuidedStep } from "@12-apps/onboarding";
4
+
5
+ import { providerForHostId, type AiHostGuide } from "../guide";
6
+ import { activeAgo, resolveHost, type AiConnection } from "./ai-connection-utils";
7
+ import {
8
+ ConfigureStageStep,
9
+ ConfigureStep,
10
+ ConfirmStep,
11
+ ConnectStep,
12
+ CopyUrlStep,
13
+ InstallStep,
14
+ } from "./ai-flow-steps";
15
+ import { AiStatusBoard, type HostStatus } from "./ai-status-board";
16
+ import { HostSelectStep } from "./host-select-step";
17
+
18
+ export type { AiConnection } from "./ai-connection-utils";
19
+ export { connectedSummary, connectedTitle } from "./ai-connection-utils";
20
+
21
+ /** The completed status board: green for each connected host, red for the rest. */
22
+ export function StatusBoard({
23
+ nav,
24
+ connections,
25
+ hosts,
26
+ }: {
27
+ nav: GuidedNav;
28
+ connections: readonly AiConnection[];
29
+ hosts: readonly AiHostGuide[];
30
+ }): React.JSX.Element {
31
+ const legacyHostId = (nav.data.connectedHost ?? nav.data.selectedHost) as string | undefined;
32
+
33
+ const statuses: HostStatus[] = hosts.map((host) => {
34
+ const provider = providerForHostId(host.id);
35
+ // Prefer a provider-attributed connection; fall back to a legacy null-host
36
+ // connection only for the host the owner completed the flow with.
37
+ const connection =
38
+ (provider ? connections.find((c) => c.host === provider) : undefined) ??
39
+ (host.id === legacyHostId ? connections.find((c) => c.host === null) : undefined) ??
40
+ null;
41
+ return {
42
+ host,
43
+ connected: connection !== null,
44
+ detail: connection ? activeAgo(connection.lastActiveAt) : undefined,
45
+ };
46
+ });
47
+
48
+ return (
49
+ <AiStatusBoard
50
+ statuses={statuses}
51
+ onConnect={(hostId) =>
52
+ nav.goTo(hosts.find((h) => h.id === hostId)?.pluginUrl ? "install" : "copy", {
53
+ selectedHost: hostId,
54
+ })
55
+ }
56
+ />
57
+ );
58
+ }
59
+
60
+ /** The first step (host picker) — picking a card advances to the right path. */
61
+ function selectStep(hosts: readonly AiHostGuide[]): GuidedStep {
62
+ return {
63
+ id: "select",
64
+ label: "Escolher",
65
+ render: (nav: GuidedNav) => (
66
+ <HostSelectStep
67
+ hosts={hosts}
68
+ selectedId={(nav.data.selectedHost as string | undefined) ?? null}
69
+ onSelect={(hostId) =>
70
+ nav.goTo(hosts.find((h) => h.id === hostId)?.pluginUrl ? "install" : "copy", {
71
+ selectedHost: hostId,
72
+ })
73
+ }
74
+ />
75
+ ),
76
+ };
77
+ }
78
+
79
+ /** The Copiar-URL step, shared by every manual flow. */
80
+ function copyStep(endpointUrl: string): GuidedStep {
81
+ return {
82
+ id: "copy",
83
+ label: "Copiar URL",
84
+ render: (nav: GuidedNav) => <CopyUrlStep nav={nav} endpointUrl={endpointUrl} />,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * The middle steps for the full manual flow. A host with `configureStages`
90
+ * (e.g. ChatGPT) gets one step per stage — each with its own deep link — after
91
+ * Copiar URL; otherwise the default copy → configure → connect split.
92
+ */
93
+ function manualMiddle(
94
+ host: AiHostGuide,
95
+ hosts: readonly AiHostGuide[],
96
+ endpointUrl: string,
97
+ connectPrompt: string,
98
+ ): GuidedStep[] {
99
+ if (host.configureStages && host.configureStages.length > 0) {
100
+ return [
101
+ copyStep(endpointUrl),
102
+ ...host.configureStages.map((stage) => ({
103
+ id: stage.id,
104
+ label: stage.label,
105
+ render: (nav: GuidedNav) => (
106
+ <ConfigureStageStep nav={nav} host={resolveHost(hosts, nav.data.selectedHost)} stage={stage} />
107
+ ),
108
+ })),
109
+ ];
110
+ }
111
+ return [
112
+ copyStep(endpointUrl),
113
+ {
114
+ id: "configure",
115
+ label: "Configurar",
116
+ render: (nav: GuidedNav) => <ConfigureStep nav={nav} host={resolveHost(hosts, nav.data.selectedHost)} />,
117
+ },
118
+ {
119
+ id: "connect",
120
+ label: "Conectar",
121
+ render: (nav: GuidedNav) => (
122
+ <ConnectStep nav={nav} host={resolveHost(hosts, nav.data.selectedHost)} connectPrompt={connectPrompt} />
123
+ ),
124
+ },
125
+ ];
126
+ }
127
+
128
+ /** The single middle step for the simplified plugin flow: install. */
129
+ function simpleMiddle(hosts: readonly AiHostGuide[], connectPrompt: string): GuidedStep[] {
130
+ return [
131
+ {
132
+ id: "install",
133
+ label: "Instalar",
134
+ render: (nav: GuidedNav) => (
135
+ <InstallStep nav={nav} host={resolveHost(hosts, nav.data.selectedHost)} connectPrompt={connectPrompt} />
136
+ ),
137
+ },
138
+ ];
139
+ }
140
+
141
+ /**
142
+ * Build the wizard's steps for the selected host's path: the simplified
143
+ * `install` flow when the host has a published `pluginUrl`, the per-stage flow
144
+ * when it declares `configureStages`, otherwise the full copy → configure →
145
+ * connect flow. `select` and `confirm` bookend all of them.
146
+ */
147
+ export function buildFlowSteps(opts: {
148
+ host: AiHostGuide;
149
+ hosts: readonly AiHostGuide[];
150
+ endpointUrl: string;
151
+ connectPrompt: string;
152
+ connections: readonly AiConnection[];
153
+ onRetest: () => void;
154
+ }): GuidedStep[] {
155
+ const { host, hosts, endpointUrl, connectPrompt, connections, onRetest } = opts;
156
+ const middle = host.pluginUrl
157
+ ? simpleMiddle(hosts, connectPrompt)
158
+ : manualMiddle(host, hosts, endpointUrl, connectPrompt);
159
+ const confirm: GuidedStep = {
160
+ id: "confirm",
161
+ label: "Confirmar",
162
+ render: (nav: GuidedNav) => (
163
+ <ConfirmStep nav={nav} connections={connections} hosts={hosts} onRetest={onRetest} />
164
+ ),
165
+ };
166
+ return [selectStep(hosts), ...middle, confirm];
167
+ }
@@ -0,0 +1,46 @@
1
+ import { Box } from "@12-apps/ui/mui/Box";
2
+ import { Stack } from "@12-apps/ui/mui/Stack";
3
+ import { Text } from "@12-apps/ui/typography/Text";
4
+
5
+ /** One entry of a landing's trust/feature strip. */
6
+ export interface FeatureBadgeItem {
7
+ icon: React.ReactNode;
8
+ label: string;
9
+ caption: string;
10
+ }
11
+
12
+ /**
13
+ * One item of an onboarding landing's trust/feature strip: a paper icon tile
14
+ * (primary-coloured glyph) with a two-line label + caption.
15
+ */
16
+ export function FeatureBadge({ icon, label, caption }: FeatureBadgeItem): React.JSX.Element {
17
+ return (
18
+ <Stack direction="row" spacing={1.5} alignItems="flex-start">
19
+ <Box
20
+ sx={{
21
+ width: 36,
22
+ height: 36,
23
+ borderRadius: 2,
24
+ display: "flex",
25
+ alignItems: "center",
26
+ justifyContent: "center",
27
+ bgcolor: "background.paper",
28
+ color: "primary.main",
29
+ border: 1,
30
+ borderColor: "divider",
31
+ flexShrink: 0,
32
+ }}
33
+ >
34
+ {icon}
35
+ </Box>
36
+ <Box>
37
+ <Text variant="body" size="sm" weight="bold" as="p">
38
+ {label}
39
+ </Text>
40
+ <Text variant="caption" color="secondary" as="p">
41
+ {caption}
42
+ </Text>
43
+ </Box>
44
+ </Stack>
45
+ );
46
+ }