@burdenoff/microfe-vibecontrols 2026.524.1 → 2026.524.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.
@@ -1,221 +1,262 @@
1
1
  import { useTr as e } from "../../shared/hooks/useTr.js";
2
- import { useAvailableTags as t, useGetEntityTags as n, useSearchTags as r, useTagEntity as i, useUntagEntity as a } from "../../hooks/useEntityTags.js";
3
- import { useCallback as o, useEffect as s, useMemo as c, useRef as l, useState as u } from "react";
4
- import { Loader2 as d, Plus as f, Search as p, Tag as m, X as h } from "lucide-react";
5
- import { jsx as g, jsxs as _ } from "react/jsx-runtime";
2
+ import { useAvailableTags as t, useCreateVibecontrolsTag as n, useGetEntityTags as r, useSearchTags as i, useTagEntity as a, useUntagEntity as ee } from "../../hooks/useEntityTags.js";
3
+ import { useCallback as o, useEffect as s, useMemo as c, useRef as te, useState as l } from "react";
4
+ import { Loader2 as u, Plus as d, Search as ne, Tag as re, X as f } from "lucide-react";
5
+ import { jsx as p, jsxs as m } from "react/jsx-runtime";
6
6
  //#region src/components/shared/EntityTagPicker.tsx
7
- var v = 4e3, y = 250;
8
- function b({ entityType: b, entityId: x, onDraftChange: S, readOnly: C = !1 }) {
9
- let w = e(), T = !x, [E, D] = u(!1), [O, k] = u(""), [A, j] = u(null), [M, N] = u([]);
7
+ var h = 4e3, ie = 250;
8
+ function g({ entityType: g, entityId: _, onDraftChange: ae, readOnly: v = !1 }) {
9
+ let y = e(), b = !_, [x, S] = l(!1), [C, w] = l(""), [oe, T] = l(""), [E, D] = l(null), [O, k] = l([]);
10
10
  s(() => {
11
- T && S?.([]);
12
- }, []);
13
- let { data: P, loading: F, refetch: I } = n(b, x ?? ""), { data: L, loading: R } = t(50, { skip: !E }), [z, { data: B, loading: V }] = r(), [H, { loading: U }] = i(), [W, { loading: G }] = a(), K = c(() => P?.vibecontrolsEntityTags ?? [], [P]), q = c(() => T ? M.map((e) => ({
11
+ b && ae?.(O.map((e) => e.id));
12
+ }, [b, O]);
13
+ let { data: A, loading: j, refetch: M } = r(g, _ ?? ""), { data: N, loading: P } = t(50, { skip: !x }), [F, { data: I, loading: L }] = i(), [R, { loading: z }] = a(), [B, { loading: V }] = ee(), [H, { loading: U }] = n(), W = c(() => A?.vibecontrolsEntityTags ?? [], [A]), G = c(() => b ? O.map((e) => ({
14
14
  key: e.id,
15
15
  tagId: e.id,
16
16
  label: e.label
17
- })) : K.map((e) => ({
17
+ })) : W.map((e) => ({
18
18
  key: e.id,
19
19
  tagId: e.tagId,
20
- label: e.tag?.label ?? w("vibecontrols.shared.entityTagPicker.untitledTag", "Untitled tag")
20
+ label: e.tag?.label ?? y("vibecontrols.shared.entityTagPicker.untitledTag", "Untitled tag")
21
21
  })), [
22
- T,
23
- M,
24
- K,
25
- w
26
- ]), J = c(() => new Set(q.map((e) => e.tagId)), [q]), Y = c(() => ((O ? B?.searchVibecontrolsTags : L?.availableVibecontrolsTags) ?? []).filter((e) => !J.has(e.id)), [
22
+ b,
27
23
  O,
28
- B,
29
- L,
30
- J
31
- ]);
24
+ W,
25
+ y
26
+ ]), K = c(() => new Set(G.map((e) => e.tagId)), [G]), q = c(() => (C ? I?.searchVibecontrolsTags : N?.availableVibecontrolsTags) ?? [], [
27
+ C,
28
+ I,
29
+ N
30
+ ]), J = c(() => q.filter((e) => !K.has(e.id)), [q, K]);
32
31
  s(() => {
33
- if (!A) return;
34
- let e = setTimeout(() => j(null), v);
32
+ if (!E) return;
33
+ let e = setTimeout(() => D(null), h);
35
34
  return () => clearTimeout(e);
36
- }, [A]);
37
- let X = l(null);
35
+ }, [E]);
36
+ let Y = te(null);
38
37
  s(() => () => {
39
- X.current && clearTimeout(X.current);
38
+ Y.current && clearTimeout(Y.current);
40
39
  }, []);
41
- let Z = o((e) => {
42
- k(e), X.current && clearTimeout(X.current), !(e.length < 2) && (X.current = setTimeout(() => {
43
- z({ variables: {
44
- query: e,
40
+ let X = o((e) => {
41
+ w(e), Y.current && clearTimeout(Y.current);
42
+ let t = e.trim();
43
+ if (t.length < 2) {
44
+ T("");
45
+ return;
46
+ }
47
+ Y.current = setTimeout(() => {
48
+ T(t), F({ variables: {
49
+ query: t,
45
50
  limit: 10
46
51
  } });
47
- }, y));
48
- }, [z]), Q = o(async (e) => {
49
- if (T) {
50
- N((t) => {
51
- if (t.some((t) => t.id === e.id)) return t;
52
- let n = [...t, e];
53
- return S?.(n.map((e) => e.id)), n;
54
- }), k(""), j(null);
52
+ }, ie);
53
+ }, [F]), Z = o(async (e) => {
54
+ if (b) {
55
+ k((t) => t.some((t) => t.id === e.id) ? t : [...t, e]), w(""), D(null);
55
56
  return;
56
57
  }
57
58
  try {
58
- await H({ variables: { input: {
59
- entityType: b,
60
- entityId: x ?? "",
59
+ await R({ variables: { input: {
60
+ entityType: g,
61
+ entityId: _ ?? "",
61
62
  tagId: e.id
62
- } } }), await I(), k(""), j(null);
63
+ } } }), await M(), w(""), D(null);
63
64
  } catch (e) {
64
- j(e instanceof Error ? e.message : w("vibecontrols.shared.entityTagPicker.failedToAddTag", "Failed to add tag"));
65
+ D(e instanceof Error ? e.message : y("vibecontrols.shared.entityTagPicker.failedToAddTag", "Failed to add tag"));
65
66
  }
66
67
  }, [
67
- T,
68
- S,
69
- H,
70
68
  b,
71
- x,
72
- I,
73
- w
74
- ]), $ = o(async (e) => {
75
- if (T) {
76
- N((t) => {
77
- let n = t.filter((t) => t.id !== e);
78
- return S?.(n.map((e) => e.id)), n;
79
- }), j(null);
69
+ R,
70
+ g,
71
+ _,
72
+ M,
73
+ y
74
+ ]), se = o(async (e) => {
75
+ if (b) {
76
+ k((t) => t.filter((t) => t.id !== e)), D(null);
80
77
  return;
81
78
  }
82
79
  try {
83
- await W({ variables: { input: {
84
- entityType: b,
85
- entityId: x ?? "",
80
+ await B({ variables: { input: {
81
+ entityType: g,
82
+ entityId: _ ?? "",
86
83
  tagId: e
87
- } } }), await I(), j(null);
84
+ } } }), await M(), D(null);
88
85
  } catch (e) {
89
- j(e instanceof Error ? e.message : w("vibecontrols.shared.entityTagPicker.failedToRemoveTag", "Failed to remove tag"));
86
+ D(e instanceof Error ? e.message : y("vibecontrols.shared.entityTagPicker.failedToRemoveTag", "Failed to remove tag"));
90
87
  }
91
88
  }, [
92
- T,
93
- S,
94
- W,
95
89
  b,
96
- x,
97
- I,
98
- w
99
- ]);
100
- return F && !T ? /* @__PURE__ */ _("div", {
90
+ B,
91
+ g,
92
+ _,
93
+ M,
94
+ y
95
+ ]), ce = o(async (e) => {
96
+ let t = e.trim();
97
+ if (t) try {
98
+ let e = (await H({
99
+ variables: { input: { label: t } },
100
+ refetchQueries: ["EntityTagPickerAvailableTags"]
101
+ })).data?.createVibecontrolsTag;
102
+ if (!e) throw Error(y("vibecontrols.shared.entityTagPicker.failedToCreateTag", "Failed to create tag"));
103
+ await Z({
104
+ id: e.id,
105
+ key: e.key,
106
+ label: e.label,
107
+ color: e.color ?? null
108
+ });
109
+ } catch (e) {
110
+ D(e instanceof Error ? e.message : y("vibecontrols.shared.entityTagPicker.failedToCreateTag", "Failed to create tag"));
111
+ }
112
+ }, [
113
+ H,
114
+ Z,
115
+ y
116
+ ]), Q = C.trim(), le = c(() => {
117
+ let e = Q.toLowerCase();
118
+ return e ? q.some((t) => t.label.toLowerCase() === e) || G.some((t) => t.label.toLowerCase() === e) : !1;
119
+ }, [
120
+ q,
121
+ G,
122
+ Q
123
+ ]), ue = oe === Q && !L, $ = !v && Q.length >= 2 && ue && !le;
124
+ return j && !b ? /* @__PURE__ */ m("div", {
101
125
  className: "flex items-center gap-2 text-text-secondary text-sm",
102
126
  role: "status",
103
127
  "aria-live": "polite",
104
- children: [/* @__PURE__ */ g(d, {
128
+ children: [/* @__PURE__ */ p(u, {
105
129
  className: "h-4 w-4 animate-spin",
106
130
  "aria-hidden": "true"
107
- }), w("vibecontrols.shared.entityTagPicker.loadingTags", "Loading tags...")]
108
- }) : /* @__PURE__ */ _("div", {
131
+ }), y("vibecontrols.shared.entityTagPicker.loadingTags", "Loading tags...")]
132
+ }) : /* @__PURE__ */ m("div", {
109
133
  className: "space-y-3",
110
134
  children: [
111
- A && /* @__PURE__ */ _("div", {
135
+ E && /* @__PURE__ */ m("div", {
112
136
  role: "alert",
113
137
  className: "flex items-start gap-2 rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700",
114
- children: [/* @__PURE__ */ g("span", {
138
+ children: [/* @__PURE__ */ p("span", {
115
139
  className: "flex-1",
116
- children: A
117
- }), /* @__PURE__ */ g("button", {
140
+ children: E
141
+ }), /* @__PURE__ */ p("button", {
118
142
  type: "button",
119
- onClick: () => j(null),
120
- "aria-label": w("vibecontrols.shared.entityTagPicker.dismissError", "Dismiss error"),
143
+ onClick: () => D(null),
144
+ "aria-label": y("vibecontrols.shared.entityTagPicker.dismissError", "Dismiss error"),
121
145
  className: "text-red-700 hover:text-red-900",
122
- children: /* @__PURE__ */ g(h, {
146
+ children: /* @__PURE__ */ p(f, {
123
147
  className: "h-3.5 w-3.5",
124
148
  "aria-hidden": "true"
125
149
  })
126
150
  })]
127
151
  }),
128
- /* @__PURE__ */ _("div", {
152
+ /* @__PURE__ */ m("div", {
129
153
  className: "flex flex-wrap gap-2",
130
- "aria-label": w("vibecontrols.shared.entityTagPicker.assignedTags", "Assigned tags"),
154
+ "aria-label": y("vibecontrols.shared.entityTagPicker.assignedTags", "Assigned tags"),
131
155
  children: [
132
- q.length === 0 && !E && /* @__PURE__ */ g("p", {
156
+ G.length === 0 && !x && /* @__PURE__ */ p("p", {
133
157
  className: "text-text-secondary text-sm",
134
- children: w("vibecontrols.shared.entityTagPicker.noTagsAssigned", "No tags assigned")
158
+ children: y("vibecontrols.shared.entityTagPicker.noTagsAssigned", "No tags assigned")
135
159
  }),
136
- q.map((e) => /* @__PURE__ */ _("span", {
160
+ G.map((e) => /* @__PURE__ */ m("span", {
137
161
  className: "inline-flex items-center gap-1 px-3 py-1 bg-action-primary-bg/10 text-action-primary-bg text-sm rounded-full",
138
162
  children: [
139
- /* @__PURE__ */ g(m, {
163
+ /* @__PURE__ */ p(re, {
140
164
  className: "h-3 w-3",
141
165
  "aria-hidden": "true"
142
166
  }),
143
167
  e.label,
144
- !C && /* @__PURE__ */ g("button", {
168
+ !v && /* @__PURE__ */ p("button", {
145
169
  type: "button",
146
- onClick: () => $(e.tagId),
147
- disabled: G,
170
+ onClick: () => se(e.tagId),
171
+ disabled: V,
148
172
  "aria-label": `Remove tag ${e.label}`,
149
173
  className: "ml-1 hover:text-red-500 transition-colors disabled:opacity-50",
150
- children: /* @__PURE__ */ g(h, {
174
+ children: /* @__PURE__ */ p(f, {
151
175
  className: "h-3 w-3",
152
176
  "aria-hidden": "true"
153
177
  })
154
178
  })
155
179
  ]
156
180
  }, e.key)),
157
- !C && !E && /* @__PURE__ */ _("button", {
181
+ !v && !x && /* @__PURE__ */ m("button", {
158
182
  type: "button",
159
- onClick: () => D(!0),
160
- "aria-label": w("vibecontrols.shared.entityTagPicker.addTag", "Add tag"),
183
+ onClick: () => S(!0),
184
+ "aria-label": y("vibecontrols.shared.entityTagPicker.addTag", "Add tag"),
161
185
  className: "inline-flex items-center gap-1 px-3 py-1 border border-dashed border-border-default text-text-secondary text-sm rounded-full hover:border-action-primary-bg hover:text-action-primary-bg transition-colors",
162
- children: [/* @__PURE__ */ g(f, {
186
+ children: [/* @__PURE__ */ p(d, {
163
187
  className: "h-3 w-3",
164
188
  "aria-hidden": "true"
165
- }), w("vibecontrols.shared.entityTagPicker.addTag", "Add Tag")]
189
+ }), y("vibecontrols.shared.entityTagPicker.addTag", "Add Tag")]
166
190
  })
167
191
  ]
168
192
  }),
169
- E && /* @__PURE__ */ _("div", {
193
+ x && /* @__PURE__ */ m("div", {
170
194
  className: "border border-border-default rounded-lg p-3 space-y-2 bg-bg-surface",
171
195
  children: [
172
- /* @__PURE__ */ _("div", {
196
+ /* @__PURE__ */ m("div", {
173
197
  className: "relative",
174
- children: [/* @__PURE__ */ g(p, {
198
+ children: [/* @__PURE__ */ p(ne, {
175
199
  className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-text-secondary",
176
200
  "aria-hidden": "true"
177
- }), /* @__PURE__ */ g("input", {
201
+ }), /* @__PURE__ */ p("input", {
178
202
  type: "text",
179
- value: O,
180
- onChange: (e) => Z(e.target.value),
181
- placeholder: w("vibecontrols.shared.entityTagPicker.searchPlaceholder", "Search tags..."),
203
+ value: C,
204
+ onChange: (e) => X(e.target.value),
205
+ placeholder: y("vibecontrols.shared.entityTagPicker.searchPlaceholder", "Search tags..."),
182
206
  autoFocus: !0,
183
- "aria-label": w("vibecontrols.shared.entityTagPicker.searchAvailableTags", "Search available tags"),
207
+ "aria-label": y("vibecontrols.shared.entityTagPicker.searchAvailableTags", "Search available tags"),
184
208
  className: "w-full pl-9 pr-3 py-2 text-sm bg-bg-default border border-border-default rounded-md focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 text-text-primary placeholder:text-text-secondary"
185
209
  })]
186
210
  }),
187
- /* @__PURE__ */ _("div", {
211
+ /* @__PURE__ */ m("div", {
188
212
  className: "flex flex-wrap gap-2 max-h-32 overflow-y-auto",
189
- "aria-label": w("vibecontrols.shared.entityTagPicker.tagSuggestions", "Tag suggestions"),
213
+ "aria-label": y("vibecontrols.shared.entityTagPicker.tagSuggestions", "Tag suggestions"),
190
214
  children: [
191
- (V || R && !O) && /* @__PURE__ */ g("span", {
215
+ (L || P && !C) && /* @__PURE__ */ p("span", {
192
216
  className: "text-text-secondary text-xs",
193
- children: w("vibecontrols.shared.entityTagPicker.loading", "Loading...")
217
+ children: y("vibecontrols.shared.entityTagPicker.loading", "Loading...")
194
218
  }),
195
- !V && !R && Y.length === 0 && /* @__PURE__ */ g("span", {
219
+ !L && !P && J.length === 0 && !$ && /* @__PURE__ */ p("span", {
196
220
  className: "text-text-secondary text-xs",
197
- children: O ? w("vibecontrols.shared.entityTagPicker.noMatchingTags", "No matching tags") : w("vibecontrols.shared.entityTagPicker.noAvailableTags", "No available tags")
221
+ children: Q ? y("vibecontrols.shared.entityTagPicker.noMatchingTags", "No matching tags") : y("vibecontrols.shared.entityTagPicker.noAvailableTags", "No tags yet — type a name to create one")
198
222
  }),
199
- Y.map((e) => /* @__PURE__ */ _("button", {
223
+ J.map((e) => /* @__PURE__ */ m("button", {
200
224
  type: "button",
201
- onClick: () => Q(e),
202
- disabled: U,
225
+ onClick: () => Z(e),
226
+ disabled: z,
203
227
  "aria-label": `Add tag ${e.label}`,
204
228
  className: "inline-flex items-center gap-1 px-3 py-1 bg-bg-sunken text-text-primary text-sm rounded-full hover:bg-action-primary-bg/10 hover:text-action-primary-bg transition-colors cursor-pointer disabled:opacity-50",
205
- children: [/* @__PURE__ */ g(f, {
229
+ children: [/* @__PURE__ */ p(d, {
206
230
  className: "h-3 w-3",
207
231
  "aria-hidden": "true"
208
232
  }), e.label]
209
233
  }, e.id))
210
234
  ]
211
235
  }),
212
- /* @__PURE__ */ g("button", {
236
+ $ && /* @__PURE__ */ m("button", {
237
+ type: "button",
238
+ onClick: () => ce(Q),
239
+ disabled: U || z,
240
+ "aria-label": y("vibecontrols.shared.entityTagPicker.createTagAria", "Create tag {{label}}", { label: Q }),
241
+ className: "inline-flex w-full items-center gap-1.5 px-3 py-2 border border-dashed border-action-primary-bg/50 text-action-primary-bg text-sm rounded-md hover:bg-action-primary-bg/10 transition-colors cursor-pointer disabled:opacity-50",
242
+ children: [U || z ? /* @__PURE__ */ p(u, {
243
+ className: "h-3.5 w-3.5 animate-spin",
244
+ "aria-hidden": "true"
245
+ }) : /* @__PURE__ */ p(d, {
246
+ className: "h-3.5 w-3.5",
247
+ "aria-hidden": "true"
248
+ }), /* @__PURE__ */ p("span", {
249
+ className: "truncate",
250
+ children: y("vibecontrols.shared.entityTagPicker.createTag", "Create \"{{label}}\"", { label: Q })
251
+ })]
252
+ }),
253
+ /* @__PURE__ */ p("button", {
213
254
  type: "button",
214
255
  onClick: () => {
215
- D(!1), k("");
256
+ S(!1), w("");
216
257
  },
217
258
  className: "text-xs text-text-secondary hover:text-text-primary",
218
- children: w("vibecontrols.shared.entityTagPicker.done", "Done")
259
+ children: y("vibecontrols.shared.entityTagPicker.done", "Done")
219
260
  })
220
261
  ]
221
262
  })
@@ -223,6 +264,6 @@ function b({ entityType: b, entityId: x, onDraftChange: S, readOnly: C = !1 }) {
223
264
  });
224
265
  }
225
266
  //#endregion
226
- export { b as EntityTagPicker };
267
+ export { g as EntityTagPicker };
227
268
 
228
269
  //# sourceMappingURL=EntityTagPicker.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"EntityTagPicker.js","names":[],"sources":["../../../src/components/shared/EntityTagPicker.tsx"],"sourcesContent":["/**\n * EntityTagPicker — Reusable tag picker for any VibeControls entity\n *\n * Two modes:\n * - Persisted (entityId provided): self-fetches assignments and writes\n * immediately via tag/untag mutations. Used on detail/edit surfaces.\n * - Draft (entityId omitted/empty): no queries or mutations — selections\n * are held locally and surfaced via `onDraftChange(tagIds)`. The parent\n * create form persists them after the entity is created (e.g. via\n * replaceVibecontrolsEntityTags with the new id). Used in create\n * forms/wizards where no entity id exists yet.\n *\n * Usage:\n * <EntityTagPicker entityType=\"VIBECONTROLS_VIBE\" entityId={vibe.id} />\n * <EntityTagPicker entityType=\"VIBECONTROLS_VIBE\" onDraftChange={setTagIds} />\n */\nimport { useState, useCallback, useMemo, useEffect, useRef } from 'react';\nimport { Tag, Plus, X, Search, Loader2 } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n useGetEntityTags,\n useAvailableTags,\n useSearchTags,\n useTagEntity,\n useUntagEntity,\n} from '../../hooks/useEntityTags';\nimport type { AvailableTag, EntityTagAssignment } from '../../hooks/useEntityTags';\n\ninterface EntityTagPickerProps {\n entityType: string;\n /**\n * Existing entity id. Omit (or pass empty) to run in draft mode for\n * create forms — selections are emitted via `onDraftChange` instead of\n * being persisted immediately.\n */\n entityId?: string;\n /**\n * Draft-mode callback. Receives the current selected tag ids whenever\n * the draft selection changes. Only invoked when `entityId` is absent.\n */\n onDraftChange?: (tagIds: string[]) => void;\n readOnly?: boolean;\n}\n\nconst ERROR_DISMISS_MS = 4000;\nconst SEARCH_DEBOUNCE_MS = 250;\n\nexport function EntityTagPicker({\n entityType,\n entityId,\n onDraftChange,\n readOnly = false,\n}: EntityTagPickerProps) {\n const tr = useTr();\n const draftMode = !entityId;\n const [isAdding, setIsAdding] = useState(false);\n const [searchQuery, setSearchQuery] = useState('');\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n // Draft-mode local selection (no persistence until the parent flushes it).\n const [draftTags, setDraftTags] = useState<AvailableTag[]>([]);\n\n // Reset parent draft-id state on mount so a fresh create form never inherits\n // tag ids left over from a previous create on the same page-level hook.\n useEffect(() => {\n if (draftMode) onDraftChange?.([]);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const {\n data: tagsData,\n loading: tagsLoading,\n refetch: refetchTags,\n } = useGetEntityTags(entityType, entityId ?? '');\n const { data: availableData, loading: availableLoading } = useAvailableTags(50, {\n skip: !isAdding,\n });\n const [searchTags, { data: searchData, loading: searchLoading }] = useSearchTags();\n const [tagEntity, { loading: tagging }] = useTagEntity();\n const [untagEntity, { loading: untagging }] = useUntagEntity();\n\n // Apollo Client v4 returns DeepPartialObject from useQuery data, but our\n // hook types declare full shapes — we cast back to the declared types since\n // the resolver is guaranteed to return non-null fields per the schema.\n const assignedTags = useMemo(\n () => (tagsData?.vibecontrolsEntityTags ?? []) as EntityTagAssignment[],\n [tagsData]\n );\n\n // Unified chip list — draft selections in draft mode, persisted\n // assignments otherwise. `tagId` drives removal; `key` is render-stable.\n const chipItems = useMemo(\n () =>\n draftMode\n ? draftTags.map((t) => ({ key: t.id, tagId: t.id, label: t.label }))\n : assignedTags.map((a) => ({\n key: a.id,\n tagId: a.tagId,\n label:\n a.tag?.label ?? tr('vibecontrols.shared.entityTagPicker.untitledTag', 'Untitled tag'),\n })),\n [draftMode, draftTags, assignedTags, tr]\n );\n\n const assignedTagIds = useMemo(() => new Set(chipItems.map((c) => c.tagId)), [chipItems]);\n\n const suggestions: AvailableTag[] = useMemo(() => {\n const source = searchQuery\n ? (searchData?.searchVibecontrolsTags as AvailableTag[] | undefined)\n : (availableData?.availableVibecontrolsTags as AvailableTag[] | undefined);\n return (source ?? []).filter((t) => !assignedTagIds.has(t.id));\n }, [searchQuery, searchData, availableData, assignedTagIds]);\n\n // Auto-dismiss error toast after a few seconds\n useEffect(() => {\n if (!errorMessage) return;\n const timer = setTimeout(() => setErrorMessage(null), ERROR_DISMISS_MS);\n return () => clearTimeout(timer);\n }, [errorMessage]);\n\n // Debounce the search query so we don't fire a request on every keystroke.\n const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n useEffect(() => {\n return () => {\n if (searchTimerRef.current) clearTimeout(searchTimerRef.current);\n };\n }, []);\n\n const handleSearch = useCallback(\n (query: string) => {\n setSearchQuery(query);\n if (searchTimerRef.current) clearTimeout(searchTimerRef.current);\n if (query.length < 2) return;\n searchTimerRef.current = setTimeout(() => {\n searchTags({ variables: { query, limit: 10 } });\n }, SEARCH_DEBOUNCE_MS);\n },\n [searchTags]\n );\n\n const handleAddTag = useCallback(\n async (tag: AvailableTag) => {\n if (draftMode) {\n setDraftTags((prev) => {\n if (prev.some((t) => t.id === tag.id)) return prev;\n const next = [...prev, tag];\n onDraftChange?.(next.map((t) => t.id));\n return next;\n });\n setSearchQuery('');\n setErrorMessage(null);\n return;\n }\n try {\n await tagEntity({\n variables: { input: { entityType, entityId: entityId ?? '', tagId: tag.id } },\n });\n await refetchTags();\n setSearchQuery('');\n setErrorMessage(null);\n } catch (err) {\n setErrorMessage(\n err instanceof Error\n ? err.message\n : tr('vibecontrols.shared.entityTagPicker.failedToAddTag', 'Failed to add tag')\n );\n }\n },\n [draftMode, onDraftChange, tagEntity, entityType, entityId, refetchTags, tr]\n );\n\n const handleRemoveTag = useCallback(\n async (tagId: string) => {\n if (draftMode) {\n setDraftTags((prev) => {\n const next = prev.filter((t) => t.id !== tagId);\n onDraftChange?.(next.map((t) => t.id));\n return next;\n });\n setErrorMessage(null);\n return;\n }\n try {\n await untagEntity({\n variables: { input: { entityType, entityId: entityId ?? '', tagId } },\n });\n await refetchTags();\n setErrorMessage(null);\n } catch (err) {\n setErrorMessage(\n err instanceof Error\n ? err.message\n : tr('vibecontrols.shared.entityTagPicker.failedToRemoveTag', 'Failed to remove tag')\n );\n }\n },\n [draftMode, onDraftChange, untagEntity, entityType, entityId, refetchTags, tr]\n );\n\n if (tagsLoading && !draftMode) {\n return (\n <div\n className=\"flex items-center gap-2 text-text-secondary text-sm\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <Loader2 className=\"h-4 w-4 animate-spin\" aria-hidden=\"true\" />\n {tr('vibecontrols.shared.entityTagPicker.loadingTags', 'Loading tags...')}\n </div>\n );\n }\n\n return (\n <div className=\"space-y-3\">\n {errorMessage && (\n <div\n role=\"alert\"\n className=\"flex items-start gap-2 rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700\"\n >\n <span className=\"flex-1\">{errorMessage}</span>\n <button\n type=\"button\"\n onClick={() => setErrorMessage(null)}\n aria-label={tr('vibecontrols.shared.entityTagPicker.dismissError', 'Dismiss error')}\n className=\"text-red-700 hover:text-red-900\"\n >\n <X className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n </button>\n </div>\n )}\n\n <div\n className=\"flex flex-wrap gap-2\"\n aria-label={tr('vibecontrols.shared.entityTagPicker.assignedTags', 'Assigned tags')}\n >\n {chipItems.length === 0 && !isAdding && (\n <p className=\"text-text-secondary text-sm\">\n {tr('vibecontrols.shared.entityTagPicker.noTagsAssigned', 'No tags assigned')}\n </p>\n )}\n {chipItems.map((chip) => (\n <span\n key={chip.key}\n className=\"inline-flex items-center gap-1 px-3 py-1 bg-action-primary-bg/10 text-action-primary-bg text-sm rounded-full\"\n >\n <Tag className=\"h-3 w-3\" aria-hidden=\"true\" />\n {chip.label}\n {!readOnly && (\n <button\n type=\"button\"\n onClick={() => handleRemoveTag(chip.tagId)}\n disabled={untagging}\n aria-label={`Remove tag ${chip.label}`}\n className=\"ml-1 hover:text-red-500 transition-colors disabled:opacity-50\"\n >\n <X className=\"h-3 w-3\" aria-hidden=\"true\" />\n </button>\n )}\n </span>\n ))}\n {!readOnly && !isAdding && (\n <button\n type=\"button\"\n onClick={() => setIsAdding(true)}\n aria-label={tr('vibecontrols.shared.entityTagPicker.addTag', 'Add tag')}\n className=\"inline-flex items-center gap-1 px-3 py-1 border border-dashed border-border-default text-text-secondary text-sm rounded-full hover:border-action-primary-bg hover:text-action-primary-bg transition-colors\"\n >\n <Plus className=\"h-3 w-3\" aria-hidden=\"true\" />\n {tr('vibecontrols.shared.entityTagPicker.addTag', 'Add Tag')}\n </button>\n )}\n </div>\n\n {isAdding && (\n <div className=\"border border-border-default rounded-lg p-3 space-y-2 bg-bg-surface\">\n <div className=\"relative\">\n <Search\n className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => handleSearch(e.target.value)}\n placeholder={tr(\n 'vibecontrols.shared.entityTagPicker.searchPlaceholder',\n 'Search tags...'\n )}\n autoFocus\n aria-label={tr(\n 'vibecontrols.shared.entityTagPicker.searchAvailableTags',\n 'Search available tags'\n )}\n className=\"w-full pl-9 pr-3 py-2 text-sm bg-bg-default border border-border-default rounded-md focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 text-text-primary placeholder:text-text-secondary\"\n />\n </div>\n <div\n className=\"flex flex-wrap gap-2 max-h-32 overflow-y-auto\"\n aria-label={tr('vibecontrols.shared.entityTagPicker.tagSuggestions', 'Tag suggestions')}\n >\n {(searchLoading || (availableLoading && !searchQuery)) && (\n <span className=\"text-text-secondary text-xs\">\n {tr('vibecontrols.shared.entityTagPicker.loading', 'Loading...')}\n </span>\n )}\n {!searchLoading && !availableLoading && suggestions.length === 0 && (\n <span className=\"text-text-secondary text-xs\">\n {searchQuery\n ? tr('vibecontrols.shared.entityTagPicker.noMatchingTags', 'No matching tags')\n : tr('vibecontrols.shared.entityTagPicker.noAvailableTags', 'No available tags')}\n </span>\n )}\n {suggestions.map((tag) => (\n <button\n key={tag.id}\n type=\"button\"\n onClick={() => handleAddTag(tag)}\n disabled={tagging}\n aria-label={`Add tag ${tag.label}`}\n className=\"inline-flex items-center gap-1 px-3 py-1 bg-bg-sunken text-text-primary text-sm rounded-full hover:bg-action-primary-bg/10 hover:text-action-primary-bg transition-colors cursor-pointer disabled:opacity-50\"\n >\n <Plus className=\"h-3 w-3\" aria-hidden=\"true\" />\n {tag.label}\n </button>\n ))}\n </div>\n <button\n type=\"button\"\n onClick={() => {\n setIsAdding(false);\n setSearchQuery('');\n }}\n className=\"text-xs text-text-secondary hover:text-text-primary\"\n >\n {tr('vibecontrols.shared.entityTagPicker.done', 'Done')}\n </button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"EntityTagPicker.js","names":[],"sources":["../../../src/components/shared/EntityTagPicker.tsx"],"sourcesContent":["/**\n * EntityTagPicker — Reusable tag picker for any VibeControls entity\n *\n * Two modes:\n * - Persisted (entityId provided): self-fetches assignments and writes\n * immediately via tag/untag mutations. Used on detail/edit surfaces.\n * - Draft (entityId omitted/empty): no queries or mutations — selections\n * are held locally and surfaced via `onDraftChange(tagIds)`. The parent\n * create form persists them after the entity is created (e.g. via\n * replaceVibecontrolsEntityTags with the new id). Used in create\n * forms/wizards where no entity id exists yet.\n *\n * Usage:\n * <EntityTagPicker entityType=\"VIBECONTROLS_VIBE\" entityId={vibe.id} />\n * <EntityTagPicker entityType=\"VIBECONTROLS_VIBE\" onDraftChange={setTagIds} />\n */\nimport { useState, useCallback, useMemo, useEffect, useRef } from 'react';\nimport { Tag, Plus, X, Search, Loader2 } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n useGetEntityTags,\n useAvailableTags,\n useSearchTags,\n useTagEntity,\n useUntagEntity,\n useCreateVibecontrolsTag,\n} from '../../hooks/useEntityTags';\nimport type { AvailableTag, EntityTagAssignment } from '../../hooks/useEntityTags';\n\ninterface EntityTagPickerProps {\n entityType: string;\n /**\n * Existing entity id. Omit (or pass empty) to run in draft mode for\n * create forms — selections are emitted via `onDraftChange` instead of\n * being persisted immediately.\n */\n entityId?: string;\n /**\n * Draft-mode callback. Receives the current selected tag ids whenever\n * the draft selection changes. Only invoked when `entityId` is absent.\n */\n onDraftChange?: (tagIds: string[]) => void;\n readOnly?: boolean;\n}\n\nconst ERROR_DISMISS_MS = 4000;\nconst SEARCH_DEBOUNCE_MS = 250;\n\nexport function EntityTagPicker({\n entityType,\n entityId,\n onDraftChange,\n readOnly = false,\n}: EntityTagPickerProps) {\n const tr = useTr();\n const draftMode = !entityId;\n const [isAdding, setIsAdding] = useState(false);\n const [searchQuery, setSearchQuery] = useState('');\n // The trimmed query the most recent debounced search actually ran for.\n // Used to gate inline creation: we only offer \"Create\" once we have search\n // results that correspond to the current query (avoids offering Create\n // against stale/empty results while the debounced search is still pending).\n const [searchedQuery, setSearchedQuery] = useState('');\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n // Draft-mode local selection (no persistence until the parent flushes it).\n const [draftTags, setDraftTags] = useState<AvailableTag[]>([]);\n\n // In draft mode, propagate the local selection to the parent whenever it\n // changes — and reset it to [] on mount, so a fresh create form never\n // inherits tag ids left over from a previous create on the same page-level\n // hook. Emitting from an effect (rather than inside a setDraftTags updater)\n // keeps the updater pure — no \"setState while rendering\" warning — and lets\n // the handlers use functional updates, which avoids lost-update races when\n // async create/add flows overlap.\n useEffect(() => {\n if (draftMode) onDraftChange?.(draftTags.map((t) => t.id));\n // onDraftChange is a stable setter from the parent's page-level hook;\n // excluded to avoid re-emitting on unrelated parent re-renders.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [draftMode, draftTags]);\n\n const {\n data: tagsData,\n loading: tagsLoading,\n refetch: refetchTags,\n } = useGetEntityTags(entityType, entityId ?? '');\n const { data: availableData, loading: availableLoading } = useAvailableTags(50, {\n skip: !isAdding,\n });\n const [searchTags, { data: searchData, loading: searchLoading }] = useSearchTags();\n const [tagEntity, { loading: tagging }] = useTagEntity();\n const [untagEntity, { loading: untagging }] = useUntagEntity();\n const [createTag, { loading: creatingTag }] = useCreateVibecontrolsTag();\n\n // Apollo Client v4 returns DeepPartialObject from useQuery data, but our\n // hook types declare full shapes — we cast back to the declared types since\n // the resolver is guaranteed to return non-null fields per the schema.\n const assignedTags = useMemo(\n () => (tagsData?.vibecontrolsEntityTags ?? []) as EntityTagAssignment[],\n [tagsData]\n );\n\n // Unified chip list — draft selections in draft mode, persisted\n // assignments otherwise. `tagId` drives removal; `key` is render-stable.\n const chipItems = useMemo(\n () =>\n draftMode\n ? draftTags.map((t) => ({ key: t.id, tagId: t.id, label: t.label }))\n : assignedTags.map((a) => ({\n key: a.id,\n tagId: a.tagId,\n label:\n a.tag?.label ?? tr('vibecontrols.shared.entityTagPicker.untitledTag', 'Untitled tag'),\n })),\n [draftMode, draftTags, assignedTags, tr]\n );\n\n const assignedTagIds = useMemo(() => new Set(chipItems.map((c) => c.tagId)), [chipItems]);\n\n // Unfiltered tag options from the active source (search results when\n // searching, otherwise the available-tags list). Kept separate from\n // `suggestions` so the create affordance can detect an exact match against\n // ALL existing tags, including ones already assigned to this entity.\n const rawTagOptions: AvailableTag[] = useMemo(() => {\n const source = searchQuery\n ? (searchData?.searchVibecontrolsTags as AvailableTag[] | undefined)\n : (availableData?.availableVibecontrolsTags as AvailableTag[] | undefined);\n return source ?? [];\n }, [searchQuery, searchData, availableData]);\n\n const suggestions: AvailableTag[] = useMemo(\n () => rawTagOptions.filter((t) => !assignedTagIds.has(t.id)),\n [rawTagOptions, assignedTagIds]\n );\n\n // Auto-dismiss error toast after a few seconds\n useEffect(() => {\n if (!errorMessage) return;\n const timer = setTimeout(() => setErrorMessage(null), ERROR_DISMISS_MS);\n return () => clearTimeout(timer);\n }, [errorMessage]);\n\n // Debounce the search query so we don't fire a request on every keystroke.\n const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n useEffect(() => {\n return () => {\n if (searchTimerRef.current) clearTimeout(searchTimerRef.current);\n };\n }, []);\n\n const handleSearch = useCallback(\n (query: string) => {\n setSearchQuery(query);\n if (searchTimerRef.current) clearTimeout(searchTimerRef.current);\n const trimmed = query.trim();\n // No search runs for short queries, so there are no authoritative results\n // for this input — clear the marker to keep the Create button gated.\n if (trimmed.length < 2) {\n setSearchedQuery('');\n return;\n }\n searchTimerRef.current = setTimeout(() => {\n setSearchedQuery(trimmed);\n searchTags({ variables: { query: trimmed, limit: 10 } });\n }, SEARCH_DEBOUNCE_MS);\n },\n [searchTags]\n );\n\n const handleAddTag = useCallback(\n async (tag: AvailableTag) => {\n if (draftMode) {\n // Functional update so overlapping async flows (e.g. an in-flight\n // create plus a click on another suggestion) can't overwrite each\n // other from a stale snapshot. The draftTags-watching effect emits\n // onDraftChange for us.\n setDraftTags((prev) => (prev.some((t) => t.id === tag.id) ? prev : [...prev, tag]));\n setSearchQuery('');\n setErrorMessage(null);\n return;\n }\n try {\n await tagEntity({\n variables: { input: { entityType, entityId: entityId ?? '', tagId: tag.id } },\n });\n await refetchTags();\n setSearchQuery('');\n setErrorMessage(null);\n } catch (err) {\n setErrorMessage(\n err instanceof Error\n ? err.message\n : tr('vibecontrols.shared.entityTagPicker.failedToAddTag', 'Failed to add tag')\n );\n }\n },\n [draftMode, tagEntity, entityType, entityId, refetchTags, tr]\n );\n\n const handleRemoveTag = useCallback(\n async (tagId: string) => {\n if (draftMode) {\n setDraftTags((prev) => prev.filter((t) => t.id !== tagId));\n setErrorMessage(null);\n return;\n }\n try {\n await untagEntity({\n variables: { input: { entityType, entityId: entityId ?? '', tagId } },\n });\n await refetchTags();\n setErrorMessage(null);\n } catch (err) {\n setErrorMessage(\n err instanceof Error\n ? err.message\n : tr('vibecontrols.shared.entityTagPicker.failedToRemoveTag', 'Failed to remove tag')\n );\n }\n },\n [draftMode, untagEntity, entityType, entityId, refetchTags, tr]\n );\n\n // Inline tag creation. No vocabularyId is sent — the backend resolves/creates\n // a default workspace VibeControls vocabulary. The freshly created tag is\n // immediately selected via handleAddTag (draft → local; persisted → tagEntity).\n const handleCreateTag = useCallback(\n async (rawLabel: string) => {\n const label = rawLabel.trim();\n if (!label) return;\n try {\n // Refetch the available-tags list so the new tag shows up as a\n // suggestion on subsequent searches (not just as the selected chip).\n const result = await createTag({\n variables: { input: { label } },\n refetchQueries: ['EntityTagPickerAvailableTags'],\n });\n const created = result.data?.createVibecontrolsTag;\n if (!created) {\n throw new Error(\n tr('vibecontrols.shared.entityTagPicker.failedToCreateTag', 'Failed to create tag')\n );\n }\n await handleAddTag({\n id: created.id,\n key: created.key,\n label: created.label,\n color: created.color ?? null,\n });\n } catch (err) {\n setErrorMessage(\n err instanceof Error\n ? err.message\n : tr('vibecontrols.shared.entityTagPicker.failedToCreateTag', 'Failed to create tag')\n );\n }\n },\n [createTag, handleAddTag, tr]\n );\n\n // Offer inline creation when the typed label doesn't already exist (covers\n // both \"no tags yet\" and \"no match\" cases). Match against the unfiltered\n // options AND the already-selected chips — checking only `suggestions` would\n // miss tags already assigned to this entity (filtered out of suggestions),\n // wrongly offering \"Create\" for a label that already exists.\n const trimmedQuery = searchQuery.trim();\n const hasExactMatch = useMemo(() => {\n const q = trimmedQuery.toLowerCase();\n if (!q) return false;\n return (\n rawTagOptions.some((t) => t.label.toLowerCase() === q) ||\n chipItems.some((c) => c.label.toLowerCase() === q)\n );\n }, [rawTagOptions, chipItems, trimmedQuery]);\n // Only offer Create once the debounced search has actually run and settled\n // for the current query — otherwise rawTagOptions may be stale/empty and we\n // could offer Create for a label that already exists. (Search runs only for\n // queries of length >= 2, which also bounds inline-created tag labels.)\n const searchSettledForQuery = searchedQuery === trimmedQuery && !searchLoading;\n const canCreateTag =\n !readOnly && trimmedQuery.length >= 2 && searchSettledForQuery && !hasExactMatch;\n\n if (tagsLoading && !draftMode) {\n return (\n <div\n className=\"flex items-center gap-2 text-text-secondary text-sm\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <Loader2 className=\"h-4 w-4 animate-spin\" aria-hidden=\"true\" />\n {tr('vibecontrols.shared.entityTagPicker.loadingTags', 'Loading tags...')}\n </div>\n );\n }\n\n return (\n <div className=\"space-y-3\">\n {errorMessage && (\n <div\n role=\"alert\"\n className=\"flex items-start gap-2 rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700\"\n >\n <span className=\"flex-1\">{errorMessage}</span>\n <button\n type=\"button\"\n onClick={() => setErrorMessage(null)}\n aria-label={tr('vibecontrols.shared.entityTagPicker.dismissError', 'Dismiss error')}\n className=\"text-red-700 hover:text-red-900\"\n >\n <X className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n </button>\n </div>\n )}\n\n <div\n className=\"flex flex-wrap gap-2\"\n aria-label={tr('vibecontrols.shared.entityTagPicker.assignedTags', 'Assigned tags')}\n >\n {chipItems.length === 0 && !isAdding && (\n <p className=\"text-text-secondary text-sm\">\n {tr('vibecontrols.shared.entityTagPicker.noTagsAssigned', 'No tags assigned')}\n </p>\n )}\n {chipItems.map((chip) => (\n <span\n key={chip.key}\n className=\"inline-flex items-center gap-1 px-3 py-1 bg-action-primary-bg/10 text-action-primary-bg text-sm rounded-full\"\n >\n <Tag className=\"h-3 w-3\" aria-hidden=\"true\" />\n {chip.label}\n {!readOnly && (\n <button\n type=\"button\"\n onClick={() => handleRemoveTag(chip.tagId)}\n disabled={untagging}\n aria-label={`Remove tag ${chip.label}`}\n className=\"ml-1 hover:text-red-500 transition-colors disabled:opacity-50\"\n >\n <X className=\"h-3 w-3\" aria-hidden=\"true\" />\n </button>\n )}\n </span>\n ))}\n {!readOnly && !isAdding && (\n <button\n type=\"button\"\n onClick={() => setIsAdding(true)}\n aria-label={tr('vibecontrols.shared.entityTagPicker.addTag', 'Add tag')}\n className=\"inline-flex items-center gap-1 px-3 py-1 border border-dashed border-border-default text-text-secondary text-sm rounded-full hover:border-action-primary-bg hover:text-action-primary-bg transition-colors\"\n >\n <Plus className=\"h-3 w-3\" aria-hidden=\"true\" />\n {tr('vibecontrols.shared.entityTagPicker.addTag', 'Add Tag')}\n </button>\n )}\n </div>\n\n {isAdding && (\n <div className=\"border border-border-default rounded-lg p-3 space-y-2 bg-bg-surface\">\n <div className=\"relative\">\n <Search\n className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => handleSearch(e.target.value)}\n placeholder={tr(\n 'vibecontrols.shared.entityTagPicker.searchPlaceholder',\n 'Search tags...'\n )}\n autoFocus\n aria-label={tr(\n 'vibecontrols.shared.entityTagPicker.searchAvailableTags',\n 'Search available tags'\n )}\n className=\"w-full pl-9 pr-3 py-2 text-sm bg-bg-default border border-border-default rounded-md focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 text-text-primary placeholder:text-text-secondary\"\n />\n </div>\n <div\n className=\"flex flex-wrap gap-2 max-h-32 overflow-y-auto\"\n aria-label={tr('vibecontrols.shared.entityTagPicker.tagSuggestions', 'Tag suggestions')}\n >\n {(searchLoading || (availableLoading && !searchQuery)) && (\n <span className=\"text-text-secondary text-xs\">\n {tr('vibecontrols.shared.entityTagPicker.loading', 'Loading...')}\n </span>\n )}\n {!searchLoading && !availableLoading && suggestions.length === 0 && !canCreateTag && (\n <span className=\"text-text-secondary text-xs\">\n {trimmedQuery\n ? tr('vibecontrols.shared.entityTagPicker.noMatchingTags', 'No matching tags')\n : tr(\n 'vibecontrols.shared.entityTagPicker.noAvailableTags',\n 'No tags yet — type a name to create one'\n )}\n </span>\n )}\n {suggestions.map((tag) => (\n <button\n key={tag.id}\n type=\"button\"\n onClick={() => handleAddTag(tag)}\n disabled={tagging}\n aria-label={`Add tag ${tag.label}`}\n className=\"inline-flex items-center gap-1 px-3 py-1 bg-bg-sunken text-text-primary text-sm rounded-full hover:bg-action-primary-bg/10 hover:text-action-primary-bg transition-colors cursor-pointer disabled:opacity-50\"\n >\n <Plus className=\"h-3 w-3\" aria-hidden=\"true\" />\n {tag.label}\n </button>\n ))}\n </div>\n {canCreateTag && (\n <button\n type=\"button\"\n onClick={() => handleCreateTag(trimmedQuery)}\n // Stay disabled through the follow-up assignment too: in persisted\n // mode handleCreateTag runs createTag (creatingTag) then tagEntity\n // (tagging); without `tagging` the button would re-enable between\n // the two, letting a second click duplicate the create.\n disabled={creatingTag || tagging}\n aria-label={tr(\n 'vibecontrols.shared.entityTagPicker.createTagAria',\n 'Create tag {{label}}',\n { label: trimmedQuery }\n )}\n className=\"inline-flex w-full items-center gap-1.5 px-3 py-2 border border-dashed border-action-primary-bg/50 text-action-primary-bg text-sm rounded-md hover:bg-action-primary-bg/10 transition-colors cursor-pointer disabled:opacity-50\"\n >\n {creatingTag || tagging ? (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Plus className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n )}\n <span className=\"truncate\">\n {tr('vibecontrols.shared.entityTagPicker.createTag', 'Create \"{{label}}\"', {\n label: trimmedQuery,\n })}\n </span>\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => {\n setIsAdding(false);\n setSearchQuery('');\n }}\n className=\"text-xs text-text-secondary hover:text-text-primary\"\n >\n {tr('vibecontrols.shared.entityTagPicker.done', 'Done')}\n </button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}