@burdenoff/microfe-vibecontrols 2026.602.3 → 2026.602.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,204 @@
1
+ import { useLinkEntityToVibeMutation as e, useListVibesQuery as t, useUnlinkEntityFromVibeMutation as n, useUpdateEntityVisibilityMutation as r } from "../../generated/wspace-operations.js";
2
+ import { useTr as i } from "../../shared/hooks/useTr.js";
3
+ import { VibeVisibility as a } from "../../generated/wspace-types.js";
4
+ import { useEffect as o, useMemo as s, useRef as c, useState as l } from "react";
5
+ import { Check as u, Link2 as d, Loader2 as f, Unlink as p } from "lucide-react";
6
+ import { jsx as m, jsxs as h } from "react/jsx-runtime";
7
+ //#region src/components/shared/EntityVibeLinkControl.tsx
8
+ function g({ entityType: g, entityId: _, currentVibeId: v, currentVisibility: y, refetchQueries: b, className: x = "" }) {
9
+ let S = i(), [C, w] = l(!1), [T, E] = l(v), [D, O] = l(y ?? a.SharedWithVibe), [k, A] = l(!1), [j, M] = l(null), N = c(null), { data: P, loading: F } = t({
10
+ variables: { pagination: { limit: 200 } },
11
+ fetchPolicy: "cache-first"
12
+ }), I = s(() => (P?.vibes?.items ?? []).map((e) => ({
13
+ id: e.id,
14
+ name: e.name
15
+ })), [P?.vibes?.items]), L = s(() => v ? I.find((e) => e.id === v)?.name ?? null : null, [v, I]), R = s(() => Array.from(new Set(["GetVibeLinkedEntities", ...b ?? []])), [b]), [z] = e(), [B] = n(), [V] = r();
16
+ o(() => {
17
+ C && (E(v), O(y ?? a.SharedWithVibe), M(null));
18
+ }, [
19
+ C,
20
+ v,
21
+ y
22
+ ]), o(() => {
23
+ if (!C) return;
24
+ let e = (e) => {
25
+ N.current?.contains(e.target) || w(!1);
26
+ };
27
+ return document.addEventListener("mousedown", e), () => document.removeEventListener("mousedown", e);
28
+ }, [C]);
29
+ let H = !!v, U = y === a.SharedWithVibe, W = async (e) => {
30
+ A(!0), M(null);
31
+ try {
32
+ await e(), w(!1);
33
+ } catch (e) {
34
+ M(e instanceof Error ? e.message : S("vibecontrols.entityVibeLink.error", "Could not update the vibe link."));
35
+ } finally {
36
+ A(!1);
37
+ }
38
+ };
39
+ return /* @__PURE__ */ h("div", {
40
+ className: "relative",
41
+ ref: N,
42
+ children: [/* @__PURE__ */ h("button", {
43
+ type: "button",
44
+ onClick: (e) => {
45
+ e.stopPropagation(), w((e) => !e);
46
+ },
47
+ "aria-haspopup": "dialog",
48
+ "aria-expanded": C,
49
+ title: H ? S("vibecontrols.entityVibeLink.linkedTo", "Linked to a vibe — click to manage") : S("vibecontrols.entityVibeLink.notLinked", "Link this to a vibe"),
50
+ className: `inline-flex max-w-[180px] items-center gap-1.5 rounded-md border px-2 py-1 text-xs transition-colors ${H ? "border-action-primary-bg/40 bg-action-primary-bg/10 text-action-primary-bg hover:bg-action-primary-bg/15" : "border-border-default text-text-secondary hover:bg-bg-elevated"} ${x}`,
51
+ children: [
52
+ /* @__PURE__ */ m(d, {
53
+ size: 13,
54
+ className: "flex-shrink-0"
55
+ }),
56
+ /* @__PURE__ */ m("span", {
57
+ className: "truncate",
58
+ children: H ? L ?? S("vibecontrols.entityVibeLink.aVibe", "Linked vibe") : S("vibecontrols.entityVibeLink.link", "Link to vibe")
59
+ }),
60
+ H && /* @__PURE__ */ m("span", {
61
+ className: `ml-0.5 inline-block size-1.5 flex-shrink-0 rounded-full ${U ? "bg-status-success-text" : "bg-text-tertiary"}`,
62
+ "aria-hidden": !0
63
+ })
64
+ ]
65
+ }), C && /* @__PURE__ */ h("div", {
66
+ role: "dialog",
67
+ "aria-label": S("vibecontrols.entityVibeLink.manage", "Manage vibe link"),
68
+ onClick: (e) => e.stopPropagation(),
69
+ className: "absolute right-0 z-30 mt-1 w-72 rounded-lg border border-border-default bg-bg-elevated p-3 shadow-lg",
70
+ children: [
71
+ /* @__PURE__ */ m("p", {
72
+ className: "mb-2 text-xs font-medium text-text-secondary",
73
+ children: S("vibecontrols.entityVibeLink.title", "Linked vibe")
74
+ }),
75
+ /* @__PURE__ */ h("select", {
76
+ value: T ?? "",
77
+ disabled: k,
78
+ onChange: (e) => E(e.target.value === "" ? null : e.target.value),
79
+ "aria-label": S("vibecontrols.entityVibeLink.pickAria", "Pick a vibe to link"),
80
+ className: "w-full rounded-md border border-border-default bg-bg-sunken px-2.5 py-1.5 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-action-primary-bg",
81
+ children: [/* @__PURE__ */ m("option", {
82
+ value: "",
83
+ children: F ? S("vibecontrols.entityVibeLink.loading", "Loading vibes…") : S("vibecontrols.entityVibeLink.none", "Not linked to any vibe")
84
+ }), I.map((e) => /* @__PURE__ */ m("option", {
85
+ value: e.id,
86
+ children: e.name
87
+ }, e.id))]
88
+ }),
89
+ T && /* @__PURE__ */ h("div", {
90
+ role: "radiogroup",
91
+ "aria-label": S("vibecontrols.entityVibeLink.visibility", "Who can see it"),
92
+ className: "mt-2.5 space-y-1.5",
93
+ children: [/* @__PURE__ */ h("label", {
94
+ className: "flex cursor-pointer items-start gap-2 text-xs text-text-primary",
95
+ children: [/* @__PURE__ */ m("input", {
96
+ type: "radio",
97
+ name: `entity-vibe-visibility-${_}`,
98
+ value: a.SharedWithVibe,
99
+ checked: D === a.SharedWithVibe,
100
+ disabled: k,
101
+ onChange: () => O(a.SharedWithVibe),
102
+ className: "mt-0.5"
103
+ }), /* @__PURE__ */ m("span", { children: S("vibecontrols.entityVibeLink.shared", "Shared with vibe — visible to everyone with access") })]
104
+ }), /* @__PURE__ */ h("label", {
105
+ className: "flex cursor-pointer items-start gap-2 text-xs text-text-primary",
106
+ children: [/* @__PURE__ */ m("input", {
107
+ type: "radio",
108
+ name: `entity-vibe-visibility-${_}`,
109
+ value: a.Private,
110
+ checked: D === a.Private,
111
+ disabled: k,
112
+ onChange: () => O(a.Private),
113
+ className: "mt-0.5"
114
+ }), /* @__PURE__ */ m("span", { children: S("vibecontrols.entityVibeLink.private", "Private to me") })]
115
+ })]
116
+ }),
117
+ j && /* @__PURE__ */ m("p", {
118
+ className: "mt-2 text-xs text-status-error-text",
119
+ children: j
120
+ }),
121
+ /* @__PURE__ */ h("div", {
122
+ className: "mt-3 flex items-center justify-between gap-2",
123
+ children: [H ? /* @__PURE__ */ h("button", {
124
+ type: "button",
125
+ onClick: async () => {
126
+ await W(() => B({
127
+ variables: {
128
+ entityType: g,
129
+ entityId: _
130
+ },
131
+ refetchQueries: R,
132
+ awaitRefetchQueries: !0
133
+ }));
134
+ },
135
+ disabled: k,
136
+ className: "inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs text-status-error-text hover:bg-status-error-bg/20 disabled:opacity-50",
137
+ children: [/* @__PURE__ */ m(p, { size: 13 }), S("vibecontrols.entityVibeLink.unlink", "Unlink")]
138
+ }) : /* @__PURE__ */ m("span", {}), /* @__PURE__ */ h("div", {
139
+ className: "flex items-center gap-2",
140
+ children: [/* @__PURE__ */ m("button", {
141
+ type: "button",
142
+ onClick: () => w(!1),
143
+ disabled: k,
144
+ className: "rounded-md px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken disabled:opacity-50",
145
+ children: S("common.cancel", "Cancel")
146
+ }), /* @__PURE__ */ h("button", {
147
+ type: "button",
148
+ onClick: async () => {
149
+ let e = {
150
+ refetchQueries: R,
151
+ awaitRefetchQueries: !0
152
+ };
153
+ if (!T) {
154
+ v ? await W(() => B({
155
+ variables: {
156
+ entityType: g,
157
+ entityId: _
158
+ },
159
+ ...e
160
+ })) : w(!1);
161
+ return;
162
+ }
163
+ if (T !== v) {
164
+ await W(() => z({
165
+ variables: { input: {
166
+ entityType: g,
167
+ entityId: _,
168
+ vibeId: T,
169
+ visibility: D
170
+ } },
171
+ ...e
172
+ }));
173
+ return;
174
+ }
175
+ if (D !== (y ?? a.SharedWithVibe)) {
176
+ await W(() => V({
177
+ variables: {
178
+ entityType: g,
179
+ entityId: _,
180
+ visibility: D
181
+ },
182
+ ...e
183
+ }));
184
+ return;
185
+ }
186
+ w(!1);
187
+ },
188
+ disabled: k,
189
+ className: "inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-2.5 py-1 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg-hover disabled:opacity-50",
190
+ children: [k ? /* @__PURE__ */ m(f, {
191
+ size: 13,
192
+ className: "animate-spin"
193
+ }) : /* @__PURE__ */ m(u, { size: 13 }), S("common.save", "Save")]
194
+ })]
195
+ })]
196
+ })
197
+ ]
198
+ })]
199
+ });
200
+ }
201
+ //#endregion
202
+ export { g as EntityVibeLinkControl };
203
+
204
+ //# sourceMappingURL=EntityVibeLinkControl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EntityVibeLinkControl.js","names":[],"sources":["../../../src/components/shared/EntityVibeLinkControl.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react';\nimport { Link2, Loader2, Unlink, Check } from 'lucide-react';\nimport {\n useListVibesQuery,\n useLinkEntityToVibeMutation,\n useUnlinkEntityFromVibeMutation,\n useUpdateEntityVisibilityMutation,\n} from '@/generated/wspace-operations';\nimport { VibeEntityType, VibeVisibility } from '@/generated/wspace-types';\nimport { useTr } from '../../shared/hooks/useTr';\n\ninterface EntityVibeLinkControlProps {\n /** Which kind of entity this control manages (ACTION, TUNNEL, …). */\n entityType: VibeEntityType;\n /** The entity's id. */\n entityId: string;\n /** Currently-linked vibe id, or null when unlinked. */\n currentVibeId: string | null;\n /** Current visibility; defaults to PRIVATE when the entity is unlinked. */\n currentVisibility?: VibeVisibility | null;\n /**\n * Apollo query names to refetch after a successful mutation. The Vibe-detail\n * linked-entity panels (GetVibeLinkedEntities) are always refetched on top of\n * whatever the host list page passes here so both directions stay in sync.\n */\n refetchQueries?: string[];\n /** Extra classes for the trigger button. */\n className?: string;\n}\n\n/**\n * Per-entity \"Linked Vibe\" control for detail headers and list rows. Shows the\n * vibe an entity is currently mapped to (resolved by name) and lets the user\n * map / re-map / unmap it and flip its visibility — the reverse direction of the\n * Vibe-detail linked-entity tabs. Operates on EXISTING entities via the generic\n * linkEntityToVibe / unlinkEntityFromVibe / updateEntityVisibility mutations, so\n * it works uniformly across every owned entity type (Action, Tunnel, VibeDeck,\n * AiPrompt, AiContext, AgentInstructionFile).\n *\n * Distinct from `LinkToVibePanel`, which is the create-form control for choosing\n * a link at creation time.\n */\nexport function EntityVibeLinkControl({\n entityType,\n entityId,\n currentVibeId,\n currentVisibility,\n refetchQueries,\n className = '',\n}: EntityVibeLinkControlProps) {\n const tr = useTr();\n const [open, setOpen] = useState(false);\n const [draftVibeId, setDraftVibeId] = useState<string | null>(currentVibeId);\n const [draftVisibility, setDraftVisibility] = useState<VibeVisibility>(\n currentVisibility ?? VibeVisibility.SharedWithVibe\n );\n const [saving, setSaving] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const containerRef = useRef<HTMLDivElement | null>(null);\n\n const { data, loading: vibesLoading } = useListVibesQuery({\n variables: { pagination: { limit: 200 } },\n fetchPolicy: 'cache-first',\n });\n\n const vibeOptions = useMemo(\n () =>\n (data?.vibes?.items ?? []).map((v: { id: string; name: string }) => ({\n id: v.id,\n name: v.name,\n })),\n [data?.vibes?.items]\n );\n\n const linkedVibeName = useMemo(() => {\n if (!currentVibeId) return null;\n return (\n vibeOptions.find((v: { id: string; name: string }) => v.id === currentVibeId)?.name ?? null\n );\n }, [currentVibeId, vibeOptions]);\n\n // Always refresh the Vibe-detail panels; merge in any host-supplied list\n // queries (deduped) so the source list reflects the new linkage too.\n const mergedRefetch = useMemo(\n () => Array.from(new Set(['GetVibeLinkedEntities', ...(refetchQueries ?? [])])),\n [refetchQueries]\n );\n\n const [linkEntity] = useLinkEntityToVibeMutation();\n const [unlinkEntity] = useUnlinkEntityFromVibeMutation();\n const [updateVisibility] = useUpdateEntityVisibilityMutation();\n\n // Re-seed drafts whenever the popover opens or the source values change so the\n // form always starts from the entity's live state.\n useEffect(() => {\n if (open) {\n setDraftVibeId(currentVibeId);\n setDraftVisibility(currentVisibility ?? VibeVisibility.SharedWithVibe);\n setError(null);\n }\n }, [open, currentVibeId, currentVisibility]);\n\n useEffect(() => {\n if (!open) return;\n const onDocClick = (e: MouseEvent) => {\n if (!containerRef.current?.contains(e.target as Node)) setOpen(false);\n };\n document.addEventListener('mousedown', onDocClick);\n return () => document.removeEventListener('mousedown', onDocClick);\n }, [open]);\n\n const isLinked = Boolean(currentVibeId);\n const isShared = currentVisibility === VibeVisibility.SharedWithVibe;\n\n const runMutation = async (fn: () => Promise<unknown>) => {\n setSaving(true);\n setError(null);\n try {\n await fn();\n setOpen(false);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr('vibecontrols.entityVibeLink.error', 'Could not update the vibe link.')\n );\n } finally {\n setSaving(false);\n }\n };\n\n const handleSave = async () => {\n const mutationOptions = { refetchQueries: mergedRefetch, awaitRefetchQueries: true };\n // No vibe selected → unlink (only meaningful when currently linked).\n if (!draftVibeId) {\n if (currentVibeId) {\n await runMutation(() =>\n unlinkEntity({ variables: { entityType, entityId }, ...mutationOptions })\n );\n } else {\n setOpen(false);\n }\n return;\n }\n // Vibe added or changed → (re)link with the chosen visibility.\n if (draftVibeId !== currentVibeId) {\n await runMutation(() =>\n linkEntity({\n variables: {\n input: { entityType, entityId, vibeId: draftVibeId, visibility: draftVisibility },\n },\n ...mutationOptions,\n })\n );\n return;\n }\n // Same vibe, visibility changed → visibility-only update. Compare against\n // the effective current visibility (matching how the draft is seeded) so an\n // unchanged selection on an entity whose visibility arrived nullish does not\n // fire a redundant no-op mutation.\n const effectiveVisibility = currentVisibility ?? VibeVisibility.SharedWithVibe;\n if (draftVisibility !== effectiveVisibility) {\n await runMutation(() =>\n updateVisibility({\n variables: { entityType, entityId, visibility: draftVisibility },\n ...mutationOptions,\n })\n );\n return;\n }\n // Nothing changed.\n setOpen(false);\n };\n\n const handleUnlink = async () => {\n await runMutation(() =>\n unlinkEntity({\n variables: { entityType, entityId },\n refetchQueries: mergedRefetch,\n awaitRefetchQueries: true,\n })\n );\n };\n\n return (\n <div className=\"relative\" ref={containerRef}>\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n setOpen((v) => !v);\n }}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n title={\n isLinked\n ? tr('vibecontrols.entityVibeLink.linkedTo', 'Linked to a vibe — click to manage')\n : tr('vibecontrols.entityVibeLink.notLinked', 'Link this to a vibe')\n }\n className={`inline-flex max-w-[180px] items-center gap-1.5 rounded-md border px-2 py-1 text-xs transition-colors ${\n isLinked\n ? 'border-action-primary-bg/40 bg-action-primary-bg/10 text-action-primary-bg hover:bg-action-primary-bg/15'\n : 'border-border-default text-text-secondary hover:bg-bg-elevated'\n } ${className}`}\n >\n <Link2 size={13} className=\"flex-shrink-0\" />\n <span className=\"truncate\">\n {isLinked\n ? (linkedVibeName ?? tr('vibecontrols.entityVibeLink.aVibe', 'Linked vibe'))\n : tr('vibecontrols.entityVibeLink.link', 'Link to vibe')}\n </span>\n {isLinked && (\n <span\n className={`ml-0.5 inline-block size-1.5 flex-shrink-0 rounded-full ${\n isShared ? 'bg-status-success-text' : 'bg-text-tertiary'\n }`}\n aria-hidden\n />\n )}\n </button>\n\n {open && (\n <div\n role=\"dialog\"\n aria-label={tr('vibecontrols.entityVibeLink.manage', 'Manage vibe link')}\n onClick={(e) => e.stopPropagation()}\n className=\"absolute right-0 z-30 mt-1 w-72 rounded-lg border border-border-default bg-bg-elevated p-3 shadow-lg\"\n >\n <p className=\"mb-2 text-xs font-medium text-text-secondary\">\n {tr('vibecontrols.entityVibeLink.title', 'Linked vibe')}\n </p>\n\n <select\n value={draftVibeId ?? ''}\n disabled={saving}\n onChange={(e) => setDraftVibeId(e.target.value === '' ? null : e.target.value)}\n aria-label={tr('vibecontrols.entityVibeLink.pickAria', 'Pick a vibe to link')}\n className=\"w-full rounded-md border border-border-default bg-bg-sunken px-2.5 py-1.5 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-action-primary-bg\"\n >\n <option value=\"\">\n {vibesLoading\n ? tr('vibecontrols.entityVibeLink.loading', 'Loading vibes…')\n : tr('vibecontrols.entityVibeLink.none', 'Not linked to any vibe')}\n </option>\n {vibeOptions.map((opt: { id: string; name: string }) => (\n <option key={opt.id} value={opt.id}>\n {opt.name}\n </option>\n ))}\n </select>\n\n {draftVibeId && (\n <div\n role=\"radiogroup\"\n aria-label={tr('vibecontrols.entityVibeLink.visibility', 'Who can see it')}\n className=\"mt-2.5 space-y-1.5\"\n >\n <label className=\"flex cursor-pointer items-start gap-2 text-xs text-text-primary\">\n <input\n type=\"radio\"\n name={`entity-vibe-visibility-${entityId}`}\n value={VibeVisibility.SharedWithVibe}\n checked={draftVisibility === VibeVisibility.SharedWithVibe}\n disabled={saving}\n onChange={() => setDraftVisibility(VibeVisibility.SharedWithVibe)}\n className=\"mt-0.5\"\n />\n <span>\n {tr(\n 'vibecontrols.entityVibeLink.shared',\n 'Shared with vibe — visible to everyone with access'\n )}\n </span>\n </label>\n <label className=\"flex cursor-pointer items-start gap-2 text-xs text-text-primary\">\n <input\n type=\"radio\"\n name={`entity-vibe-visibility-${entityId}`}\n value={VibeVisibility.Private}\n checked={draftVisibility === VibeVisibility.Private}\n disabled={saving}\n onChange={() => setDraftVisibility(VibeVisibility.Private)}\n className=\"mt-0.5\"\n />\n <span>{tr('vibecontrols.entityVibeLink.private', 'Private to me')}</span>\n </label>\n </div>\n )}\n\n {error && <p className=\"mt-2 text-xs text-status-error-text\">{error}</p>}\n\n <div className=\"mt-3 flex items-center justify-between gap-2\">\n {isLinked ? (\n <button\n type=\"button\"\n onClick={handleUnlink}\n disabled={saving}\n className=\"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs text-status-error-text hover:bg-status-error-bg/20 disabled:opacity-50\"\n >\n <Unlink size={13} />\n {tr('vibecontrols.entityVibeLink.unlink', 'Unlink')}\n </button>\n ) : (\n <span />\n )}\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n disabled={saving}\n className=\"rounded-md px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken disabled:opacity-50\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n onClick={handleSave}\n disabled={saving}\n className=\"inline-flex items-center gap-1 rounded-md bg-action-primary-bg px-2.5 py-1 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg-hover disabled:opacity-50\"\n >\n {saving ? <Loader2 size={13} className=\"animate-spin\" /> : <Check size={13} />}\n {tr('common.save', 'Save')}\n </button>\n </div>\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -16,6 +16,7 @@ import "./StatCard.js";
16
16
  import "./InstallTabs.js";
17
17
  import "./AgentSetupBanner.js";
18
18
  import "./LinkToVibePanel.js";
19
+ import "./EntityVibeLinkControl.js";
19
20
  import "./GetHelpButton.js";
20
21
  import "./SearchableSelect.js";
21
22
  import "./SessionNotificationBanner.js";
@@ -1,14 +1,15 @@
1
1
  import { useTr as e } from "../../shared/hooks/useTr.js";
2
- import { TunnelStatus as t } from "../../generated/wspace-types.js";
3
- import { TunnelStatusBadge as n } from "./TunnelStatusBadge.js";
4
- import { ProviderBadge as r } from "./ProviderBadge.js";
5
- import { ExternalLink as i, Loader2 as a, Play as o, Square as s, Trash2 as c } from "lucide-react";
6
- import { motion as l } from "framer-motion";
7
- import { jsx as u, jsxs as d } from "react/jsx-runtime";
2
+ import { TunnelStatus as t, VibeEntityType as n } from "../../generated/wspace-types.js";
3
+ import { EntityVibeLinkControl as r } from "../shared/EntityVibeLinkControl.js";
4
+ import { TunnelStatusBadge as i } from "./TunnelStatusBadge.js";
5
+ import { ProviderBadge as a } from "./ProviderBadge.js";
6
+ import { ExternalLink as o, Loader2 as s, Play as c, Square as l, Trash2 as u } from "lucide-react";
7
+ import { motion as d } from "framer-motion";
8
+ import { jsx as f, jsxs as p } from "react/jsx-runtime";
8
9
  //#region src/components/tunnels/TunnelCard.tsx
9
- function f({ tunnel: f, isPending: p = !1, onOpen: m, onStart: h, onStop: g, onDelete: _ }) {
10
- let v = e(), y = !p && f.status !== t.Active && f.status !== t.Provisioning, b = !p && f.status === t.Active, x = f.primaryDomain?.hostname, S = x ? `${f.protocol.toLowerCase() === "tcp" ? "tcp" : "https"}://${x}` : null;
11
- return /* @__PURE__ */ d(l.div, {
10
+ function m({ tunnel: m, isPending: h = !1, onOpen: g, onStart: _, onStop: v, onDelete: y }) {
11
+ let b = e(), x = !h && m.status !== t.Active && m.status !== t.Provisioning, S = !h && m.status === t.Active, C = m.primaryDomain?.hostname, w = C ? `${m.protocol.toLowerCase() === "tcp" ? "tcp" : "https"}://${C}` : null;
12
+ return /* @__PURE__ */ p(d.div, {
12
13
  initial: {
13
14
  opacity: 0,
14
15
  y: 4
@@ -18,85 +19,92 @@ function f({ tunnel: f, isPending: p = !1, onOpen: m, onStart: h, onStop: g, onD
18
19
  y: 0
19
20
  },
20
21
  className: "rounded-lg border border-border-subtle bg-bg-surface p-4 hover:border-border-default transition-colors",
21
- children: [/* @__PURE__ */ d("div", {
22
+ children: [/* @__PURE__ */ p("div", {
22
23
  className: "flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3",
23
- children: [/* @__PURE__ */ d("button", {
24
+ children: [/* @__PURE__ */ p("button", {
24
25
  type: "button",
25
- onClick: () => m(f.id),
26
+ onClick: () => g(m.id),
26
27
  className: "flex-1 text-left min-w-0",
27
28
  children: [
28
- /* @__PURE__ */ d("div", {
29
+ /* @__PURE__ */ p("div", {
29
30
  className: "flex flex-wrap items-center gap-2 mb-1",
30
31
  children: [
31
- /* @__PURE__ */ u("h3", {
32
+ /* @__PURE__ */ f("h3", {
32
33
  className: "text-base font-semibold text-text-primary truncate",
33
- children: f.name
34
+ children: m.name
34
35
  }),
35
- /* @__PURE__ */ u(n, { status: f.status }),
36
- /* @__PURE__ */ u(r, { providerKey: f.providerKey })
36
+ /* @__PURE__ */ f(i, { status: m.status }),
37
+ /* @__PURE__ */ f(a, { providerKey: m.providerKey })
37
38
  ]
38
39
  }),
39
- S ? /* @__PURE__ */ u("p", {
40
+ w ? /* @__PURE__ */ f("p", {
40
41
  className: "text-sm text-text-secondary truncate",
41
- children: S
42
- }) : /* @__PURE__ */ u("p", {
42
+ children: w
43
+ }) : /* @__PURE__ */ f("p", {
43
44
  className: "text-sm text-text-muted",
44
- children: v("vibecontrols.tunnels.noDomain", "No domain attached")
45
+ children: b("vibecontrols.tunnels.noDomain", "No domain attached")
45
46
  }),
46
- /* @__PURE__ */ d("p", {
47
+ /* @__PURE__ */ p("p", {
47
48
  className: "text-xs text-text-muted mt-1",
48
49
  children: [
49
- f.protocol,
50
+ m.protocol,
50
51
  ":",
51
- f.localPort,
52
+ m.localPort,
52
53
  " on ",
53
- f.localHost
54
+ m.localHost
54
55
  ]
55
56
  })
56
57
  ]
57
- }), /* @__PURE__ */ d("div", {
58
+ }), /* @__PURE__ */ p("div", {
58
59
  className: "flex items-center gap-2 flex-shrink-0 flex-wrap",
59
60
  children: [
60
- S && /* @__PURE__ */ d("a", {
61
- href: S,
61
+ w && /* @__PURE__ */ p("a", {
62
+ href: w,
62
63
  target: "_blank",
63
64
  rel: "noreferrer",
64
65
  className: "inline-flex items-center rounded-md border border-border-default px-3 py-1.5 text-xs text-text-primary hover:bg-bg-elevated",
65
66
  onClick: (e) => e.stopPropagation(),
66
- children: [/* @__PURE__ */ u(i, { className: "size-3.5 mr-1" }), v("vibecontrols.tunnels.open", "Open")]
67
+ children: [/* @__PURE__ */ f(o, { className: "size-3.5 mr-1" }), b("vibecontrols.tunnels.open", "Open")]
67
68
  }),
68
- p && /* @__PURE__ */ d("span", {
69
+ h && /* @__PURE__ */ p("span", {
69
70
  className: "inline-flex items-center rounded-md border border-border-default px-3 py-1.5 text-xs text-text-muted",
70
- children: [/* @__PURE__ */ u(a, { className: "size-3.5 mr-1 animate-spin" }), f.status === "ACTIVE" ? v("vibecontrols.tunnels.stopping", "Stopping…") : v("vibecontrols.tunnels.starting", "Starting…")]
71
+ children: [/* @__PURE__ */ f(s, { className: "size-3.5 mr-1 animate-spin" }), m.status === "ACTIVE" ? b("vibecontrols.tunnels.stopping", "Stopping…") : b("vibecontrols.tunnels.starting", "Starting…")]
71
72
  }),
72
- y && /* @__PURE__ */ d("button", {
73
+ x && /* @__PURE__ */ p("button", {
73
74
  type: "button",
74
- onClick: () => h(f.id),
75
+ onClick: () => _(m.id),
75
76
  className: "inline-flex items-center rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-fg hover:bg-action-primary-bg-hover",
76
- children: [/* @__PURE__ */ u(o, { className: "size-3.5 mr-1" }), v("vibecontrols.tunnels.start", "Start")]
77
+ children: [/* @__PURE__ */ f(c, { className: "size-3.5 mr-1" }), b("vibecontrols.tunnels.start", "Start")]
77
78
  }),
78
- b && /* @__PURE__ */ d("button", {
79
+ S && /* @__PURE__ */ p("button", {
79
80
  type: "button",
80
- onClick: () => g(f.id),
81
+ onClick: () => v(m.id),
81
82
  className: "inline-flex items-center rounded-md border border-border-default px-3 py-1.5 text-xs text-text-primary hover:bg-bg-elevated",
82
- children: [/* @__PURE__ */ u(s, { className: "size-3.5 mr-1" }), v("vibecontrols.tunnels.stop", "Stop")]
83
+ children: [/* @__PURE__ */ f(l, { className: "size-3.5 mr-1" }), b("vibecontrols.tunnels.stop", "Stop")]
83
84
  }),
84
- /* @__PURE__ */ u("button", {
85
+ /* @__PURE__ */ f(r, {
86
+ entityType: n.Tunnel,
87
+ entityId: m.id,
88
+ currentVibeId: m.vibeId ?? null,
89
+ currentVisibility: m.visibility ?? null,
90
+ refetchQueries: ["ListTunnels"]
91
+ }),
92
+ /* @__PURE__ */ f("button", {
85
93
  type: "button",
86
- onClick: () => _(f.id, f.name),
94
+ onClick: () => y(m.id, m.name),
87
95
  className: "inline-flex items-center rounded-md border border-border-default px-2 py-1.5 text-xs text-status-error-fg hover:bg-status-error-bg",
88
96
  "aria-label": "Delete tunnel",
89
- children: /* @__PURE__ */ u(c, { className: "size-3.5" })
97
+ children: /* @__PURE__ */ f(u, { className: "size-3.5" })
90
98
  })
91
99
  ]
92
100
  })]
93
- }), f.lastError && /* @__PURE__ */ u("div", {
101
+ }), m.lastError && /* @__PURE__ */ f("div", {
94
102
  className: "mt-3 rounded-md bg-status-error-bg px-3 py-2 text-xs text-status-error-fg",
95
- children: f.lastError
103
+ children: m.lastError
96
104
  })]
97
105
  });
98
106
  }
99
107
  //#endregion
100
- export { f as TunnelCard };
108
+ export { m as TunnelCard };
101
109
 
102
110
  //# sourceMappingURL=TunnelCard.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TunnelCard.js","names":[],"sources":["../../../src/components/tunnels/TunnelCard.tsx"],"sourcesContent":["import { motion } from 'framer-motion';\nimport { Loader2, Play, Square, Trash2, ExternalLink } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\n\nimport type { ListTunnelsQuery } from '@/generated/wspace-operations';\nimport { TunnelStatus } from '@/generated/wspace-types';\nimport { TunnelStatusBadge } from './TunnelStatusBadge';\nimport { ProviderBadge } from './ProviderBadge';\n\ntype TunnelRow = NonNullable<ListTunnelsQuery['tunnels']>['items'][number];\n\ninterface TunnelCardProps {\n tunnel: TunnelRow;\n isPending?: boolean;\n onOpen: (id: string) => void;\n onStart: (id: string) => void;\n onStop: (id: string) => void;\n onDelete: (id: string, name: string) => void;\n}\n\nexport function TunnelCard({\n tunnel,\n isPending = false,\n onOpen,\n onStart,\n onStop,\n onDelete,\n}: TunnelCardProps) {\n const tr = useTr();\n const canStart =\n !isPending &&\n tunnel.status !== TunnelStatus.Active &&\n tunnel.status !== TunnelStatus.Provisioning;\n const canStop = !isPending && tunnel.status === TunnelStatus.Active;\n const hostname = tunnel.primaryDomain?.hostname;\n const publicUrl = hostname\n ? `${tunnel.protocol.toLowerCase() === 'tcp' ? 'tcp' : 'https'}://${hostname}`\n : null;\n\n return (\n <motion.div\n initial={{ opacity: 0, y: 4 }}\n animate={{ opacity: 1, y: 0 }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 hover:border-border-default transition-colors\"\n >\n <div className=\"flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3\">\n <button\n type=\"button\"\n onClick={() => onOpen(tunnel.id)}\n className=\"flex-1 text-left min-w-0\"\n >\n <div className=\"flex flex-wrap items-center gap-2 mb-1\">\n <h3 className=\"text-base font-semibold text-text-primary truncate\">{tunnel.name}</h3>\n <TunnelStatusBadge status={tunnel.status} />\n <ProviderBadge providerKey={tunnel.providerKey} />\n </div>\n {publicUrl ? (\n <p className=\"text-sm text-text-secondary truncate\">{publicUrl}</p>\n ) : (\n <p className=\"text-sm text-text-muted\">\n {tr('vibecontrols.tunnels.noDomain', 'No domain attached')}\n </p>\n )}\n <p className=\"text-xs text-text-muted mt-1\">\n {tunnel.protocol}:{tunnel.localPort} on {tunnel.localHost}\n </p>\n </button>\n\n <div className=\"flex items-center gap-2 flex-shrink-0 flex-wrap\">\n {publicUrl && (\n <a\n href={publicUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"inline-flex items-center rounded-md border border-border-default px-3 py-1.5 text-xs text-text-primary hover:bg-bg-elevated\"\n onClick={(e) => e.stopPropagation()}\n >\n <ExternalLink className=\"size-3.5 mr-1\" />\n {tr('vibecontrols.tunnels.open', 'Open')}\n </a>\n )}\n {isPending && (\n <span className=\"inline-flex items-center rounded-md border border-border-default px-3 py-1.5 text-xs text-text-muted\">\n <Loader2 className=\"size-3.5 mr-1 animate-spin\" />\n {tunnel.status === 'ACTIVE'\n ? tr('vibecontrols.tunnels.stopping', 'Stopping…')\n : tr('vibecontrols.tunnels.starting', 'Starting…')}\n </span>\n )}\n {canStart && (\n <button\n type=\"button\"\n onClick={() => onStart(tunnel.id)}\n className=\"inline-flex items-center rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-fg hover:bg-action-primary-bg-hover\"\n >\n <Play className=\"size-3.5 mr-1\" />\n {tr('vibecontrols.tunnels.start', 'Start')}\n </button>\n )}\n {canStop && (\n <button\n type=\"button\"\n onClick={() => onStop(tunnel.id)}\n className=\"inline-flex items-center rounded-md border border-border-default px-3 py-1.5 text-xs text-text-primary hover:bg-bg-elevated\"\n >\n <Square className=\"size-3.5 mr-1\" />\n {tr('vibecontrols.tunnels.stop', 'Stop')}\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => onDelete(tunnel.id, tunnel.name)}\n className=\"inline-flex items-center rounded-md border border-border-default px-2 py-1.5 text-xs text-status-error-fg hover:bg-status-error-bg\"\n aria-label=\"Delete tunnel\"\n >\n <Trash2 className=\"size-3.5\" />\n </button>\n </div>\n </div>\n {tunnel.lastError && (\n <div className=\"mt-3 rounded-md bg-status-error-bg px-3 py-2 text-xs text-status-error-fg\">\n {tunnel.lastError}\n </div>\n )}\n </motion.div>\n );\n}\n"],"mappings":";;;;;;;;AAoBA,SAAgB,EAAW,EACzB,WACA,eAAY,IACZ,WACA,YACA,WACA,eACkB;CAClB,IAAM,IAAK,GAAO,EACZ,IACJ,CAAC,KACD,EAAO,WAAW,EAAa,UAC/B,EAAO,WAAW,EAAa,cAC3B,IAAU,CAAC,KAAa,EAAO,WAAW,EAAa,QACvD,IAAW,EAAO,eAAe,UACjC,IAAY,IACd,GAAG,EAAO,SAAS,aAAa,KAAK,QAAQ,QAAQ,QAAQ,KAAK,MAClE;AAEJ,QACE,kBAAC,EAAO,KAAR;EACE,SAAS;GAAE,SAAS;GAAG,GAAG;GAAG;EAC7B,SAAS;GAAE,SAAS;GAAG,GAAG;GAAG;EAC7B,WAAU;YAHZ,CAKE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAO,EAAO,GAAG;IAChC,WAAU;cAHZ;KAKE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,MAAD;QAAI,WAAU;kBAAsD,EAAO;QAAU,CAAA;OACrF,kBAAC,GAAD,EAAmB,QAAQ,EAAO,QAAU,CAAA;OAC5C,kBAAC,GAAD,EAAe,aAAa,EAAO,aAAe,CAAA;OAC9C;;KACL,IACC,kBAAC,KAAD;MAAG,WAAU;gBAAwC;MAAc,CAAA,GAEnE,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAG,iCAAiC,qBAAqB;MACxD,CAAA;KAEN,kBAAC,KAAD;MAAG,WAAU;gBAAb;OACG,EAAO;OAAS;OAAE,EAAO;OAAU;OAAK,EAAO;OAC9C;;KACG;OAET,kBAAC,OAAD;IAAK,WAAU;cAAf;KACG,KACC,kBAAC,KAAD;MACE,MAAM;MACN,QAAO;MACP,KAAI;MACJ,WAAU;MACV,UAAU,MAAM,EAAE,iBAAiB;gBALrC,CAOE,kBAAC,GAAD,EAAc,WAAU,iBAAkB,CAAA,EACzC,EAAG,6BAA6B,OAAO,CACtC;;KAEL,KACC,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACE,kBAAC,GAAD,EAAS,WAAU,8BAA+B,CAAA,EACjD,EAAO,WAAW,WACf,EAAG,iCAAiC,YAAY,GAChD,EAAG,iCAAiC,YAAY,CAC/C;;KAER,KACC,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAQ,EAAO,GAAG;MACjC,WAAU;gBAHZ,CAKE,kBAAC,GAAD,EAAM,WAAU,iBAAkB,CAAA,EACjC,EAAG,8BAA8B,QAAQ,CACnC;;KAEV,KACC,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAO,EAAO,GAAG;MAChC,WAAU;gBAHZ,CAKE,kBAAC,GAAD,EAAQ,WAAU,iBAAkB,CAAA,EACnC,EAAG,6BAA6B,OAAO,CACjC;;KAEX,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAS,EAAO,IAAI,EAAO,KAAK;MAC/C,WAAU;MACV,cAAW;gBAEX,kBAAC,GAAD,EAAQ,WAAU,YAAa,CAAA;MACxB,CAAA;KACL;MACF;MACL,EAAO,aACN,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAO;GACJ,CAAA,CAEG"}
1
+ {"version":3,"file":"TunnelCard.js","names":[],"sources":["../../../src/components/tunnels/TunnelCard.tsx"],"sourcesContent":["import { motion } from 'framer-motion';\nimport { Loader2, Play, Square, Trash2, ExternalLink } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\n\nimport type { ListTunnelsQuery } from '@/generated/wspace-operations';\nimport { TunnelStatus, VibeEntityType } from '@/generated/wspace-types';\nimport { TunnelStatusBadge } from './TunnelStatusBadge';\nimport { ProviderBadge } from './ProviderBadge';\nimport { EntityVibeLinkControl } from '../shared/EntityVibeLinkControl';\n\ntype TunnelRow = NonNullable<ListTunnelsQuery['tunnels']>['items'][number];\n\ninterface TunnelCardProps {\n tunnel: TunnelRow;\n isPending?: boolean;\n onOpen: (id: string) => void;\n onStart: (id: string) => void;\n onStop: (id: string) => void;\n onDelete: (id: string, name: string) => void;\n}\n\nexport function TunnelCard({\n tunnel,\n isPending = false,\n onOpen,\n onStart,\n onStop,\n onDelete,\n}: TunnelCardProps) {\n const tr = useTr();\n const canStart =\n !isPending &&\n tunnel.status !== TunnelStatus.Active &&\n tunnel.status !== TunnelStatus.Provisioning;\n const canStop = !isPending && tunnel.status === TunnelStatus.Active;\n const hostname = tunnel.primaryDomain?.hostname;\n const publicUrl = hostname\n ? `${tunnel.protocol.toLowerCase() === 'tcp' ? 'tcp' : 'https'}://${hostname}`\n : null;\n\n return (\n <motion.div\n initial={{ opacity: 0, y: 4 }}\n animate={{ opacity: 1, y: 0 }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 hover:border-border-default transition-colors\"\n >\n <div className=\"flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3\">\n <button\n type=\"button\"\n onClick={() => onOpen(tunnel.id)}\n className=\"flex-1 text-left min-w-0\"\n >\n <div className=\"flex flex-wrap items-center gap-2 mb-1\">\n <h3 className=\"text-base font-semibold text-text-primary truncate\">{tunnel.name}</h3>\n <TunnelStatusBadge status={tunnel.status} />\n <ProviderBadge providerKey={tunnel.providerKey} />\n </div>\n {publicUrl ? (\n <p className=\"text-sm text-text-secondary truncate\">{publicUrl}</p>\n ) : (\n <p className=\"text-sm text-text-muted\">\n {tr('vibecontrols.tunnels.noDomain', 'No domain attached')}\n </p>\n )}\n <p className=\"text-xs text-text-muted mt-1\">\n {tunnel.protocol}:{tunnel.localPort} on {tunnel.localHost}\n </p>\n </button>\n\n <div className=\"flex items-center gap-2 flex-shrink-0 flex-wrap\">\n {publicUrl && (\n <a\n href={publicUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"inline-flex items-center rounded-md border border-border-default px-3 py-1.5 text-xs text-text-primary hover:bg-bg-elevated\"\n onClick={(e) => e.stopPropagation()}\n >\n <ExternalLink className=\"size-3.5 mr-1\" />\n {tr('vibecontrols.tunnels.open', 'Open')}\n </a>\n )}\n {isPending && (\n <span className=\"inline-flex items-center rounded-md border border-border-default px-3 py-1.5 text-xs text-text-muted\">\n <Loader2 className=\"size-3.5 mr-1 animate-spin\" />\n {tunnel.status === 'ACTIVE'\n ? tr('vibecontrols.tunnels.stopping', 'Stopping…')\n : tr('vibecontrols.tunnels.starting', 'Starting…')}\n </span>\n )}\n {canStart && (\n <button\n type=\"button\"\n onClick={() => onStart(tunnel.id)}\n className=\"inline-flex items-center rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-fg hover:bg-action-primary-bg-hover\"\n >\n <Play className=\"size-3.5 mr-1\" />\n {tr('vibecontrols.tunnels.start', 'Start')}\n </button>\n )}\n {canStop && (\n <button\n type=\"button\"\n onClick={() => onStop(tunnel.id)}\n className=\"inline-flex items-center rounded-md border border-border-default px-3 py-1.5 text-xs text-text-primary hover:bg-bg-elevated\"\n >\n <Square className=\"size-3.5 mr-1\" />\n {tr('vibecontrols.tunnels.stop', 'Stop')}\n </button>\n )}\n <EntityVibeLinkControl\n entityType={VibeEntityType.Tunnel}\n entityId={tunnel.id}\n currentVibeId={tunnel.vibeId ?? null}\n currentVisibility={tunnel.visibility ?? null}\n refetchQueries={['ListTunnels']}\n />\n <button\n type=\"button\"\n onClick={() => onDelete(tunnel.id, tunnel.name)}\n className=\"inline-flex items-center rounded-md border border-border-default px-2 py-1.5 text-xs text-status-error-fg hover:bg-status-error-bg\"\n aria-label=\"Delete tunnel\"\n >\n <Trash2 className=\"size-3.5\" />\n </button>\n </div>\n </div>\n {tunnel.lastError && (\n <div className=\"mt-3 rounded-md bg-status-error-bg px-3 py-2 text-xs text-status-error-fg\">\n {tunnel.lastError}\n </div>\n )}\n </motion.div>\n );\n}\n"],"mappings":";;;;;;;;;AAqBA,SAAgB,EAAW,EACzB,WACA,eAAY,IACZ,WACA,YACA,WACA,eACkB;CAClB,IAAM,IAAK,GAAO,EACZ,IACJ,CAAC,KACD,EAAO,WAAW,EAAa,UAC/B,EAAO,WAAW,EAAa,cAC3B,IAAU,CAAC,KAAa,EAAO,WAAW,EAAa,QACvD,IAAW,EAAO,eAAe,UACjC,IAAY,IACd,GAAG,EAAO,SAAS,aAAa,KAAK,QAAQ,QAAQ,QAAQ,KAAK,MAClE;AAEJ,QACE,kBAAC,EAAO,KAAR;EACE,SAAS;GAAE,SAAS;GAAG,GAAG;GAAG;EAC7B,SAAS;GAAE,SAAS;GAAG,GAAG;GAAG;EAC7B,WAAU;YAHZ,CAKE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAO,EAAO,GAAG;IAChC,WAAU;cAHZ;KAKE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,MAAD;QAAI,WAAU;kBAAsD,EAAO;QAAU,CAAA;OACrF,kBAAC,GAAD,EAAmB,QAAQ,EAAO,QAAU,CAAA;OAC5C,kBAAC,GAAD,EAAe,aAAa,EAAO,aAAe,CAAA;OAC9C;;KACL,IACC,kBAAC,KAAD;MAAG,WAAU;gBAAwC;MAAc,CAAA,GAEnE,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAG,iCAAiC,qBAAqB;MACxD,CAAA;KAEN,kBAAC,KAAD;MAAG,WAAU;gBAAb;OACG,EAAO;OAAS;OAAE,EAAO;OAAU;OAAK,EAAO;OAC9C;;KACG;OAET,kBAAC,OAAD;IAAK,WAAU;cAAf;KACG,KACC,kBAAC,KAAD;MACE,MAAM;MACN,QAAO;MACP,KAAI;MACJ,WAAU;MACV,UAAU,MAAM,EAAE,iBAAiB;gBALrC,CAOE,kBAAC,GAAD,EAAc,WAAU,iBAAkB,CAAA,EACzC,EAAG,6BAA6B,OAAO,CACtC;;KAEL,KACC,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACE,kBAAC,GAAD,EAAS,WAAU,8BAA+B,CAAA,EACjD,EAAO,WAAW,WACf,EAAG,iCAAiC,YAAY,GAChD,EAAG,iCAAiC,YAAY,CAC/C;;KAER,KACC,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAQ,EAAO,GAAG;MACjC,WAAU;gBAHZ,CAKE,kBAAC,GAAD,EAAM,WAAU,iBAAkB,CAAA,EACjC,EAAG,8BAA8B,QAAQ,CACnC;;KAEV,KACC,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAO,EAAO,GAAG;MAChC,WAAU;gBAHZ,CAKE,kBAAC,GAAD,EAAQ,WAAU,iBAAkB,CAAA,EACnC,EAAG,6BAA6B,OAAO,CACjC;;KAEX,kBAAC,GAAD;MACE,YAAY,EAAe;MAC3B,UAAU,EAAO;MACjB,eAAe,EAAO,UAAU;MAChC,mBAAmB,EAAO,cAAc;MACxC,gBAAgB,CAAC,cAAc;MAC/B,CAAA;KACF,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAS,EAAO,IAAI,EAAO,KAAK;MAC/C,WAAU;MACV,cAAW;gBAEX,kBAAC,GAAD,EAAQ,WAAU,YAAa,CAAA;MACxB,CAAA;KACL;MACF;MACL,EAAO,aACN,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAO;GACJ,CAAA,CAEG"}