@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.
Files changed (44) hide show
  1. package/dist/VibeControlsRoutes.js +142 -146
  2. package/dist/VibeControlsRoutes.js.map +1 -1
  3. package/dist/components/agent-manager/controls/ModelSelector.js +1 -1
  4. package/dist/components/agent-manager/controls/ModelSelector.js.map +1 -1
  5. package/dist/components/agents/AgentCard.js +106 -85
  6. package/dist/components/agents/AgentCard.js.map +1 -1
  7. package/dist/components/agents/AgentStatusBadge.js +25 -24
  8. package/dist/components/agents/AgentStatusBadge.js.map +1 -1
  9. package/dist/components/shared/LinkToVibePanel.js +36 -26
  10. package/dist/components/shared/LinkToVibePanel.js.map +1 -1
  11. package/dist/components/shared/PluginGate.js +72 -68
  12. package/dist/components/shared/PluginGate.js.map +1 -1
  13. package/dist/components/shared/cardMotion.js +28 -0
  14. package/dist/components/shared/cardMotion.js.map +1 -0
  15. package/dist/components/templates/TemplateCard.js +43 -50
  16. package/dist/components/templates/TemplateCard.js.map +1 -1
  17. package/dist/components/tunnels/TunnelCreateWizard.js +57 -49
  18. package/dist/components/tunnels/TunnelCreateWizard.js.map +1 -1
  19. package/dist/components/vibe/VibeLinkedEntitiesPanel.js +121 -72
  20. package/dist/components/vibe/VibeLinkedEntitiesPanel.js.map +1 -1
  21. package/dist/components/vibes/VibeCard.js +65 -72
  22. package/dist/components/vibes/VibeCard.js.map +1 -1
  23. package/dist/generated/wspace-operations.js +964 -886
  24. package/dist/generated/wspace-operations.js.map +1 -1
  25. package/dist/generated/wspace-types.js.map +1 -1
  26. package/dist/index.js +10 -10
  27. package/dist/pages/ActionsPage.js +297 -268
  28. package/dist/pages/ActionsPage.js.map +1 -1
  29. package/dist/pages/AgentDetailsPage.js +11 -11
  30. package/dist/pages/AnalyticsPage.js +2 -2
  31. package/dist/pages/AnalyticsPage.js.map +1 -1
  32. package/dist/pages/TargetsPage.js +16 -16
  33. package/dist/pages/VibeAuditPage.js +7 -7
  34. package/dist/pages/VibeDeckPage.js +180 -180
  35. package/dist/pages/VibeDeckPage.js.map +1 -1
  36. package/dist/pages/ai/AIPage.js +2 -2
  37. package/dist/pages/ai/AIPage.js.map +1 -1
  38. package/dist/pages/ai/AgentManagerPage.js +162 -127
  39. package/dist/pages/ai/AgentManagerPage.js.map +1 -1
  40. package/dist/utils/agentHealth.js +31 -0
  41. package/dist/utils/agentHealth.js.map +1 -0
  42. package/package.json +2 -2
  43. package/dist/pages/PluginStandalonePage.js +0 -151
  44. 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, useMemo as a } from "react";
6
- import { AlertCircle as o, Clock as s, Download as c, Loader2 as l, Puzzle as u, RefreshCw as d, Server as f } from "lucide-react";
7
- import { Fragment as p, jsx as m, jsxs as h } from "react/jsx-runtime";
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 g(e) {
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: v, compact: y = !1, onInstalled: b, children: x } = _, S = a(() => g(_), [_]), { loading: C, initialLoading: w, installed: T, installing: E, install: D, error: O, errorIsAgentState: k, quotaExhausted: A, clearQuotaExhausted: j, refetch: M } = n(a(() => {
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 S) for (let n of t.packages) e.add(n);
27
+ for (let t of w) for (let n of t.packages) e.add(n);
28
28
  return [...e];
29
- }, [S]), v), N = t(), P = a(() => S.map((e) => e.mode === "any" ? e.packages.some((e) => !!T[e]) : e.packages.every((e) => !!T[e])), [S, T]), F = S.length > 0 && P.every(Boolean), I = i(async (e) => {
30
- await D(e) && b?.();
31
- }, [D, b]);
32
- return v ? F ? /* @__PURE__ */ m(p, { children: x }) : w ? /* @__PURE__ */ m("div", {
33
- className: `rounded-lg border border-border-default bg-bg-surface ${y ? "p-4" : "p-6"}`,
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__ */ h("div", {
39
+ children: /* @__PURE__ */ _("div", {
36
40
  className: "flex items-center gap-2 text-sm text-text-secondary",
37
- children: [/* @__PURE__ */ m(l, { className: "size-4 animate-spin" }), "Checking plugin availability on the agent…"]
41
+ children: [/* @__PURE__ */ g(d, { className: "size-4 animate-spin" }), "Checking plugin availability on the agent…"]
38
42
  })
39
- }) : k ? /* @__PURE__ */ m("div", {
40
- className: `rounded-lg border border-border-default bg-bg-surface ${y ? "p-4" : "p-6"}`,
41
- children: /* @__PURE__ */ h("div", {
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__ */ m(s, { className: "size-5 text-status-warning-text shrink-0 mt-0.5" }), /* @__PURE__ */ h("div", {
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__ */ m("p", {
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__ */ m("p", {
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__ */ m("p", {
58
+ /* @__PURE__ */ g("p", {
55
59
  className: "text-xs font-mono text-text-placeholder break-all",
56
- children: O
60
+ children: A
57
61
  }),
58
- /* @__PURE__ */ h("div", {
62
+ /* @__PURE__ */ _("div", {
59
63
  className: "flex flex-wrap items-center gap-2 pt-1",
60
- children: [/* @__PURE__ */ h("button", {
64
+ children: [/* @__PURE__ */ _("button", {
61
65
  type: "button",
62
- onClick: () => void M(),
63
- disabled: C,
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__ */ m(d, { className: `size-3.5 ${C ? "animate-spin" : ""}` }), "Retry"]
67
- }), /* @__PURE__ */ m("button", {
70
+ children: [/* @__PURE__ */ g(p, { className: `size-3.5 ${T ? "animate-spin" : ""}` }), "Retry"]
71
+ }), /* @__PURE__ */ g("button", {
68
72
  type: "button",
69
- onClick: () => N("/agents"),
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__ */ h("div", {
81
+ }) : /* @__PURE__ */ _("div", {
78
82
  className: "space-y-3",
79
- children: [S.map((e, t) => {
80
- if (P[t]) return null;
81
- let n = (e.mode, e.packages.filter((e) => !T[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.";
82
- return /* @__PURE__ */ m("div", {
83
- className: `rounded-lg border border-border-default bg-bg-surface ${y ? "p-4" : "p-6"}`,
84
- children: /* @__PURE__ */ h("div", {
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__ */ m(u, { className: "size-5 text-action-primary-bg shrink-0 mt-0.5" }), /* @__PURE__ */ h("div", {
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__ */ m("p", {
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__ */ m("p", {
97
+ /* @__PURE__ */ g("p", {
94
98
  className: "text-sm font-semibold text-text-primary",
95
99
  children: e.title ?? i
96
100
  }),
97
- /* @__PURE__ */ m("p", {
101
+ /* @__PURE__ */ g("p", {
98
102
  className: "mt-1 text-sm text-text-secondary",
99
103
  children: e.description ?? a
100
104
  }),
101
- O && /* @__PURE__ */ h("div", {
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__ */ m(o, { className: "size-3.5 mt-0.5 shrink-0" }), /* @__PURE__ */ m("span", { children: O })]
108
+ children: [/* @__PURE__ */ g(c, { className: "size-3.5 mt-0.5 shrink-0" }), /* @__PURE__ */ g("span", { children: A })]
105
109
  }),
106
- /* @__PURE__ */ m("ul", {
110
+ /* @__PURE__ */ g("ul", {
107
111
  className: "mt-4 space-y-2",
108
112
  children: n.map((e) => {
109
- let t = r(e), n = E === e, i = E !== null;
110
- return /* @__PURE__ */ h("li", {
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__ */ h("div", {
116
+ children: [/* @__PURE__ */ _("div", {
113
117
  className: "min-w-0",
114
118
  children: [
115
- /* @__PURE__ */ m("p", {
119
+ /* @__PURE__ */ g("p", {
116
120
  className: "text-sm font-medium text-text-primary truncate",
117
121
  children: t.displayName
118
122
  }),
119
- /* @__PURE__ */ m("p", {
123
+ /* @__PURE__ */ g("p", {
120
124
  className: "text-xs text-text-secondary truncate",
121
125
  children: t.description
122
126
  }),
123
- /* @__PURE__ */ m("p", {
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__ */ m("button", {
133
+ }), /* @__PURE__ */ g("button", {
130
134
  type: "button",
131
- onClick: () => void I(e),
132
- disabled: i || C,
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(p, { children: [/* @__PURE__ */ m(l, { className: "size-3.5 animate-spin" }), "Installing…"] }) : /* @__PURE__ */ h(p, { children: [/* @__PURE__ */ m(c, { className: "size-3.5" }), "Install"] })
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
- C && n.length === 0 && /* @__PURE__ */ h("div", {
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__ */ m(l, { className: "size-3.5 animate-spin" }), "Checking plugins…"]
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__ */ m(e, {
149
- isOpen: A,
150
- onClose: j,
152
+ }), /* @__PURE__ */ g(e, {
153
+ isOpen: M,
154
+ onClose: N,
151
155
  onConfirm: () => {
152
- j(), N("/billing/plans");
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__ */ m("div", {
158
- className: `rounded-lg border border-border-default bg-bg-surface ${y ? "p-4" : "p-6"}`,
159
- children: /* @__PURE__ */ h("div", {
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(f, { className: "size-5 text-text-secondary shrink-0 mt-0.5" }), /* @__PURE__ */ h("div", {
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__ */ m("p", {
168
+ /* @__PURE__ */ g("p", {
165
169
  className: "text-sm font-semibold text-text-primary",
166
170
  children: "No active agent"
167
171
  }),
168
- /* @__PURE__ */ m("p", {
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__ */ m("button", {
176
+ /* @__PURE__ */ g("button", {
173
177
  type: "button",
174
- onClick: () => N("/agents"),
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 { _ as PluginGate };
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":"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"}
@@ -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 { useI18n as e } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
2
- import { Copy as t, FileCode as n, Layers as r, Tag as i, Variable as a } from "lucide-react";
3
- import { motion as o } from "framer-motion";
4
- import { jsx as s, jsxs as c } from "react/jsx-runtime";
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 l = {
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
- }, u = ({ template: u, onOpen: d, className: f = "" }) => {
13
- let { t: p } = e(), m = (e, t) => {
14
- let n = p(e);
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__ */ c(o.div, {
18
- whileHover: {
19
- y: -2,
20
- transition: {
21
- duration: .15,
22
- ease: [
23
- .4,
24
- 0,
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__ */ c("div", {
27
+ /* @__PURE__ */ l("div", {
35
28
  className: "flex items-start gap-2 sm:gap-3",
36
- children: [/* @__PURE__ */ s("div", {
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__ */ s(n, { className: "size-4 sm:w-5 sm:h-5 text-action-primary-bg" })
39
- }), /* @__PURE__ */ c("div", {
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__ */ s("h3", {
34
+ children: [/* @__PURE__ */ c("h3", {
42
35
  className: "font-semibold text-text-primary truncate text-sm sm:text-base",
43
- children: u.name
44
- }), /* @__PURE__ */ c("div", {
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
- u.category && /* @__PURE__ */ c("span", {
48
- className: `inline-flex items-center gap-1 px-1.5 py-0.5 text-xs rounded ${l[u.category] || "bg-bg-sunken text-text-secondary"}`,
49
- children: [/* @__PURE__ */ s(r, { className: "size-3" }), m(`template.category.${u.category.toLowerCase()}`, u.category)]
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__ */ c("span", {
44
+ /* @__PURE__ */ l("span", {
52
45
  className: "inline-flex items-center gap-1 text-xs text-text-muted",
53
46
  children: [
54
- /* @__PURE__ */ s(t, { className: "size-3" }),
55
- u.cloneCount,
47
+ /* @__PURE__ */ c(n, { className: "size-3" }),
48
+ d.cloneCount,
56
49
  " ",
57
- m("template.clones", "clones")
50
+ h("template.clones", "clones")
58
51
  ]
59
52
  }),
60
- u.variableCount > 0 && /* @__PURE__ */ c("span", {
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__ */ s(a, { className: "size-3" }),
64
- u.variableCount,
56
+ /* @__PURE__ */ c(o, { className: "size-3" }),
57
+ d.variableCount,
65
58
  " var",
66
- u.variableCount === 1 ? "" : "s"
59
+ d.variableCount === 1 ? "" : "s"
67
60
  ]
68
61
  })
69
62
  ]
70
63
  })]
71
64
  })]
72
65
  }),
73
- u.description && /* @__PURE__ */ s("p", {
66
+ d.description && /* @__PURE__ */ c("p", {
74
67
  className: "mt-2 text-sm text-text-secondary line-clamp-2",
75
- children: u.description
68
+ children: d.description
76
69
  }),
77
- u.tags.length > 0 && /* @__PURE__ */ c("div", {
70
+ d.tags.length > 0 && /* @__PURE__ */ l("div", {
78
71
  className: "mt-3 flex flex-wrap gap-1",
79
- children: [u.tags.slice(0, 3).map((e) => /* @__PURE__ */ c("span", {
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__ */ s(i, { className: "size-3" }), e]
82
- }, e)), u.tags.length > 3 && /* @__PURE__ */ c("span", {
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: ["+", u.tags.length - 3]
77
+ children: ["+", d.tags.length - 3]
85
78
  })]
86
79
  })
87
80
  ]
88
81
  });
89
82
  };
90
83
  //#endregion
91
- export { u as TemplateCard };
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={{ y: -2, transition: { duration: 0.15, ease: [0.4, 0, 0.2, 1] } }}\n whileTap={{ scale: 0.98 }}\n className={`bg-bg-surface border border-border-default rounded-lg p-4 hover:border-border-strong hover:shadow-md transition-all 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":";;;;;AAuBA,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,YAAY;GAAE,GAAG;GAAI,YAAY;IAAE,UAAU;IAAM,MAAM;KAAC;KAAK;KAAG;KAAK;KAAE;IAAE;GAAE;EAC7E,UAAU,EAAE,OAAO,KAAM;EACzB,WAAW,sIAAsI;EACjJ,eAAe,IAAS,EAAS;YAJnC;GAME,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"}
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"}