@burdenoff/microfe-vibecontrols 2026.531.16 → 2026.601.2
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/VibeControlsRoutes.js +142 -146
- package/dist/VibeControlsRoutes.js.map +1 -1
- package/dist/components/agent-manager/controls/ModelSelector.js +1 -1
- package/dist/components/agent-manager/controls/ModelSelector.js.map +1 -1
- package/dist/components/agents/AgentCard.js +106 -85
- package/dist/components/agents/AgentCard.js.map +1 -1
- package/dist/components/agents/AgentStatusBadge.js +25 -24
- package/dist/components/agents/AgentStatusBadge.js.map +1 -1
- package/dist/components/shared/LinkToVibePanel.js +36 -26
- package/dist/components/shared/LinkToVibePanel.js.map +1 -1
- package/dist/components/shared/PluginGate.js +72 -68
- package/dist/components/shared/PluginGate.js.map +1 -1
- package/dist/components/shared/cardMotion.js +28 -0
- package/dist/components/shared/cardMotion.js.map +1 -0
- package/dist/components/templates/TemplateCard.js +43 -50
- package/dist/components/templates/TemplateCard.js.map +1 -1
- package/dist/components/tunnels/TunnelCreateWizard.js +57 -49
- package/dist/components/tunnels/TunnelCreateWizard.js.map +1 -1
- package/dist/components/vibe/VibeLinkedEntitiesPanel.js +121 -72
- package/dist/components/vibe/VibeLinkedEntitiesPanel.js.map +1 -1
- package/dist/components/vibes/VibeCard.js +65 -72
- package/dist/components/vibes/VibeCard.js.map +1 -1
- package/dist/generated/wspace-operations.js +964 -886
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/index.js +10 -10
- package/dist/pages/ActionsPage.js +297 -268
- package/dist/pages/ActionsPage.js.map +1 -1
- package/dist/pages/AgentDetailsPage.js +11 -11
- package/dist/pages/AnalyticsPage.js +2 -2
- package/dist/pages/AnalyticsPage.js.map +1 -1
- package/dist/pages/TargetsPage.js +16 -16
- package/dist/pages/VibeAuditPage.js +7 -7
- package/dist/pages/VibeDeckPage.js +180 -180
- package/dist/pages/VibeDeckPage.js.map +1 -1
- package/dist/pages/ai/AIPage.js +2 -2
- package/dist/pages/ai/AIPage.js.map +1 -1
- package/dist/pages/ai/AgentManagerPage.js +162 -127
- package/dist/pages/ai/AgentManagerPage.js.map +1 -1
- package/dist/utils/agentHealth.js +31 -0
- package/dist/utils/agentHealth.js.map +1 -0
- package/package.json +2 -2
- package/dist/pages/PluginStandalonePage.js +0 -151
- package/dist/pages/PluginStandalonePage.js.map +0 -1
|
@@ -2,11 +2,11 @@ import { QuotaExhaustedDialog as e } from "./QuotaExhaustedDialog.js";
|
|
|
2
2
|
import { useVibeNavigate as t } from "../../providers/VibeControlsProvider.js";
|
|
3
3
|
import { usePluginAvailability as n } from "../../hooks/usePluginAvailability.js";
|
|
4
4
|
import { getPluginMeta as r } from "../../constants/pluginCatalog.js";
|
|
5
|
-
import { useCallback as i,
|
|
6
|
-
import { AlertCircle as
|
|
7
|
-
import { Fragment as
|
|
5
|
+
import { useCallback as i, useEffect as a, useMemo as o, useRef as s } from "react";
|
|
6
|
+
import { AlertCircle as c, Clock as l, Download as u, Loader2 as d, Puzzle as f, RefreshCw as p, Server as m } from "lucide-react";
|
|
7
|
+
import { Fragment as h, jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
8
8
|
//#region src/components/shared/PluginGate.tsx
|
|
9
|
-
function
|
|
9
|
+
function v(e) {
|
|
10
10
|
return e.requirements && e.requirements.length > 0 ? e.requirements.map((e) => ({
|
|
11
11
|
packages: e.packages,
|
|
12
12
|
mode: e.mode ?? "all",
|
|
@@ -21,52 +21,56 @@ function g(e) {
|
|
|
21
21
|
description: e.description
|
|
22
22
|
}] : [];
|
|
23
23
|
}
|
|
24
|
-
function
|
|
25
|
-
let { agentId:
|
|
24
|
+
function y(y) {
|
|
25
|
+
let { agentId: b, compact: x = !1, onInstalled: S, children: C } = y, w = o(() => v(y), [y]), { loading: T, initialLoading: E, installed: D, installing: O, install: k, error: A, errorIsAgentState: j, quotaExhausted: M, clearQuotaExhausted: N, refetch: P } = n(o(() => {
|
|
26
26
|
let e = /* @__PURE__ */ new Set();
|
|
27
|
-
for (let t of
|
|
27
|
+
for (let t of w) for (let n of t.packages) e.add(n);
|
|
28
28
|
return [...e];
|
|
29
|
-
}, [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
}, [w]), b), F = t(), I = o(() => w.map((e) => e.mode === "any" ? e.packages.some((e) => !!D[e]) : e.packages.every((e) => !!D[e])), [w, D]), L = w.length > 0 && I.every(Boolean), R = s(L);
|
|
30
|
+
a(() => {
|
|
31
|
+
L && !R.current && S?.(), R.current = L;
|
|
32
|
+
}, [L, S]);
|
|
33
|
+
let z = i(async (e) => {
|
|
34
|
+
await k(e);
|
|
35
|
+
}, [k]);
|
|
36
|
+
return b ? L ? /* @__PURE__ */ g(h, { children: C }) : E ? /* @__PURE__ */ g("div", {
|
|
37
|
+
className: `rounded-lg border border-border-default bg-bg-surface ${x ? "p-4" : "p-6"}`,
|
|
34
38
|
"data-testid": "plugin-gate-initial-loading",
|
|
35
|
-
children: /* @__PURE__ */
|
|
39
|
+
children: /* @__PURE__ */ _("div", {
|
|
36
40
|
className: "flex items-center gap-2 text-sm text-text-secondary",
|
|
37
|
-
children: [/* @__PURE__ */
|
|
41
|
+
children: [/* @__PURE__ */ g(d, { className: "size-4 animate-spin" }), "Checking plugin availability on the agent…"]
|
|
38
42
|
})
|
|
39
|
-
}) :
|
|
40
|
-
className: `rounded-lg border border-border-default bg-bg-surface ${
|
|
41
|
-
children: /* @__PURE__ */
|
|
43
|
+
}) : j ? /* @__PURE__ */ g("div", {
|
|
44
|
+
className: `rounded-lg border border-border-default bg-bg-surface ${x ? "p-4" : "p-6"}`,
|
|
45
|
+
children: /* @__PURE__ */ _("div", {
|
|
42
46
|
className: "flex items-start gap-3",
|
|
43
|
-
children: [/* @__PURE__ */
|
|
47
|
+
children: [/* @__PURE__ */ g(l, { className: "size-5 text-status-warning-text shrink-0 mt-0.5" }), /* @__PURE__ */ _("div", {
|
|
44
48
|
className: "flex-1 min-w-0 space-y-2",
|
|
45
49
|
children: [
|
|
46
|
-
/* @__PURE__ */
|
|
50
|
+
/* @__PURE__ */ g("p", {
|
|
47
51
|
className: "text-sm font-semibold text-text-primary",
|
|
48
52
|
children: "Agent is not ready yet"
|
|
49
53
|
}),
|
|
50
|
-
/* @__PURE__ */
|
|
54
|
+
/* @__PURE__ */ g("p", {
|
|
51
55
|
className: "text-sm text-text-secondary",
|
|
52
56
|
children: "Your agent is still starting up or waiting for gateway credentials. Plugin install is only available once the agent reaches the ready state."
|
|
53
57
|
}),
|
|
54
|
-
/* @__PURE__ */
|
|
58
|
+
/* @__PURE__ */ g("p", {
|
|
55
59
|
className: "text-xs font-mono text-text-placeholder break-all",
|
|
56
|
-
children:
|
|
60
|
+
children: A
|
|
57
61
|
}),
|
|
58
|
-
/* @__PURE__ */
|
|
62
|
+
/* @__PURE__ */ _("div", {
|
|
59
63
|
className: "flex flex-wrap items-center gap-2 pt-1",
|
|
60
|
-
children: [/* @__PURE__ */
|
|
64
|
+
children: [/* @__PURE__ */ _("button", {
|
|
61
65
|
type: "button",
|
|
62
|
-
onClick: () => void
|
|
63
|
-
disabled:
|
|
66
|
+
onClick: () => void P(),
|
|
67
|
+
disabled: T,
|
|
64
68
|
className: "inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50",
|
|
65
69
|
"data-testid": "plugin-gate-retry",
|
|
66
|
-
children: [/* @__PURE__ */
|
|
67
|
-
}), /* @__PURE__ */
|
|
70
|
+
children: [/* @__PURE__ */ g(p, { className: `size-3.5 ${T ? "animate-spin" : ""}` }), "Retry"]
|
|
71
|
+
}), /* @__PURE__ */ g("button", {
|
|
68
72
|
type: "button",
|
|
69
|
-
onClick: () =>
|
|
73
|
+
onClick: () => F("/agents"),
|
|
70
74
|
className: "rounded-md border border-border-default px-3 py-1.5 text-xs font-medium text-text-secondary hover:text-text-primary hover:border-border-strong transition-colors",
|
|
71
75
|
children: "Manage agents"
|
|
72
76
|
})]
|
|
@@ -74,104 +78,104 @@ function _(_) {
|
|
|
74
78
|
]
|
|
75
79
|
})]
|
|
76
80
|
})
|
|
77
|
-
}) : /* @__PURE__ */
|
|
81
|
+
}) : /* @__PURE__ */ _("div", {
|
|
78
82
|
className: "space-y-3",
|
|
79
|
-
children: [
|
|
80
|
-
if (
|
|
81
|
-
let n = (e.mode, e.packages.filter((e) => !
|
|
82
|
-
return /* @__PURE__ */
|
|
83
|
-
className: `rounded-lg border border-border-default bg-bg-surface ${
|
|
84
|
-
children: /* @__PURE__ */
|
|
83
|
+
children: [w.map((e, t) => {
|
|
84
|
+
if (I[t]) return null;
|
|
85
|
+
let n = (e.mode, e.packages.filter((e) => !D[e])), i = e.mode === "any" ? "Install a plugin to continue" : e.packages.length === 1 ? "Plugin required" : "Plugins required", a = e.mode === "any" ? "Pick any one of the plugins below. Once installed, this surface will render automatically." : e.packages.length === 1 ? "This surface needs the following agent plugin installed." : "This surface needs the following agent plugins installed.";
|
|
86
|
+
return /* @__PURE__ */ g("div", {
|
|
87
|
+
className: `rounded-lg border border-border-default bg-bg-surface ${x ? "p-4" : "p-6"}`,
|
|
88
|
+
children: /* @__PURE__ */ _("div", {
|
|
85
89
|
className: "flex items-start gap-3",
|
|
86
|
-
children: [/* @__PURE__ */
|
|
90
|
+
children: [/* @__PURE__ */ g(f, { className: "size-5 text-action-primary-bg shrink-0 mt-0.5" }), /* @__PURE__ */ _("div", {
|
|
87
91
|
className: "flex-1 min-w-0",
|
|
88
92
|
children: [
|
|
89
|
-
e.eyebrow && /* @__PURE__ */
|
|
93
|
+
e.eyebrow && /* @__PURE__ */ g("p", {
|
|
90
94
|
className: "text-xs uppercase tracking-wide text-text-secondary mb-1",
|
|
91
95
|
children: e.eyebrow
|
|
92
96
|
}),
|
|
93
|
-
/* @__PURE__ */
|
|
97
|
+
/* @__PURE__ */ g("p", {
|
|
94
98
|
className: "text-sm font-semibold text-text-primary",
|
|
95
99
|
children: e.title ?? i
|
|
96
100
|
}),
|
|
97
|
-
/* @__PURE__ */
|
|
101
|
+
/* @__PURE__ */ g("p", {
|
|
98
102
|
className: "mt-1 text-sm text-text-secondary",
|
|
99
103
|
children: e.description ?? a
|
|
100
104
|
}),
|
|
101
|
-
|
|
105
|
+
A && /* @__PURE__ */ _("div", {
|
|
102
106
|
role: "alert",
|
|
103
107
|
className: "mt-3 flex items-start gap-2 rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-xs text-status-error-text",
|
|
104
|
-
children: [/* @__PURE__ */
|
|
108
|
+
children: [/* @__PURE__ */ g(c, { className: "size-3.5 mt-0.5 shrink-0" }), /* @__PURE__ */ g("span", { children: A })]
|
|
105
109
|
}),
|
|
106
|
-
/* @__PURE__ */
|
|
110
|
+
/* @__PURE__ */ g("ul", {
|
|
107
111
|
className: "mt-4 space-y-2",
|
|
108
112
|
children: n.map((e) => {
|
|
109
|
-
let t = r(e), n =
|
|
110
|
-
return /* @__PURE__ */
|
|
113
|
+
let t = r(e), n = O === e, i = O !== null;
|
|
114
|
+
return /* @__PURE__ */ _("li", {
|
|
111
115
|
className: "flex items-center justify-between gap-3 rounded-md border border-border-default bg-bg-sunken px-3 py-2",
|
|
112
|
-
children: [/* @__PURE__ */
|
|
116
|
+
children: [/* @__PURE__ */ _("div", {
|
|
113
117
|
className: "min-w-0",
|
|
114
118
|
children: [
|
|
115
|
-
/* @__PURE__ */
|
|
119
|
+
/* @__PURE__ */ g("p", {
|
|
116
120
|
className: "text-sm font-medium text-text-primary truncate",
|
|
117
121
|
children: t.displayName
|
|
118
122
|
}),
|
|
119
|
-
/* @__PURE__ */
|
|
123
|
+
/* @__PURE__ */ g("p", {
|
|
120
124
|
className: "text-xs text-text-secondary truncate",
|
|
121
125
|
children: t.description
|
|
122
126
|
}),
|
|
123
|
-
/* @__PURE__ */
|
|
127
|
+
/* @__PURE__ */ g("p", {
|
|
124
128
|
className: "text-[10px] font-mono text-text-placeholder truncate",
|
|
125
129
|
title: e,
|
|
126
130
|
children: e
|
|
127
131
|
})
|
|
128
132
|
]
|
|
129
|
-
}), /* @__PURE__ */
|
|
133
|
+
}), /* @__PURE__ */ g("button", {
|
|
130
134
|
type: "button",
|
|
131
|
-
onClick: () => void
|
|
132
|
-
disabled: i ||
|
|
135
|
+
onClick: () => void z(e),
|
|
136
|
+
disabled: i || T,
|
|
133
137
|
className: "inline-flex items-center gap-1.5 whitespace-nowrap rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
|
|
134
138
|
"data-testid": `plugin-install-${e}`,
|
|
135
|
-
children: n ? /* @__PURE__ */ h
|
|
139
|
+
children: n ? /* @__PURE__ */ _(h, { children: [/* @__PURE__ */ g(d, { className: "size-3.5 animate-spin" }), "Installing…"] }) : /* @__PURE__ */ _(h, { children: [/* @__PURE__ */ g(u, { className: "size-3.5" }), "Install"] })
|
|
136
140
|
})]
|
|
137
141
|
}, e);
|
|
138
142
|
})
|
|
139
143
|
}),
|
|
140
|
-
|
|
144
|
+
T && n.length === 0 && /* @__PURE__ */ _("div", {
|
|
141
145
|
className: "mt-4 flex items-center gap-2 text-xs text-text-secondary",
|
|
142
|
-
children: [/* @__PURE__ */
|
|
146
|
+
children: [/* @__PURE__ */ g(d, { className: "size-3.5 animate-spin" }), "Checking plugins…"]
|
|
143
147
|
})
|
|
144
148
|
]
|
|
145
149
|
})]
|
|
146
150
|
})
|
|
147
151
|
}, t);
|
|
148
|
-
}), /* @__PURE__ */
|
|
149
|
-
isOpen:
|
|
150
|
-
onClose:
|
|
152
|
+
}), /* @__PURE__ */ g(e, {
|
|
153
|
+
isOpen: M,
|
|
154
|
+
onClose: N,
|
|
151
155
|
onConfirm: () => {
|
|
152
|
-
|
|
156
|
+
N(), F("/billing/plans");
|
|
153
157
|
},
|
|
154
158
|
title: "Plugin quota reached",
|
|
155
159
|
message: "You've used all plugin install quota on your current plan. Upgrade to install more plugins."
|
|
156
160
|
})]
|
|
157
|
-
}) : /* @__PURE__ */
|
|
158
|
-
className: `rounded-lg border border-border-default bg-bg-surface ${
|
|
159
|
-
children: /* @__PURE__ */
|
|
161
|
+
}) : /* @__PURE__ */ g("div", {
|
|
162
|
+
className: `rounded-lg border border-border-default bg-bg-surface ${x ? "p-4" : "p-6"}`,
|
|
163
|
+
children: /* @__PURE__ */ _("div", {
|
|
160
164
|
className: "flex items-start gap-3",
|
|
161
|
-
children: [/* @__PURE__ */ m
|
|
165
|
+
children: [/* @__PURE__ */ g(m, { className: "size-5 text-text-secondary shrink-0 mt-0.5" }), /* @__PURE__ */ _("div", {
|
|
162
166
|
className: "flex-1 space-y-2",
|
|
163
167
|
children: [
|
|
164
|
-
/* @__PURE__ */
|
|
168
|
+
/* @__PURE__ */ g("p", {
|
|
165
169
|
className: "text-sm font-semibold text-text-primary",
|
|
166
170
|
children: "No active agent"
|
|
167
171
|
}),
|
|
168
|
-
/* @__PURE__ */
|
|
172
|
+
/* @__PURE__ */ g("p", {
|
|
169
173
|
className: "text-sm text-text-secondary",
|
|
170
174
|
children: "This surface needs an active vibecontrols agent with a tunnel URL and API key."
|
|
171
175
|
}),
|
|
172
|
-
/* @__PURE__ */
|
|
176
|
+
/* @__PURE__ */ g("button", {
|
|
173
177
|
type: "button",
|
|
174
|
-
onClick: () =>
|
|
178
|
+
onClick: () => F("/agents"),
|
|
175
179
|
className: "px-3 py-1.5 text-xs font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors",
|
|
176
180
|
children: "Go to Agents"
|
|
177
181
|
})
|
|
@@ -181,6 +185,6 @@ function _(_) {
|
|
|
181
185
|
});
|
|
182
186
|
}
|
|
183
187
|
//#endregion
|
|
184
|
-
export {
|
|
188
|
+
export { y as PluginGate };
|
|
185
189
|
|
|
186
190
|
//# sourceMappingURL=PluginGate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PluginGate.js","names":[],"sources":["../../../src/components/shared/PluginGate.tsx"],"sourcesContent":["/**\n * PluginGate — gates a UI surface on one or more vibecontrols-agent plugins\n * being installed. If missing, renders a card listing each required plugin\n * with an Install button. Once satisfied, renders children.\n *\n * Supports two modes:\n * - Single-group (backwards compat): `required` + `mode` + `title/description`.\n * - Multi-group: `requirements={[{packages, mode, ...}, ...]}` — one fetch\n * for all packages across all groups, unified loading state, stacked\n * install cards for any unsatisfied group. Prevents the \"flicker cascade\"\n * you get when nesting PluginGates (outer resolves, inner starts its own\n * initial-loading spinner, etc.).\n */\n\nimport { useCallback, useMemo, type ReactNode } from 'react';\nimport { Puzzle, Download, Loader2, AlertCircle, Server, RefreshCw, Clock } from 'lucide-react';\nimport { usePluginAvailability } from '@/hooks/usePluginAvailability';\nimport { getPluginMeta } from '@/constants/pluginCatalog';\nimport { QuotaExhaustedDialog } from './QuotaExhaustedDialog';\nimport { useVibeNavigate } from '@/providers/VibeControlsProvider';\n\nexport interface PluginRequirement {\n /** Package names that satisfy this requirement. */\n packages: readonly string[];\n /** 'all' (default) — every package must be installed. 'any' — at least one. */\n mode?: 'all' | 'any';\n /** Optional per-group eyebrow, title, description (fall back to defaults). */\n eyebrow?: string;\n title?: string;\n description?: string;\n}\n\nexport interface PluginGateProps {\n /** Active agent id. When null, gate shows an \"activate an agent\" prompt. */\n agentId: string | null;\n\n // ── Single-group API (backwards compat) ────────────────────────────────\n /** Package names required for the wrapped surface to render. */\n required?: readonly string[];\n /** 'all' (default) — every package must be installed. 'any' — at least one. */\n mode?: 'all' | 'any';\n /** Card heading. */\n title?: string;\n /** Card subheading / instruction line. */\n description?: string;\n /** Optional eyebrow label above the title (e.g. \"Plugin required\"). */\n eyebrow?: string;\n\n // ── Multi-group API (preferred when gating multiple requirements) ──────\n /** Multiple requirement groups — fetched + evaluated in one pass. */\n requirements?: readonly PluginRequirement[];\n\n /** Pass to render a compact version (smaller vertical padding). */\n compact?: boolean;\n /** Optional callback fired after install succeeds and gate resolves. */\n onInstalled?: () => void;\n children: ReactNode;\n}\n\ninterface ResolvedRequirement {\n packages: readonly string[];\n mode: 'all' | 'any';\n eyebrow?: string;\n title?: string;\n description?: string;\n}\n\nfunction resolveRequirements(props: PluginGateProps): ResolvedRequirement[] {\n if (props.requirements && props.requirements.length > 0) {\n return props.requirements.map((r) => ({\n packages: r.packages,\n mode: r.mode ?? 'all',\n eyebrow: r.eyebrow,\n title: r.title,\n description: r.description,\n }));\n }\n if (props.required && props.required.length > 0) {\n return [\n {\n packages: props.required,\n mode: props.mode ?? 'all',\n eyebrow: props.eyebrow,\n title: props.title,\n description: props.description,\n },\n ];\n }\n return [];\n}\n\nexport function PluginGate(props: PluginGateProps) {\n const { agentId, compact = false, onInstalled, children } = props;\n // Plugins are agent-wide (BOFF-2612). `activeProfile` no longer\n // participates in plugin availability — the install state is the\n // same for every profile on the agent.\n const resolved = useMemo(() => resolveRequirements(props), [props]);\n\n const allPackages = useMemo(() => {\n const set = new Set<string>();\n for (const req of resolved) {\n for (const pkg of req.packages) set.add(pkg);\n }\n return [...set];\n }, [resolved]);\n\n const {\n loading,\n initialLoading,\n installed,\n installing,\n install,\n error,\n errorIsAgentState,\n quotaExhausted,\n clearQuotaExhausted,\n refetch,\n } = usePluginAvailability(allPackages, agentId);\n\n const navigate = useVibeNavigate();\n\n // Per-requirement satisfaction.\n const requirementSatisfied = useMemo(\n () =>\n resolved.map((req) => {\n if (req.mode === 'any') return req.packages.some((p) => !!installed[p]);\n return req.packages.every((p) => !!installed[p]);\n }),\n [resolved, installed]\n );\n\n const allSatisfied = resolved.length > 0 && requirementSatisfied.every(Boolean);\n\n const handleInstall = useCallback(\n async (pkg: string) => {\n const ok = await install(pkg);\n if (ok) onInstalled?.();\n },\n [install, onInstalled]\n );\n\n // No agent available — universal precondition.\n if (!agentId) {\n return (\n <div\n className={`rounded-lg border border-border-default bg-bg-surface ${compact ? 'p-4' : 'p-6'}`}\n >\n <div className=\"flex items-start gap-3\">\n <Server className=\"size-5 text-text-secondary shrink-0 mt-0.5\" />\n <div className=\"flex-1 space-y-2\">\n <p className=\"text-sm font-semibold text-text-primary\">No active agent</p>\n <p className=\"text-sm text-text-secondary\">\n This surface needs an active vibecontrols agent with a tunnel URL and API key.\n </p>\n <button\n type=\"button\"\n onClick={() => navigate('/agents')}\n className=\"px-3 py-1.5 text-xs font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n >\n Go to Agents\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n if (allSatisfied) {\n return <>{children}</>;\n }\n\n // First-fetch loading state — one spinner for ALL requirements. Prevents\n // the cascade you get with nested PluginGates (outer resolves -> inner\n // starts a fresh initialLoading cycle). After the first fetch settles,\n // subsequent refetches (focus, install, retry) fall through to the\n // install card so context isn't replaced by a spinner.\n if (initialLoading) {\n return (\n <div\n className={`rounded-lg border border-border-default bg-bg-surface ${compact ? 'p-4' : 'p-6'}`}\n data-testid=\"plugin-gate-initial-loading\"\n >\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"size-4 animate-spin\" />\n Checking plugin availability on the agent…\n </div>\n </div>\n );\n }\n\n // Agent is not ready (initializing / awaiting-config / unreachable).\n // The install button would just fail with the same message — instead,\n // render a dedicated waiting state with a retry button and an escape\n // hatch to the Agents page.\n if (errorIsAgentState) {\n return (\n <div\n className={`rounded-lg border border-border-default bg-bg-surface ${compact ? 'p-4' : 'p-6'}`}\n >\n <div className=\"flex items-start gap-3\">\n <Clock className=\"size-5 text-status-warning-text shrink-0 mt-0.5\" />\n <div className=\"flex-1 min-w-0 space-y-2\">\n <p className=\"text-sm font-semibold text-text-primary\">Agent is not ready yet</p>\n <p className=\"text-sm text-text-secondary\">\n Your agent is still starting up or waiting for gateway credentials. Plugin install is\n only available once the agent reaches the ready state.\n </p>\n <p className=\"text-xs font-mono text-text-placeholder break-all\">{error}</p>\n <div className=\"flex flex-wrap items-center gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => void refetch()}\n disabled={loading}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n data-testid=\"plugin-gate-retry\"\n >\n <RefreshCw className={`size-3.5 ${loading ? 'animate-spin' : ''}`} />\n Retry\n </button>\n <button\n type=\"button\"\n onClick={() => navigate('/agents')}\n className=\"rounded-md border border-border-default px-3 py-1.5 text-xs font-medium text-text-secondary hover:text-text-primary hover:border-border-strong transition-colors\"\n >\n Manage agents\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n }\n\n // Stack one install card per unsatisfied requirement. The user sees every\n // outstanding dependency at once instead of resolving them one-by-one with\n // a re-render between each.\n return (\n <div className=\"space-y-3\">\n {resolved.map((req, idx) => {\n if (requirementSatisfied[idx]) return null;\n const packagesToOffer =\n req.mode === 'any'\n ? req.packages.filter((p) => !installed[p])\n : req.packages.filter((p) => !installed[p]);\n const defaultTitle =\n req.mode === 'any'\n ? 'Install a plugin to continue'\n : req.packages.length === 1\n ? 'Plugin required'\n : 'Plugins required';\n const defaultDescription =\n req.mode === 'any'\n ? 'Pick any one of the plugins below. Once installed, this surface will render automatically.'\n : req.packages.length === 1\n ? 'This surface needs the following agent plugin installed.'\n : 'This surface needs the following agent plugins installed.';\n\n return (\n <div\n key={idx}\n className={`rounded-lg border border-border-default bg-bg-surface ${compact ? 'p-4' : 'p-6'}`}\n >\n <div className=\"flex items-start gap-3\">\n <Puzzle className=\"size-5 text-action-primary-bg shrink-0 mt-0.5\" />\n <div className=\"flex-1 min-w-0\">\n {req.eyebrow && (\n <p className=\"text-xs uppercase tracking-wide text-text-secondary mb-1\">\n {req.eyebrow}\n </p>\n )}\n <p className=\"text-sm font-semibold text-text-primary\">\n {req.title ?? defaultTitle}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {req.description ?? defaultDescription}\n </p>\n\n {error && (\n <div\n role=\"alert\"\n className=\"mt-3 flex items-start gap-2 rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-xs text-status-error-text\"\n >\n <AlertCircle className=\"size-3.5 mt-0.5 shrink-0\" />\n <span>{error}</span>\n </div>\n )}\n\n <ul className=\"mt-4 space-y-2\">\n {packagesToOffer.map((pkg) => {\n const meta = getPluginMeta(pkg);\n const isInstalling = installing === pkg;\n const isBusy = installing !== null;\n return (\n <li\n key={pkg}\n className=\"flex items-center justify-between gap-3 rounded-md border border-border-default bg-bg-sunken px-3 py-2\"\n >\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary truncate\">\n {meta.displayName}\n </p>\n <p className=\"text-xs text-text-secondary truncate\">{meta.description}</p>\n <p\n className=\"text-[10px] font-mono text-text-placeholder truncate\"\n title={pkg}\n >\n {pkg}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => void handleInstall(pkg)}\n disabled={isBusy || loading}\n className=\"inline-flex items-center gap-1.5 whitespace-nowrap rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n data-testid={`plugin-install-${pkg}`}\n >\n {isInstalling ? (\n <>\n <Loader2 className=\"size-3.5 animate-spin\" />\n Installing…\n </>\n ) : (\n <>\n <Download className=\"size-3.5\" />\n Install\n </>\n )}\n </button>\n </li>\n );\n })}\n </ul>\n\n {loading && packagesToOffer.length === 0 && (\n <div className=\"mt-4 flex items-center gap-2 text-xs text-text-secondary\">\n <Loader2 className=\"size-3.5 animate-spin\" />\n Checking plugins…\n </div>\n )}\n </div>\n </div>\n </div>\n );\n })}\n\n <QuotaExhaustedDialog\n isOpen={quotaExhausted}\n onClose={clearQuotaExhausted}\n onConfirm={() => {\n clearQuotaExhausted();\n navigate('/billing/plans');\n }}\n title=\"Plugin quota reached\"\n message=\"You've used all plugin install quota on your current plan. Upgrade to install more plugins.\"\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PluginGate.js","names":[],"sources":["../../../src/components/shared/PluginGate.tsx"],"sourcesContent":["/**\n * PluginGate — gates a UI surface on one or more vibecontrols-agent plugins\n * being installed. If missing, renders a card listing each required plugin\n * with an Install button. Once satisfied, renders children.\n *\n * Supports two modes:\n * - Single-group (backwards compat): `required` + `mode` + `title/description`.\n * - Multi-group: `requirements={[{packages, mode, ...}, ...]}` — one fetch\n * for all packages across all groups, unified loading state, stacked\n * install cards for any unsatisfied group. Prevents the \"flicker cascade\"\n * you get when nesting PluginGates (outer resolves, inner starts its own\n * initial-loading spinner, etc.).\n */\n\nimport { useCallback, useEffect, useMemo, useRef, type ReactNode } from 'react';\nimport { Puzzle, Download, Loader2, AlertCircle, Server, RefreshCw, Clock } from 'lucide-react';\nimport { usePluginAvailability } from '@/hooks/usePluginAvailability';\nimport { getPluginMeta } from '@/constants/pluginCatalog';\nimport { QuotaExhaustedDialog } from './QuotaExhaustedDialog';\nimport { useVibeNavigate } from '@/providers/VibeControlsProvider';\n\nexport interface PluginRequirement {\n /** Package names that satisfy this requirement. */\n packages: readonly string[];\n /** 'all' (default) — every package must be installed. 'any' — at least one. */\n mode?: 'all' | 'any';\n /** Optional per-group eyebrow, title, description (fall back to defaults). */\n eyebrow?: string;\n title?: string;\n description?: string;\n}\n\nexport interface PluginGateProps {\n /** Active agent id. When null, gate shows an \"activate an agent\" prompt. */\n agentId: string | null;\n\n // ── Single-group API (backwards compat) ────────────────────────────────\n /** Package names required for the wrapped surface to render. */\n required?: readonly string[];\n /** 'all' (default) — every package must be installed. 'any' — at least one. */\n mode?: 'all' | 'any';\n /** Card heading. */\n title?: string;\n /** Card subheading / instruction line. */\n description?: string;\n /** Optional eyebrow label above the title (e.g. \"Plugin required\"). */\n eyebrow?: string;\n\n // ── Multi-group API (preferred when gating multiple requirements) ──────\n /** Multiple requirement groups — fetched + evaluated in one pass. */\n requirements?: readonly PluginRequirement[];\n\n /** Pass to render a compact version (smaller vertical padding). */\n compact?: boolean;\n /** Optional callback fired after install succeeds and gate resolves. */\n onInstalled?: () => void;\n children: ReactNode;\n}\n\ninterface ResolvedRequirement {\n packages: readonly string[];\n mode: 'all' | 'any';\n eyebrow?: string;\n title?: string;\n description?: string;\n}\n\nfunction resolveRequirements(props: PluginGateProps): ResolvedRequirement[] {\n if (props.requirements && props.requirements.length > 0) {\n return props.requirements.map((r) => ({\n packages: r.packages,\n mode: r.mode ?? 'all',\n eyebrow: r.eyebrow,\n title: r.title,\n description: r.description,\n }));\n }\n if (props.required && props.required.length > 0) {\n return [\n {\n packages: props.required,\n mode: props.mode ?? 'all',\n eyebrow: props.eyebrow,\n title: props.title,\n description: props.description,\n },\n ];\n }\n return [];\n}\n\nexport function PluginGate(props: PluginGateProps) {\n const { agentId, compact = false, onInstalled, children } = props;\n // Plugins are agent-wide (BOFF-2612). `activeProfile` no longer\n // participates in plugin availability — the install state is the\n // same for every profile on the agent.\n const resolved = useMemo(() => resolveRequirements(props), [props]);\n\n const allPackages = useMemo(() => {\n const set = new Set<string>();\n for (const req of resolved) {\n for (const pkg of req.packages) set.add(pkg);\n }\n return [...set];\n }, [resolved]);\n\n const {\n loading,\n initialLoading,\n installed,\n installing,\n install,\n error,\n errorIsAgentState,\n quotaExhausted,\n clearQuotaExhausted,\n refetch,\n } = usePluginAvailability(allPackages, agentId);\n\n const navigate = useVibeNavigate();\n\n // Per-requirement satisfaction.\n const requirementSatisfied = useMemo(\n () =>\n resolved.map((req) => {\n if (req.mode === 'any') return req.packages.some((p) => !!installed[p]);\n return req.packages.every((p) => !!installed[p]);\n }),\n [resolved, installed]\n );\n\n const allSatisfied = resolved.length > 0 && requirementSatisfied.every(Boolean);\n\n // Fire `onInstalled` exactly once, on the unsatisfied → satisfied transition.\n // A freshly-installed provider lands asynchronously (the install mutation\n // returns before the agent finishes the npm fetch), so the reliable signal\n // is the gate's own requirement state flipping to satisfied — driven by the\n // delayed refetch + plugin subscription in usePluginAvailability — not the\n // install button's optimistic success. The ref guards against re-firing on\n // every subsequent re-render where `allSatisfied` stays true, and against the\n // double-navigate the old immediate fire could cause.\n const wasSatisfiedRef = useRef(allSatisfied);\n useEffect(() => {\n if (allSatisfied && !wasSatisfiedRef.current) {\n onInstalled?.();\n }\n wasSatisfiedRef.current = allSatisfied;\n }, [allSatisfied, onInstalled]);\n\n const handleInstall = useCallback(\n async (pkg: string) => {\n // Kick off the install; `onInstalled` is now fired by the satisfied\n // transition effect above once the gate actually resolves, so we don't\n // call it here (which previously fired before the provider was live).\n await install(pkg);\n },\n [install]\n );\n\n // No agent available — universal precondition.\n if (!agentId) {\n return (\n <div\n className={`rounded-lg border border-border-default bg-bg-surface ${compact ? 'p-4' : 'p-6'}`}\n >\n <div className=\"flex items-start gap-3\">\n <Server className=\"size-5 text-text-secondary shrink-0 mt-0.5\" />\n <div className=\"flex-1 space-y-2\">\n <p className=\"text-sm font-semibold text-text-primary\">No active agent</p>\n <p className=\"text-sm text-text-secondary\">\n This surface needs an active vibecontrols agent with a tunnel URL and API key.\n </p>\n <button\n type=\"button\"\n onClick={() => navigate('/agents')}\n className=\"px-3 py-1.5 text-xs font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n >\n Go to Agents\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n if (allSatisfied) {\n return <>{children}</>;\n }\n\n // First-fetch loading state — one spinner for ALL requirements. Prevents\n // the cascade you get with nested PluginGates (outer resolves -> inner\n // starts a fresh initialLoading cycle). After the first fetch settles,\n // subsequent refetches (focus, install, retry) fall through to the\n // install card so context isn't replaced by a spinner.\n if (initialLoading) {\n return (\n <div\n className={`rounded-lg border border-border-default bg-bg-surface ${compact ? 'p-4' : 'p-6'}`}\n data-testid=\"plugin-gate-initial-loading\"\n >\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"size-4 animate-spin\" />\n Checking plugin availability on the agent…\n </div>\n </div>\n );\n }\n\n // Agent is not ready (initializing / awaiting-config / unreachable).\n // The install button would just fail with the same message — instead,\n // render a dedicated waiting state with a retry button and an escape\n // hatch to the Agents page.\n if (errorIsAgentState) {\n return (\n <div\n className={`rounded-lg border border-border-default bg-bg-surface ${compact ? 'p-4' : 'p-6'}`}\n >\n <div className=\"flex items-start gap-3\">\n <Clock className=\"size-5 text-status-warning-text shrink-0 mt-0.5\" />\n <div className=\"flex-1 min-w-0 space-y-2\">\n <p className=\"text-sm font-semibold text-text-primary\">Agent is not ready yet</p>\n <p className=\"text-sm text-text-secondary\">\n Your agent is still starting up or waiting for gateway credentials. Plugin install is\n only available once the agent reaches the ready state.\n </p>\n <p className=\"text-xs font-mono text-text-placeholder break-all\">{error}</p>\n <div className=\"flex flex-wrap items-center gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => void refetch()}\n disabled={loading}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n data-testid=\"plugin-gate-retry\"\n >\n <RefreshCw className={`size-3.5 ${loading ? 'animate-spin' : ''}`} />\n Retry\n </button>\n <button\n type=\"button\"\n onClick={() => navigate('/agents')}\n className=\"rounded-md border border-border-default px-3 py-1.5 text-xs font-medium text-text-secondary hover:text-text-primary hover:border-border-strong transition-colors\"\n >\n Manage agents\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n }\n\n // Stack one install card per unsatisfied requirement. The user sees every\n // outstanding dependency at once instead of resolving them one-by-one with\n // a re-render between each.\n return (\n <div className=\"space-y-3\">\n {resolved.map((req, idx) => {\n if (requirementSatisfied[idx]) return null;\n const packagesToOffer =\n req.mode === 'any'\n ? req.packages.filter((p) => !installed[p])\n : req.packages.filter((p) => !installed[p]);\n const defaultTitle =\n req.mode === 'any'\n ? 'Install a plugin to continue'\n : req.packages.length === 1\n ? 'Plugin required'\n : 'Plugins required';\n const defaultDescription =\n req.mode === 'any'\n ? 'Pick any one of the plugins below. Once installed, this surface will render automatically.'\n : req.packages.length === 1\n ? 'This surface needs the following agent plugin installed.'\n : 'This surface needs the following agent plugins installed.';\n\n return (\n <div\n key={idx}\n className={`rounded-lg border border-border-default bg-bg-surface ${compact ? 'p-4' : 'p-6'}`}\n >\n <div className=\"flex items-start gap-3\">\n <Puzzle className=\"size-5 text-action-primary-bg shrink-0 mt-0.5\" />\n <div className=\"flex-1 min-w-0\">\n {req.eyebrow && (\n <p className=\"text-xs uppercase tracking-wide text-text-secondary mb-1\">\n {req.eyebrow}\n </p>\n )}\n <p className=\"text-sm font-semibold text-text-primary\">\n {req.title ?? defaultTitle}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {req.description ?? defaultDescription}\n </p>\n\n {error && (\n <div\n role=\"alert\"\n className=\"mt-3 flex items-start gap-2 rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-xs text-status-error-text\"\n >\n <AlertCircle className=\"size-3.5 mt-0.5 shrink-0\" />\n <span>{error}</span>\n </div>\n )}\n\n <ul className=\"mt-4 space-y-2\">\n {packagesToOffer.map((pkg) => {\n const meta = getPluginMeta(pkg);\n const isInstalling = installing === pkg;\n const isBusy = installing !== null;\n return (\n <li\n key={pkg}\n className=\"flex items-center justify-between gap-3 rounded-md border border-border-default bg-bg-sunken px-3 py-2\"\n >\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary truncate\">\n {meta.displayName}\n </p>\n <p className=\"text-xs text-text-secondary truncate\">{meta.description}</p>\n <p\n className=\"text-[10px] font-mono text-text-placeholder truncate\"\n title={pkg}\n >\n {pkg}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => void handleInstall(pkg)}\n disabled={isBusy || loading}\n className=\"inline-flex items-center gap-1.5 whitespace-nowrap rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n data-testid={`plugin-install-${pkg}`}\n >\n {isInstalling ? (\n <>\n <Loader2 className=\"size-3.5 animate-spin\" />\n Installing…\n </>\n ) : (\n <>\n <Download className=\"size-3.5\" />\n Install\n </>\n )}\n </button>\n </li>\n );\n })}\n </ul>\n\n {loading && packagesToOffer.length === 0 && (\n <div className=\"mt-4 flex items-center gap-2 text-xs text-text-secondary\">\n <Loader2 className=\"size-3.5 animate-spin\" />\n Checking plugins…\n </div>\n )}\n </div>\n </div>\n </div>\n );\n })}\n\n <QuotaExhaustedDialog\n isOpen={quotaExhausted}\n onClose={clearQuotaExhausted}\n onConfirm={() => {\n clearQuotaExhausted();\n navigate('/billing/plans');\n }}\n title=\"Plugin quota reached\"\n message=\"You've used all plugin install quota on your current plan. Upgrade to install more plugins.\"\n />\n </div>\n );\n}\n"],"mappings":";;;;;;;;AAmEA,SAAS,EAAoB,GAA+C;AAqB1E,QApBI,EAAM,gBAAgB,EAAM,aAAa,SAAS,IAC7C,EAAM,aAAa,KAAK,OAAO;EACpC,UAAU,EAAE;EACZ,MAAM,EAAE,QAAQ;EAChB,SAAS,EAAE;EACX,OAAO,EAAE;EACT,aAAa,EAAE;EAChB,EAAE,GAED,EAAM,YAAY,EAAM,SAAS,SAAS,IACrC,CACL;EACE,UAAU,EAAM;EAChB,MAAM,EAAM,QAAQ;EACpB,SAAS,EAAM;EACf,OAAO,EAAM;EACb,aAAa,EAAM;EACpB,CACF,GAEI,EAAE;;AAGX,SAAgB,EAAW,GAAwB;CACjD,IAAM,EAAE,YAAS,aAAU,IAAO,gBAAa,gBAAa,GAItD,IAAW,QAAc,EAAoB,EAAM,EAAE,CAAC,EAAM,CAAC,EAU7D,EACJ,YACA,mBACA,cACA,eACA,YACA,UACA,sBACA,mBACA,wBACA,eACE,EAnBgB,QAAc;EAChC,IAAM,oBAAM,IAAI,KAAa;AAC7B,OAAK,IAAM,KAAO,EAChB,MAAK,IAAM,KAAO,EAAI,SAAU,GAAI,IAAI,EAAI;AAE9C,SAAO,CAAC,GAAG,EAAI;IACd,CAAC,EAAS,CAAC,EAayB,EAAQ,EAEzC,IAAW,GAAiB,EAG5B,IAAuB,QAEzB,EAAS,KAAK,MACR,EAAI,SAAS,QAAc,EAAI,SAAS,MAAM,MAAM,CAAC,CAAC,EAAU,GAAG,GAChE,EAAI,SAAS,OAAO,MAAM,CAAC,CAAC,EAAU,GAAG,CAChD,EACJ,CAAC,GAAU,EAAU,CACtB,EAEK,IAAe,EAAS,SAAS,KAAK,EAAqB,MAAM,QAAQ,EAUzE,IAAkB,EAAO,EAAa;AAC5C,SAAgB;AAId,EAHI,KAAgB,CAAC,EAAgB,WACnC,KAAe,EAEjB,EAAgB,UAAU;IACzB,CAAC,GAAc,EAAY,CAAC;CAE/B,IAAM,IAAgB,EACpB,OAAO,MAAgB;AAIrB,QAAM,EAAQ,EAAI;IAEpB,CAAC,EAAQ,CACV;AAiGD,QA9FK,IAyBD,IACK,kBAAA,GAAA,EAAG,aAAY,CAAA,GAQpB,IAEA,kBAAC,OAAD;EACE,WAAW,yDAAyD,IAAU,QAAQ;EACtF,eAAY;YAEZ,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,EAAA,6CAEvC;;EACF,CAAA,GAQN,IAEA,kBAAC,OAAD;EACE,WAAW,yDAAyD,IAAU,QAAQ;YAEtF,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAO,WAAU,mDAAoD,CAAA,EACrE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,KAAD;MAAG,WAAU;gBAA0C;MAA0B,CAAA;KACjF,kBAAC,KAAD;MAAG,WAAU;gBAA8B;MAGvC,CAAA;KACJ,kBAAC,KAAD;MAAG,WAAU;gBAAqD;MAAU,CAAA;KAC5E,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,KAAK,GAAS;OAC7B,UAAU;OACV,WAAU;OACV,eAAY;iBALd,CAOE,kBAAC,GAAD,EAAW,WAAW,YAAY,IAAU,iBAAiB,MAAQ,CAAA,EAAA,QAE9D;UACT,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAS,UAAU;OAClC,WAAU;iBACX;OAEQ,CAAA,CACL;;KACF;MACF;;EACF,CAAA,GAQR,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAS,KAAK,GAAK,MAAQ;AAC1B,OAAI,EAAqB,GAAM,QAAO;GACtC,IAAM,KACJ,EAAI,MACA,EAAI,SAAS,QAAQ,MAAM,CAAC,EAAU,GAAG,GAEzC,IACJ,EAAI,SAAS,QACT,iCACA,EAAI,SAAS,WAAW,IACtB,oBACA,oBACF,IACJ,EAAI,SAAS,QACT,+FACA,EAAI,SAAS,WAAW,IACtB,6DACA;AAER,UACE,kBAAC,OAAD;IAEE,WAAW,yDAAyD,IAAU,QAAQ;cAEtF,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,iDAAkD,CAAA,EACpE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACG,EAAI,WACH,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAI;QACH,CAAA;OAEN,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAI,SAAS;QACZ,CAAA;OACJ,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAI,eAAe;QAClB,CAAA;OAEH,KACC,kBAAC,OAAD;QACE,MAAK;QACL,WAAU;kBAFZ,CAIE,kBAAC,GAAD,EAAa,WAAU,4BAA6B,CAAA,EACpD,kBAAC,QAAD,EAAA,UAAO,GAAa,CAAA,CAChB;;OAGR,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAgB,KAAK,MAAQ;SAC5B,IAAM,IAAO,EAAc,EAAI,EACzB,IAAe,MAAe,GAC9B,IAAS,MAAe;AAC9B,gBACE,kBAAC,MAAD;UAEE,WAAU;oBAFZ,CAIE,kBAAC,OAAD;WAAK,WAAU;qBAAf;YACE,kBAAC,KAAD;aAAG,WAAU;uBACV,EAAK;aACJ,CAAA;YACJ,kBAAC,KAAD;aAAG,WAAU;uBAAwC,EAAK;aAAgB,CAAA;YAC1E,kBAAC,KAAD;aACE,WAAU;aACV,OAAO;uBAEN;aACC,CAAA;YACA;cACN,kBAAC,UAAD;WACE,MAAK;WACL,eAAe,KAAK,EAAc,EAAI;WACtC,UAAU,KAAU;WACpB,WAAU;WACV,eAAa,kBAAkB;qBAE9B,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAS,WAAU,yBAA0B,CAAA,EAAA,cAE5C,EAAA,CAAA,GAEH,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAU,WAAU,YAAa,CAAA,EAAA,UAEhC,EAAA,CAAA;WAEE,CAAA,CACN;YAlCE,EAkCF;UAEP;QACC,CAAA;OAEJ,KAAW,EAAgB,WAAW,KACrC,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAS,WAAU,yBAA0B,CAAA,EAAA,oBAEzC;;OAEJ;QACF;;IACF,EAlFC,EAkFD;IAER,EAEF,kBAAC,GAAD;GACE,QAAQ;GACR,SAAS;GACT,iBAAiB;AAEf,IADA,GAAqB,EACrB,EAAS,iBAAiB;;GAE5B,OAAM;GACN,SAAQ;GACR,CAAA,CACE;MAnNJ,kBAAC,OAAD;EACE,WAAW,yDAAyD,IAAU,QAAQ;YAEtF,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,8CAA+C,CAAA,EACjE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,KAAD;MAAG,WAAU;gBAA0C;MAAmB,CAAA;KAC1E,kBAAC,KAAD;MAAG,WAAU;gBAA8B;MAEvC,CAAA;KACJ,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAS,UAAU;MAClC,WAAU;gBACX;MAEQ,CAAA;KACL;MACF;;EACF,CAAA"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/components/shared/cardMotion.ts
|
|
2
|
+
var e = {
|
|
3
|
+
rest: {
|
|
4
|
+
y: 0,
|
|
5
|
+
boxShadow: "var(--shadow-elevation-0, 0 1px 2px rgba(0,0,0,0.05))"
|
|
6
|
+
},
|
|
7
|
+
hover: {
|
|
8
|
+
y: -2,
|
|
9
|
+
boxShadow: "var(--shadow-elevation-2, 0 4px 12px rgba(0,0,0,0.10))",
|
|
10
|
+
transition: {
|
|
11
|
+
duration: .15,
|
|
12
|
+
ease: [
|
|
13
|
+
.4,
|
|
14
|
+
0,
|
|
15
|
+
.2,
|
|
16
|
+
1
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
tap: {
|
|
21
|
+
scale: .98,
|
|
22
|
+
transition: { duration: .1 }
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { e as cardHoverVariants };
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=cardMotion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cardMotion.js","names":[],"sources":["../../../src/components/shared/cardMotion.ts"],"sourcesContent":["import type { Variants } from 'framer-motion';\n\n/**\n * Hover/tap motion for cards (vibes, templates, …).\n *\n * Animates ONLY compositor-friendly properties — `y` (transform) and\n * `boxShadow` — so the effect runs on the GPU at 60fps. Previously these\n * cards combined a framer-motion `whileHover` with Tailwind `transition-all`,\n * which animated non-compositor properties (border, box-shadow) on the main\n * thread with mismatched timing, producing visible jank on the listing pages.\n *\n * Border-color hover stays a cheap CSS `transition-colors` on the element.\n */\nexport const cardHoverVariants: Variants = {\n rest: {\n y: 0,\n boxShadow: 'var(--shadow-elevation-0, 0 1px 2px rgba(0,0,0,0.05))',\n },\n hover: {\n y: -2,\n boxShadow: 'var(--shadow-elevation-2, 0 4px 12px rgba(0,0,0,0.10))',\n transition: { duration: 0.15, ease: [0.4, 0, 0.2, 1] },\n },\n tap: { scale: 0.98, transition: { duration: 0.1 } },\n};\n"],"mappings":";AAaA,IAAa,IAA8B;CACzC,MAAM;EACJ,GAAG;EACH,WAAW;EACZ;CACD,OAAO;EACL,GAAG;EACH,WAAW;EACX,YAAY;GAAE,UAAU;GAAM,MAAM;IAAC;IAAK;IAAG;IAAK;IAAE;GAAE;EACvD;CACD,KAAK;EAAE,OAAO;EAAM,YAAY,EAAE,UAAU,IAAK;EAAE;CACpD"}
|
|
@@ -1,93 +1,86 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { cardHoverVariants as e } from "../shared/cardMotion.js";
|
|
2
|
+
import { useI18n as t } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
3
|
+
import { Copy as n, FileCode as r, Layers as i, Tag as a, Variable as o } from "lucide-react";
|
|
4
|
+
import { motion as s } from "framer-motion";
|
|
5
|
+
import { jsx as c, jsxs as l } from "react/jsx-runtime";
|
|
5
6
|
//#region src/components/templates/TemplateCard.tsx
|
|
6
|
-
var
|
|
7
|
+
var u = {
|
|
7
8
|
SERVICE: "bg-action-primary-bg/10 text-action-primary-bg",
|
|
8
9
|
FRONTEND: "bg-status-info-bg text-status-info-text",
|
|
9
10
|
LIBRARY: "bg-status-warning-bg text-status-warning-text",
|
|
10
11
|
INFRASTRUCTURE: "bg-status-success-bg text-status-success-text",
|
|
11
12
|
STARTER: "bg-bg-sunken text-text-secondary"
|
|
12
|
-
},
|
|
13
|
-
let { t:
|
|
14
|
-
let n =
|
|
13
|
+
}, d = ({ template: d, onOpen: f, className: p = "" }) => {
|
|
14
|
+
let { t: m } = t(), h = (e, t) => {
|
|
15
|
+
let n = m(e);
|
|
15
16
|
return n === e ? t : n;
|
|
16
17
|
};
|
|
17
|
-
return /* @__PURE__ */
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.2,
|
|
26
|
-
1
|
|
27
|
-
]
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
whileTap: { scale: .98 },
|
|
31
|
-
className: `bg-bg-surface border border-border-default rounded-lg p-4 hover:border-border-strong hover:shadow-md transition-all cursor-pointer ${f}`,
|
|
32
|
-
onClick: () => d?.(u),
|
|
18
|
+
return /* @__PURE__ */ l(s.div, {
|
|
19
|
+
initial: "rest",
|
|
20
|
+
animate: "rest",
|
|
21
|
+
whileHover: "hover",
|
|
22
|
+
whileTap: "tap",
|
|
23
|
+
variants: e,
|
|
24
|
+
className: `bg-bg-surface border border-border-default rounded-lg p-4 hover:border-border-strong transition-colors cursor-pointer ${p}`,
|
|
25
|
+
onClick: () => f?.(d),
|
|
33
26
|
children: [
|
|
34
|
-
/* @__PURE__ */
|
|
27
|
+
/* @__PURE__ */ l("div", {
|
|
35
28
|
className: "flex items-start gap-2 sm:gap-3",
|
|
36
|
-
children: [/* @__PURE__ */
|
|
29
|
+
children: [/* @__PURE__ */ c("div", {
|
|
37
30
|
className: "flex-shrink-0 size-8 sm:w-10 sm:h-10 rounded-lg bg-action-primary-bg/10 flex items-center justify-center",
|
|
38
|
-
children: /* @__PURE__ */
|
|
39
|
-
}), /* @__PURE__ */
|
|
31
|
+
children: /* @__PURE__ */ c(r, { className: "size-4 sm:w-5 sm:h-5 text-action-primary-bg" })
|
|
32
|
+
}), /* @__PURE__ */ l("div", {
|
|
40
33
|
className: "min-w-0 flex-1",
|
|
41
|
-
children: [/* @__PURE__ */
|
|
34
|
+
children: [/* @__PURE__ */ c("h3", {
|
|
42
35
|
className: "font-semibold text-text-primary truncate text-sm sm:text-base",
|
|
43
|
-
children:
|
|
44
|
-
}), /* @__PURE__ */
|
|
36
|
+
children: d.name
|
|
37
|
+
}), /* @__PURE__ */ l("div", {
|
|
45
38
|
className: "flex items-center gap-2 mt-0.5 flex-wrap",
|
|
46
39
|
children: [
|
|
47
|
-
|
|
48
|
-
className: `inline-flex items-center gap-1 px-1.5 py-0.5 text-xs rounded ${
|
|
49
|
-
children: [/* @__PURE__ */
|
|
40
|
+
d.category && /* @__PURE__ */ l("span", {
|
|
41
|
+
className: `inline-flex items-center gap-1 px-1.5 py-0.5 text-xs rounded ${u[d.category] || "bg-bg-sunken text-text-secondary"}`,
|
|
42
|
+
children: [/* @__PURE__ */ c(i, { className: "size-3" }), h(`template.category.${d.category.toLowerCase()}`, d.category)]
|
|
50
43
|
}),
|
|
51
|
-
/* @__PURE__ */
|
|
44
|
+
/* @__PURE__ */ l("span", {
|
|
52
45
|
className: "inline-flex items-center gap-1 text-xs text-text-muted",
|
|
53
46
|
children: [
|
|
54
|
-
/* @__PURE__ */
|
|
55
|
-
|
|
47
|
+
/* @__PURE__ */ c(n, { className: "size-3" }),
|
|
48
|
+
d.cloneCount,
|
|
56
49
|
" ",
|
|
57
|
-
|
|
50
|
+
h("template.clones", "clones")
|
|
58
51
|
]
|
|
59
52
|
}),
|
|
60
|
-
|
|
53
|
+
d.variableCount > 0 && /* @__PURE__ */ l("span", {
|
|
61
54
|
className: "inline-flex items-center gap-1 text-xs text-text-muted",
|
|
62
55
|
children: [
|
|
63
|
-
/* @__PURE__ */
|
|
64
|
-
|
|
56
|
+
/* @__PURE__ */ c(o, { className: "size-3" }),
|
|
57
|
+
d.variableCount,
|
|
65
58
|
" var",
|
|
66
|
-
|
|
59
|
+
d.variableCount === 1 ? "" : "s"
|
|
67
60
|
]
|
|
68
61
|
})
|
|
69
62
|
]
|
|
70
63
|
})]
|
|
71
64
|
})]
|
|
72
65
|
}),
|
|
73
|
-
|
|
66
|
+
d.description && /* @__PURE__ */ c("p", {
|
|
74
67
|
className: "mt-2 text-sm text-text-secondary line-clamp-2",
|
|
75
|
-
children:
|
|
68
|
+
children: d.description
|
|
76
69
|
}),
|
|
77
|
-
|
|
70
|
+
d.tags.length > 0 && /* @__PURE__ */ l("div", {
|
|
78
71
|
className: "mt-3 flex flex-wrap gap-1",
|
|
79
|
-
children: [
|
|
72
|
+
children: [d.tags.slice(0, 3).map((e) => /* @__PURE__ */ l("span", {
|
|
80
73
|
className: "inline-flex items-center gap-1 px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded",
|
|
81
|
-
children: [/* @__PURE__ */
|
|
82
|
-
}, e)),
|
|
74
|
+
children: [/* @__PURE__ */ c(a, { className: "size-3" }), e]
|
|
75
|
+
}, e)), d.tags.length > 3 && /* @__PURE__ */ l("span", {
|
|
83
76
|
className: "px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded",
|
|
84
|
-
children: ["+",
|
|
77
|
+
children: ["+", d.tags.length - 3]
|
|
85
78
|
})]
|
|
86
79
|
})
|
|
87
80
|
]
|
|
88
81
|
});
|
|
89
82
|
};
|
|
90
83
|
//#endregion
|
|
91
|
-
export {
|
|
84
|
+
export { d as TemplateCard };
|
|
92
85
|
|
|
93
86
|
//# sourceMappingURL=TemplateCard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TemplateCard.js","names":[],"sources":["../../../src/components/templates/TemplateCard.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { motion } from 'framer-motion';\nimport { FileCode, Tag, Copy, Layers, Variable } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nexport interface TemplateCardData {\n id: string;\n name: string;\n description: string | null;\n category: string | null;\n cloneCount: number;\n variableCount: number;\n tags: string[];\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface TemplateCardProps {\n template: TemplateCardData;\n onOpen?: (template: TemplateCardData) => void;\n className?: string;\n}\n\nconst categoryStyles: Record<string, string> = {\n SERVICE: 'bg-action-primary-bg/10 text-action-primary-bg',\n FRONTEND: 'bg-status-info-bg text-status-info-text',\n LIBRARY: 'bg-status-warning-bg text-status-warning-text',\n INFRASTRUCTURE: 'bg-status-success-bg text-status-success-text',\n STARTER: 'bg-bg-sunken text-text-secondary',\n};\n\nexport const TemplateCard: FC<TemplateCardProps> = ({ template, onOpen, className = '' }) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string) => {\n const v = t(key);\n return v === key ? fallback : v;\n };\n\n return (\n <motion.div\n whileHover
|
|
1
|
+
{"version":3,"file":"TemplateCard.js","names":[],"sources":["../../../src/components/templates/TemplateCard.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { motion } from 'framer-motion';\nimport { FileCode, Tag, Copy, Layers, Variable } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { cardHoverVariants } from '../shared/cardMotion';\n\nexport interface TemplateCardData {\n id: string;\n name: string;\n description: string | null;\n category: string | null;\n cloneCount: number;\n variableCount: number;\n tags: string[];\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface TemplateCardProps {\n template: TemplateCardData;\n onOpen?: (template: TemplateCardData) => void;\n className?: string;\n}\n\nconst categoryStyles: Record<string, string> = {\n SERVICE: 'bg-action-primary-bg/10 text-action-primary-bg',\n FRONTEND: 'bg-status-info-bg text-status-info-text',\n LIBRARY: 'bg-status-warning-bg text-status-warning-text',\n INFRASTRUCTURE: 'bg-status-success-bg text-status-success-text',\n STARTER: 'bg-bg-sunken text-text-secondary',\n};\n\nexport const TemplateCard: FC<TemplateCardProps> = ({ template, onOpen, className = '' }) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string) => {\n const v = t(key);\n return v === key ? fallback : v;\n };\n\n return (\n <motion.div\n initial=\"rest\"\n animate=\"rest\"\n whileHover=\"hover\"\n whileTap=\"tap\"\n variants={cardHoverVariants}\n className={`bg-bg-surface border border-border-default rounded-lg p-4 hover:border-border-strong transition-colors cursor-pointer ${className}`}\n onClick={() => onOpen?.(template)}\n >\n <div className=\"flex items-start gap-2 sm:gap-3\">\n <div className=\"flex-shrink-0 size-8 sm:w-10 sm:h-10 rounded-lg bg-action-primary-bg/10 flex items-center justify-center\">\n <FileCode className=\"size-4 sm:w-5 sm:h-5 text-action-primary-bg\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <h3 className=\"font-semibold text-text-primary truncate text-sm sm:text-base\">\n {template.name}\n </h3>\n <div className=\"flex items-center gap-2 mt-0.5 flex-wrap\">\n {template.category && (\n <span\n className={`inline-flex items-center gap-1 px-1.5 py-0.5 text-xs rounded ${categoryStyles[template.category] || 'bg-bg-sunken text-text-secondary'}`}\n >\n <Layers className=\"size-3\" />\n {tr(`template.category.${template.category.toLowerCase()}`, template.category)}\n </span>\n )}\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Copy className=\"size-3\" />\n {template.cloneCount} {tr('template.clones', 'clones')}\n </span>\n {template.variableCount > 0 && (\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Variable className=\"size-3\" />\n {template.variableCount} var{template.variableCount !== 1 ? 's' : ''}\n </span>\n )}\n </div>\n </div>\n </div>\n\n {template.description && (\n <p className=\"mt-2 text-sm text-text-secondary line-clamp-2\">{template.description}</p>\n )}\n\n {template.tags.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1\">\n {template.tags.slice(0, 3).map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center gap-1 px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded\"\n >\n <Tag className=\"size-3\" />\n {tag}\n </span>\n ))}\n {template.tags.length > 3 && (\n <span className=\"px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded\">\n +{template.tags.length - 3}\n </span>\n )}\n </div>\n )}\n </motion.div>\n );\n};\n"],"mappings":";;;;;;AAwBA,IAAM,IAAyC;CAC7C,SAAS;CACT,UAAU;CACV,SAAS;CACT,gBAAgB;CAChB,SAAS;CACV,EAEY,KAAuC,EAAE,aAAU,WAAQ,eAAY,SAAS;CAC3F,IAAM,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAAqB;EAC5C,IAAM,IAAI,EAAE,EAAI;AAChB,SAAO,MAAM,IAAM,IAAW;;AAGhC,QACE,kBAAC,EAAO,KAAR;EACE,SAAQ;EACR,SAAQ;EACR,YAAW;EACX,UAAS;EACT,UAAU;EACV,WAAW,yHAAyH;EACpI,eAAe,IAAS,EAAS;YAPnC;GASE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD,EAAU,WAAU,+CAAgD,CAAA;KAChE,CAAA,EACN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAS;MACP,CAAA,EACL,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACG,EAAS,YACR,kBAAC,QAAD;QACE,WAAW,gEAAgE,EAAe,EAAS,aAAa;kBADlH,CAGE,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA,EAC5B,EAAG,qBAAqB,EAAS,SAAS,aAAa,IAAI,EAAS,SAAS,CACzE;;OAET,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACE,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA;SAC1B,EAAS;SAAW;SAAE,EAAG,mBAAmB,SAAS;SACjD;;OACN,EAAS,gBAAgB,KACxB,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACE,kBAAC,GAAD,EAAU,WAAU,UAAW,CAAA;SAC9B,EAAS;SAAc;SAAK,EAAS,kBAAkB,IAAU,KAAN;SACvD;;OAEL;QACF;OACF;;GAEL,EAAS,eACR,kBAAC,KAAD;IAAG,WAAU;cAAiD,EAAS;IAAgB,CAAA;GAGxF,EAAS,KAAK,SAAS,KACtB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,EAAS,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK,MAC9B,kBAAC,QAAD;KAEE,WAAU;eAFZ,CAIE,kBAAC,GAAD,EAAK,WAAU,UAAW,CAAA,EACzB,EACI;OALA,EAKA,CACP,EACD,EAAS,KAAK,SAAS,KACtB,kBAAC,QAAD;KAAM,WAAU;eAAhB,CAA+E,KAC3E,EAAS,KAAK,SAAS,EACpB;OAEL;;GAEG"}
|