@burdenoff/microfe-workspaces 2026.530.2 → 2026.531.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 (33) hide show
  1. package/dist/WorkspacesRoutes.js +19 -15
  2. package/dist/WorkspacesRoutes.js.map +1 -1
  3. package/dist/components/ProjectCard.js +2 -2
  4. package/dist/components/ProjectCard.js.map +1 -1
  5. package/dist/components/tags/ProjectTagsWidget.js +53 -57
  6. package/dist/components/tags/ProjectTagsWidget.js.map +1 -1
  7. package/dist/generated/wspace-operations.js +24 -9
  8. package/dist/generated/wspace-operations.js.map +1 -1
  9. package/dist/hooks/index.js +1 -0
  10. package/dist/hooks/useMemberMutations.js +2 -2
  11. package/dist/hooks/useMemberMutations.js.map +1 -1
  12. package/dist/hooks/usePendingInvitations.js +29 -0
  13. package/dist/hooks/usePendingInvitations.js.map +1 -0
  14. package/dist/hooks/useProjectMutations.js +107 -6
  15. package/dist/hooks/useProjectMutations.js.map +1 -1
  16. package/dist/hooks/useProjects.js +1 -1
  17. package/dist/hooks/useProjects.js.map +1 -1
  18. package/dist/hooks/useWorkspaceMembers.js +1 -1
  19. package/dist/hooks/useWorkspaceMembers.js.map +1 -1
  20. package/dist/index.js +20 -18
  21. package/dist/pages/InviteAcceptancePage.js +68 -44
  22. package/dist/pages/InviteAcceptancePage.js.map +1 -1
  23. package/dist/pages/PendingInvitesPage.js +254 -0
  24. package/dist/pages/PendingInvitesPage.js.map +1 -0
  25. package/dist/pages/ProjectDetailPage.js +187 -161
  26. package/dist/pages/ProjectDetailPage.js.map +1 -1
  27. package/dist/pages/ProjectsListPage.js +2 -2
  28. package/dist/pages/ProjectsListPage.js.map +1 -1
  29. package/dist/pages/WorkspacesListPage.js +118 -102
  30. package/dist/pages/WorkspacesListPage.js.map +1 -1
  31. package/dist/utils/projectTags.js +28 -2
  32. package/dist/utils/projectTags.js.map +1 -1
  33. package/package.json +1 -1
@@ -5,14 +5,14 @@ import { FeatureGate as i } from "@burdenoff/fe-libs/shared/feature-flags";
5
5
  import { jsx as a, jsxs as o } from "react/jsx-runtime";
6
6
  import { Navigate as s, Route as c, Routes as l } from "react-router-dom";
7
7
  //#region src/WorkspacesRoutes.tsx
8
- var u = r(() => import("./pages/CreateWorkspacePage.js").then((e) => ({ default: e.CreateWorkspacePage }))), d = r(() => import("./pages/WorkspaceDetailPage.js").then((e) => ({ default: e.WorkspaceDetailPage }))), f = r(() => import("./pages/WorkspaceSettingsPage.js").then((e) => ({ default: e.WorkspaceSettingsPage }))), p = r(() => import("./pages/ProjectsListPage.js").then((e) => ({ default: e.ProjectsListPage }))), m = r(() => import("./pages/ProjectDetailPage.js").then((e) => ({ default: e.ProjectDetailPage }))), h = r(() => import("./pages/MembersListPage.js").then((e) => ({ default: e.MembersListPage }))), g = r(() => import("./pages/ActivityListPage.js").then((e) => ({ default: e.ActivityListPage }))), _ = r(() => import("./pages/InviteAcceptancePage.js").then((e) => ({ default: e.InviteAcceptancePage })));
9
- function v() {
8
+ var u = r(() => import("./pages/CreateWorkspacePage.js").then((e) => ({ default: e.CreateWorkspacePage }))), d = r(() => import("./pages/WorkspaceDetailPage.js").then((e) => ({ default: e.WorkspaceDetailPage }))), f = r(() => import("./pages/WorkspaceSettingsPage.js").then((e) => ({ default: e.WorkspaceSettingsPage }))), p = r(() => import("./pages/ProjectsListPage.js").then((e) => ({ default: e.ProjectsListPage }))), m = r(() => import("./pages/ProjectDetailPage.js").then((e) => ({ default: e.ProjectDetailPage }))), h = r(() => import("./pages/MembersListPage.js").then((e) => ({ default: e.MembersListPage }))), g = r(() => import("./pages/ActivityListPage.js").then((e) => ({ default: e.ActivityListPage }))), _ = r(() => import("./pages/InviteAcceptancePage.js").then((e) => ({ default: e.InviteAcceptancePage }))), v = r(() => import("./pages/PendingInvitesPage.js").then((e) => ({ default: e.PendingInvitesPage })));
9
+ function y() {
10
10
  return /* @__PURE__ */ a("div", {
11
11
  className: "flex items-center justify-center h-64",
12
12
  children: /* @__PURE__ */ a("div", { className: "animate-spin rounded-full size-8 border-b-2 border-primary" })
13
13
  });
14
14
  }
15
- function y() {
15
+ function b() {
16
16
  return /* @__PURE__ */ a("div", {
17
17
  className: "flex h-full items-center justify-center p-8 text-center",
18
18
  children: /* @__PURE__ */ o("div", { children: [/* @__PURE__ */ a("h2", {
@@ -24,24 +24,24 @@ function y() {
24
24
  })] })
25
25
  });
26
26
  }
27
- function b(e, t, n = !1) {
27
+ function x(e, t, n = !1) {
28
28
  return /* @__PURE__ */ a(i, {
29
29
  flag: e,
30
30
  defaultValue: n,
31
- fallback: /* @__PURE__ */ a(y, {}),
31
+ fallback: /* @__PURE__ */ a(b, {}),
32
32
  children: t
33
33
  });
34
34
  }
35
- function x() {
35
+ function S() {
36
36
  let { basePath: t = "/workspaces" } = e();
37
37
  return /* @__PURE__ */ a(s, {
38
38
  to: `${t}/list`,
39
39
  replace: !0
40
40
  });
41
41
  }
42
- function S() {
42
+ function C() {
43
43
  return /* @__PURE__ */ a(n, {
44
- fallback: /* @__PURE__ */ a(v, {}),
44
+ fallback: /* @__PURE__ */ a(y, {}),
45
45
  children: /* @__PURE__ */ o(l, { children: [
46
46
  /* @__PURE__ */ a(c, {
47
47
  index: !0,
@@ -58,6 +58,10 @@ function S() {
58
58
  path: "new",
59
59
  element: /* @__PURE__ */ a(u, {})
60
60
  }),
61
+ /* @__PURE__ */ a(c, {
62
+ path: "invites",
63
+ element: /* @__PURE__ */ a(v, {})
64
+ }),
61
65
  /* @__PURE__ */ a(c, {
62
66
  path: "invite/:invitationId",
63
67
  element: /* @__PURE__ */ a(_, {})
@@ -68,7 +72,7 @@ function S() {
68
72
  }),
69
73
  /* @__PURE__ */ a(c, {
70
74
  path: ":workspaceId/settings",
71
- element: b("workspaces.settings", /* @__PURE__ */ a(f, {}), !0)
75
+ element: x("workspaces.settings", /* @__PURE__ */ a(f, {}), !0)
72
76
  }),
73
77
  /* @__PURE__ */ a(c, {
74
78
  path: "projects",
@@ -84,28 +88,28 @@ function S() {
84
88
  }),
85
89
  /* @__PURE__ */ a(c, {
86
90
  path: "members",
87
- element: b("workspaces.members", /* @__PURE__ */ a(h, {}), !0)
91
+ element: x("workspaces.members", /* @__PURE__ */ a(h, {}), !0)
88
92
  }),
89
93
  /* @__PURE__ */ a(c, {
90
94
  path: ":workspaceId/members",
91
- element: b("workspaces.members", /* @__PURE__ */ a(h, {}), !0)
95
+ element: x("workspaces.members", /* @__PURE__ */ a(h, {}), !0)
92
96
  }),
93
97
  /* @__PURE__ */ a(c, {
94
98
  path: "activity",
95
- element: b("workspaces.activity", /* @__PURE__ */ a(g, {}))
99
+ element: x("workspaces.activity", /* @__PURE__ */ a(g, {}))
96
100
  }),
97
101
  /* @__PURE__ */ a(c, {
98
102
  path: ":workspaceId/activity",
99
- element: b("workspaces.activity", /* @__PURE__ */ a(g, {}))
103
+ element: x("workspaces.activity", /* @__PURE__ */ a(g, {}))
100
104
  }),
101
105
  /* @__PURE__ */ a(c, {
102
106
  path: "*",
103
- element: /* @__PURE__ */ a(x, {})
107
+ element: /* @__PURE__ */ a(S, {})
104
108
  })
105
109
  ] })
106
110
  });
107
111
  }
108
112
  //#endregion
109
- export { S as WorkspacesRoutes };
113
+ export { C as WorkspacesRoutes };
110
114
 
111
115
  //# sourceMappingURL=WorkspacesRoutes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"WorkspacesRoutes.js","names":[],"sources":["../src/WorkspacesRoutes.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport { Routes, Route, Navigate } from 'react-router-dom';\nimport { FeatureGate } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { useWorkspacesContext } from './providers/WorkspacesProvider';\n\n// Eager load - main entry point\nimport { WorkspacesListPage } from './pages/WorkspacesListPage';\n\n// Lazy load - secondary pages\nconst CreateWorkspacePage = lazy(() =>\n import('./pages/CreateWorkspacePage').then((m) => ({ default: m.CreateWorkspacePage }))\n);\nconst WorkspaceDetailPage = lazy(() =>\n import('./pages/WorkspaceDetailPage').then((m) => ({ default: m.WorkspaceDetailPage }))\n);\nconst WorkspaceSettingsPage = lazy(() =>\n import('./pages/WorkspaceSettingsPage').then((m) => ({ default: m.WorkspaceSettingsPage }))\n);\nconst ProjectsListPage = lazy(() =>\n import('./pages/ProjectsListPage').then((m) => ({ default: m.ProjectsListPage }))\n);\nconst ProjectDetailPage = lazy(() =>\n import('./pages/ProjectDetailPage').then((m) => ({ default: m.ProjectDetailPage }))\n);\nconst MembersListPage = lazy(() =>\n import('./pages/MembersListPage').then((m) => ({ default: m.MembersListPage }))\n);\nconst ActivityListPage = lazy(() =>\n import('./pages/ActivityListPage').then((m) => ({ default: m.ActivityListPage }))\n);\nconst InviteAcceptancePage = lazy(() =>\n import('./pages/InviteAcceptancePage').then((m) => ({ default: m.InviteAcceptancePage }))\n);\n\n/**\n * Loading fallback for lazy-loaded components\n */\nfunction PageLoader() {\n return (\n <div className=\"flex items-center justify-center h-64\">\n <div className=\"animate-spin rounded-full size-8 border-b-2 border-primary\" />\n </div>\n );\n}\n\n/**\n * Fallback shown when a feature-gated route is disabled.\n */\nfunction FeatureDisabledFallback() {\n return (\n <div className=\"flex h-full items-center justify-center p-8 text-center\">\n <div>\n <h2 className=\"text-lg font-semibold mb-2 text-text-primary\">Feature Not Available</h2>\n <p className=\"text-sm text-text-secondary\">\n This feature is not currently available. Enable it under Feature Previews to access this\n functionality.\n </p>\n </div>\n </div>\n );\n}\n\n/**\n * Helper: wraps a page element in a FeatureGate.\n * defaultValue=false → hidden until explicitly enabled (unreleased feature).\n */\nfunction gated(flag: string, element: React.ReactNode, defaultValue = false): React.ReactElement {\n return (\n <FeatureGate flag={flag} defaultValue={defaultValue} fallback={<FeatureDisabledFallback />}>\n {element}\n </FeatureGate>\n );\n}\n\n/**\n * Catch-all redirect component that uses the basePath from context\n * to navigate to an absolute path, preventing infinite redirect loops.\n */\nfunction CatchAllRedirect() {\n const { basePath = '/workspaces' } = useWorkspacesContext();\n return <Navigate to={`${basePath}/list`} replace />;\n}\n\n/**\n * Internal routing for the Workspaces microfrontend.\n * Defines all routes and their corresponding page components.\n *\n * - WorkspacesListPage: Eager loaded (main entry point)\n * - All other pages: Lazy loaded for better performance\n * - Projects, Members, Activity: gated by feature flags (unreleased)\n */\nexport function WorkspacesRoutes() {\n return (\n <Suspense fallback={<PageLoader />}>\n <Routes>\n {/* Default redirect */}\n <Route index element={<Navigate to=\"list\" replace />} />\n\n {/* Workspaces List - Eager loaded */}\n <Route path=\"list\" element={<WorkspacesListPage />} />\n\n {/* Create Workspace (must come before :workspaceId to avoid conflicts) */}\n <Route path=\"new\" element={<CreateWorkspacePage />} />\n\n {/* Invitation Acceptance (must come before :workspaceId to avoid conflicts) */}\n <Route path=\"invite/:invitationId\" element={<InviteAcceptancePage />} />\n\n {/* Workspace Detail */}\n <Route path=\":workspaceId\" element={<WorkspaceDetailPage />} />\n <Route\n path=\":workspaceId/settings\"\n element={gated('workspaces.settings', <WorkspaceSettingsPage />, true)}\n />\n\n {/* Projects — core feature, never gated */}\n <Route path=\"projects\" element={<ProjectsListPage />} />\n <Route path=\":workspaceId/projects\" element={<ProjectsListPage />} />\n <Route path=\":workspaceId/projects/:projectId\" element={<ProjectDetailPage />} />\n\n {/* Members — gated by workspaces.members */}\n <Route path=\"members\" element={gated('workspaces.members', <MembersListPage />, true)} />\n <Route\n path=\":workspaceId/members\"\n element={gated('workspaces.members', <MembersListPage />, true)}\n />\n\n {/* Activity — gated by workspaces.activity */}\n <Route path=\"activity\" element={gated('workspaces.activity', <ActivityListPage />)} />\n <Route\n path=\":workspaceId/activity\"\n element={gated('workspaces.activity', <ActivityListPage />)}\n />\n\n {/* Catch-all redirect - uses absolute path to prevent infinite loops */}\n <Route path=\"*\" element={<CatchAllRedirect />} />\n </Routes>\n </Suspense>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"WorkspacesRoutes.js","names":[],"sources":["../src/WorkspacesRoutes.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport { Routes, Route, Navigate } from 'react-router-dom';\nimport { FeatureGate } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { useWorkspacesContext } from './providers/WorkspacesProvider';\n\n// Eager load - main entry point\nimport { WorkspacesListPage } from './pages/WorkspacesListPage';\n\n// Lazy load - secondary pages\nconst CreateWorkspacePage = lazy(() =>\n import('./pages/CreateWorkspacePage').then((m) => ({ default: m.CreateWorkspacePage }))\n);\nconst WorkspaceDetailPage = lazy(() =>\n import('./pages/WorkspaceDetailPage').then((m) => ({ default: m.WorkspaceDetailPage }))\n);\nconst WorkspaceSettingsPage = lazy(() =>\n import('./pages/WorkspaceSettingsPage').then((m) => ({ default: m.WorkspaceSettingsPage }))\n);\nconst ProjectsListPage = lazy(() =>\n import('./pages/ProjectsListPage').then((m) => ({ default: m.ProjectsListPage }))\n);\nconst ProjectDetailPage = lazy(() =>\n import('./pages/ProjectDetailPage').then((m) => ({ default: m.ProjectDetailPage }))\n);\nconst MembersListPage = lazy(() =>\n import('./pages/MembersListPage').then((m) => ({ default: m.MembersListPage }))\n);\nconst ActivityListPage = lazy(() =>\n import('./pages/ActivityListPage').then((m) => ({ default: m.ActivityListPage }))\n);\nconst InviteAcceptancePage = lazy(() =>\n import('./pages/InviteAcceptancePage').then((m) => ({ default: m.InviteAcceptancePage }))\n);\nconst PendingInvitesPage = lazy(() =>\n import('./pages/PendingInvitesPage').then((m) => ({ default: m.PendingInvitesPage }))\n);\n\n/**\n * Loading fallback for lazy-loaded components\n */\nfunction PageLoader() {\n return (\n <div className=\"flex items-center justify-center h-64\">\n <div className=\"animate-spin rounded-full size-8 border-b-2 border-primary\" />\n </div>\n );\n}\n\n/**\n * Fallback shown when a feature-gated route is disabled.\n */\nfunction FeatureDisabledFallback() {\n return (\n <div className=\"flex h-full items-center justify-center p-8 text-center\">\n <div>\n <h2 className=\"text-lg font-semibold mb-2 text-text-primary\">Feature Not Available</h2>\n <p className=\"text-sm text-text-secondary\">\n This feature is not currently available. Enable it under Feature Previews to access this\n functionality.\n </p>\n </div>\n </div>\n );\n}\n\n/**\n * Helper: wraps a page element in a FeatureGate.\n * defaultValue=false → hidden until explicitly enabled (unreleased feature).\n */\nfunction gated(flag: string, element: React.ReactNode, defaultValue = false): React.ReactElement {\n return (\n <FeatureGate flag={flag} defaultValue={defaultValue} fallback={<FeatureDisabledFallback />}>\n {element}\n </FeatureGate>\n );\n}\n\n/**\n * Catch-all redirect component that uses the basePath from context\n * to navigate to an absolute path, preventing infinite redirect loops.\n */\nfunction CatchAllRedirect() {\n const { basePath = '/workspaces' } = useWorkspacesContext();\n return <Navigate to={`${basePath}/list`} replace />;\n}\n\n/**\n * Internal routing for the Workspaces microfrontend.\n * Defines all routes and their corresponding page components.\n *\n * - WorkspacesListPage: Eager loaded (main entry point)\n * - All other pages: Lazy loaded for better performance\n * - Projects, Members, Activity: gated by feature flags (unreleased)\n */\nexport function WorkspacesRoutes() {\n return (\n <Suspense fallback={<PageLoader />}>\n <Routes>\n {/* Default redirect */}\n <Route index element={<Navigate to=\"list\" replace />} />\n\n {/* Workspaces List - Eager loaded */}\n <Route path=\"list\" element={<WorkspacesListPage />} />\n\n {/* Create Workspace (must come before :workspaceId to avoid conflicts) */}\n <Route path=\"new\" element={<CreateWorkspacePage />} />\n\n {/* Pending Invites (must come before :workspaceId to avoid conflicts) */}\n <Route path=\"invites\" element={<PendingInvitesPage />} />\n\n {/* Invitation Acceptance (must come before :workspaceId to avoid conflicts) */}\n <Route path=\"invite/:invitationId\" element={<InviteAcceptancePage />} />\n\n {/* Workspace Detail */}\n <Route path=\":workspaceId\" element={<WorkspaceDetailPage />} />\n <Route\n path=\":workspaceId/settings\"\n element={gated('workspaces.settings', <WorkspaceSettingsPage />, true)}\n />\n\n {/* Projects — core feature, never gated */}\n <Route path=\"projects\" element={<ProjectsListPage />} />\n <Route path=\":workspaceId/projects\" element={<ProjectsListPage />} />\n <Route path=\":workspaceId/projects/:projectId\" element={<ProjectDetailPage />} />\n\n {/* Members — gated by workspaces.members */}\n <Route path=\"members\" element={gated('workspaces.members', <MembersListPage />, true)} />\n <Route\n path=\":workspaceId/members\"\n element={gated('workspaces.members', <MembersListPage />, true)}\n />\n\n {/* Activity — gated by workspaces.activity */}\n <Route path=\"activity\" element={gated('workspaces.activity', <ActivityListPage />)} />\n <Route\n path=\":workspaceId/activity\"\n element={gated('workspaces.activity', <ActivityListPage />)}\n />\n\n {/* Catch-all redirect - uses absolute path to prevent infinite loops */}\n <Route path=\"*\" element={<CatchAllRedirect />} />\n </Routes>\n </Suspense>\n );\n}\n"],"mappings":"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"}
@@ -4,7 +4,7 @@ import { useScopedNavigation as n } from "../hooks/useScopedNavigation.js";
4
4
  import { nativeConfirm as r, nativeImpact as i } from "../utils/nativeBridge.js";
5
5
  import { useArchiveProject as a, useDeleteProject as o, useUnarchiveProject as s } from "../hooks/useProjectMutations.js";
6
6
  import { formatDate as c } from "../utils/formatters.js";
7
- import { normalizeProjectTags as l } from "../utils/projectTags.js";
7
+ import { getProjectDisplayTags as l } from "../utils/projectTags.js";
8
8
  import { Archive as u, FolderKanban as d, MoreVertical as f } from "lucide-react";
9
9
  import { jsx as p, jsxs as m } from "react/jsx-runtime";
10
10
  import { Badge as h, Button as g, Card as _, CardContent as v, CardHeader as y, CardTitle as b, DropdownMenu as x, DropdownMenuContent as S, DropdownMenuItem as C, DropdownMenuTrigger as w } from "@burdenoff/fe-libs/ui";
@@ -12,7 +12,7 @@ import { useNavigate as T } from "react-router-dom";
12
12
  import { TagBadge as E } from "@burdenoff/fe-libs/tag";
13
13
  //#region src/components/ProjectCard.tsx
14
14
  function D({ project: D, selected: O = !1, onToggleSelect: k, onTagClick: A, canArchive: j = !1, canDelete: M = !1 }) {
15
- let N = T(), { basePath: P = "/workspaces" } = e(), { withCurrentSearch: F } = n(), { mutate: I } = a(D.workspaceId), { mutate: L } = s(D.workspaceId), { mutate: R } = o(D.workspaceId), z = l(D.tags), B = () => {
15
+ let N = T(), { basePath: P = "/workspaces" } = e(), { withCurrentSearch: F } = n(), { mutate: I } = a(D.workspaceId), { mutate: L } = s(D.workspaceId), { mutate: R } = o(D.workspaceId), z = l(D), B = () => {
16
16
  N(F(`${P}/${D.workspaceId}/projects/${D.id}`));
17
17
  };
18
18
  return /* @__PURE__ */ m(_, {
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectCard.js","names":[],"sources":["../../src/components/ProjectCard.tsx"],"sourcesContent":["import type React from 'react';\nimport { FolderKanban, Archive, MoreVertical } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\n\nimport type { Project, ProjectTag } from '../types';\nimport { formatDate } from '../utils/formatters';\nimport { normalizeProjectTags } from '../utils/projectTags';\nimport { getProjectStatusClasses } from '../utils/tokenHelpers';\nimport {\n useArchiveProject,\n useUnarchiveProject,\n useDeleteProject,\n} from '../hooks/useProjectMutations';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport { useScopedNavigation } from '../hooks/useScopedNavigation';\nimport { nativeConfirm, nativeImpact } from '../utils/nativeBridge';\n\ninterface ProjectCardProps {\n project: Project;\n selected?: boolean;\n onToggleSelect?: (checked: boolean) => void;\n onTagClick?: (tag: ProjectTag) => void;\n canArchive?: boolean;\n canDelete?: boolean;\n}\n\nexport function ProjectCard({\n project,\n selected = false,\n onToggleSelect,\n onTagClick,\n canArchive = false,\n canDelete = false,\n}: ProjectCardProps) {\n const navigate = useNavigate();\n const { basePath = '/workspaces' } = useWorkspacesContext();\n const { withCurrentSearch } = useScopedNavigation();\n const { mutate: archiveProject } = useArchiveProject(project.workspaceId);\n const { mutate: unarchiveProject } = useUnarchiveProject(project.workspaceId);\n const { mutate: deleteProject } = useDeleteProject(project.workspaceId);\n const projectTags = normalizeProjectTags(project.tags);\n\n const handleCardClick = () => {\n navigate(withCurrentSearch(`${basePath}/${project.workspaceId}/projects/${project.id}`));\n };\n\n const handleArchive = (e: React.MouseEvent) => {\n e.stopPropagation();\n if (project.archived) {\n unarchiveProject(project.id);\n } else {\n archiveProject(project.id);\n }\n };\n\n const handleDelete = async (e: React.MouseEvent) => {\n e.stopPropagation();\n void nativeImpact('medium');\n const native = await nativeConfirm({\n title: 'Delete project',\n message: `Are you sure you want to delete \"${project.name}\"?`,\n okButtonTitle: 'Delete',\n cancelButtonTitle: 'Cancel',\n });\n const ok =\n native === null ? confirm(`Are you sure you want to delete \"${project.name}\"?`) : native;\n if (ok) {\n deleteProject(project.id);\n }\n };\n\n const handleCardKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n handleCardClick();\n }\n };\n\n return (\n <Card\n className={`cursor-pointer relative ${\n selected ? 'ring-2 ring-primary/25 border-primary/50' : ''\n }`}\n onClick={handleCardClick}\n onKeyDown={handleCardKeyDown}\n role=\"button\"\n tabIndex={0}\n aria-label={`Open project ${project.name}`}\n >\n <CardHeader>\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3 flex-1\">\n {onToggleSelect && (\n <input\n type=\"checkbox\"\n checked={selected}\n onChange={(event) => onToggleSelect(event.target.checked)}\n onClick={(event) => event.stopPropagation()}\n aria-label={`Select project ${project.name}`}\n className=\"mt-1 size-4 rounded border-border text-primary\"\n />\n )}\n <div className=\"p-3 rounded-lg bg-primary/10 transition-colors\">\n <FolderKanban className=\"text-primary\" size={24} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <CardTitle className=\"truncate\">{project.name}</CardTitle>\n <p className=\"truncate text-sm text-muted-foreground\">{project.key}</p>\n </div>\n </div>\n <DropdownMenu>\n <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>\n <Button variant=\"ghost\" size=\"sm\" className=\"size-8 p-0\">\n <MoreVertical size={16} />\n <span className=\"sr-only\">Open project actions</span>\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {canArchive && (\n <DropdownMenuItem onClick={handleArchive}>\n <Archive size={16} className=\"mr-2\" />\n {project.archived ? 'Unarchive' : 'Archive'}\n </DropdownMenuItem>\n )}\n {canDelete && (\n <DropdownMenuItem onClick={handleDelete} className=\"text-destructive\">\n Delete\n </DropdownMenuItem>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <div className=\"flex items-center gap-2\">\n <Badge className={`border ${getProjectStatusClasses(project.status)}`}>\n {project.status}\n </Badge>\n {project.archived && (\n <Badge\n variant=\"outline\"\n className=\"border-workspace-archived-border text-workspace-archived-text\"\n >\n <Archive size={12} className=\"mr-1\" />\n Archived\n </Badge>\n )}\n </div>\n <div className=\"text-xs text-muted-foreground\">Created {formatDate(project.createdAt)}</div>\n {projectTags.length > 0 && (\n <div className=\"flex flex-wrap gap-1.5\" aria-label=\"Project tags\">\n {projectTags.slice(0, 4).map((tag) => (\n <span\n key={tag.tagId}\n {...(onTagClick\n ? {\n role: 'button',\n tabIndex: 0,\n onClick: (event: React.MouseEvent<HTMLSpanElement>) => {\n event.stopPropagation();\n onTagClick(tag);\n },\n onKeyDown: (event: React.KeyboardEvent<HTMLSpanElement>) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n event.stopPropagation();\n onTagClick(tag);\n }\n },\n }\n : {})}\n >\n <TagBadge\n id={tag.tagId}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n size=\"sm\"\n className={onTagClick ? 'cursor-pointer' : undefined}\n />\n </span>\n ))}\n {projectTags.length > 4 && (\n <Badge variant=\"outline\" className=\"text-xs\">\n +{projectTags.length - 4}\n </Badge>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ProjectCard.js","names":[],"sources":["../../src/components/ProjectCard.tsx"],"sourcesContent":["import type React from 'react';\nimport { FolderKanban, Archive, MoreVertical } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\n\nimport type { Project, ProjectTag } from '../types';\nimport { formatDate } from '../utils/formatters';\nimport { getProjectDisplayTags } from '../utils/projectTags';\nimport { getProjectStatusClasses } from '../utils/tokenHelpers';\nimport {\n useArchiveProject,\n useUnarchiveProject,\n useDeleteProject,\n} from '../hooks/useProjectMutations';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport { useScopedNavigation } from '../hooks/useScopedNavigation';\nimport { nativeConfirm, nativeImpact } from '../utils/nativeBridge';\n\ninterface ProjectCardProps {\n project: Project;\n selected?: boolean;\n onToggleSelect?: (checked: boolean) => void;\n onTagClick?: (tag: ProjectTag) => void;\n canArchive?: boolean;\n canDelete?: boolean;\n}\n\nexport function ProjectCard({\n project,\n selected = false,\n onToggleSelect,\n onTagClick,\n canArchive = false,\n canDelete = false,\n}: ProjectCardProps) {\n const navigate = useNavigate();\n const { basePath = '/workspaces' } = useWorkspacesContext();\n const { withCurrentSearch } = useScopedNavigation();\n const { mutate: archiveProject } = useArchiveProject(project.workspaceId);\n const { mutate: unarchiveProject } = useUnarchiveProject(project.workspaceId);\n const { mutate: deleteProject } = useDeleteProject(project.workspaceId);\n const projectTags = getProjectDisplayTags(project);\n\n const handleCardClick = () => {\n navigate(withCurrentSearch(`${basePath}/${project.workspaceId}/projects/${project.id}`));\n };\n\n const handleArchive = (e: React.MouseEvent) => {\n e.stopPropagation();\n if (project.archived) {\n unarchiveProject(project.id);\n } else {\n archiveProject(project.id);\n }\n };\n\n const handleDelete = async (e: React.MouseEvent) => {\n e.stopPropagation();\n void nativeImpact('medium');\n const native = await nativeConfirm({\n title: 'Delete project',\n message: `Are you sure you want to delete \"${project.name}\"?`,\n okButtonTitle: 'Delete',\n cancelButtonTitle: 'Cancel',\n });\n const ok =\n native === null ? confirm(`Are you sure you want to delete \"${project.name}\"?`) : native;\n if (ok) {\n deleteProject(project.id);\n }\n };\n\n const handleCardKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n handleCardClick();\n }\n };\n\n return (\n <Card\n className={`cursor-pointer relative ${\n selected ? 'ring-2 ring-primary/25 border-primary/50' : ''\n }`}\n onClick={handleCardClick}\n onKeyDown={handleCardKeyDown}\n role=\"button\"\n tabIndex={0}\n aria-label={`Open project ${project.name}`}\n >\n <CardHeader>\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3 flex-1\">\n {onToggleSelect && (\n <input\n type=\"checkbox\"\n checked={selected}\n onChange={(event) => onToggleSelect(event.target.checked)}\n onClick={(event) => event.stopPropagation()}\n aria-label={`Select project ${project.name}`}\n className=\"mt-1 size-4 rounded border-border text-primary\"\n />\n )}\n <div className=\"p-3 rounded-lg bg-primary/10 transition-colors\">\n <FolderKanban className=\"text-primary\" size={24} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <CardTitle className=\"truncate\">{project.name}</CardTitle>\n <p className=\"truncate text-sm text-muted-foreground\">{project.key}</p>\n </div>\n </div>\n <DropdownMenu>\n <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>\n <Button variant=\"ghost\" size=\"sm\" className=\"size-8 p-0\">\n <MoreVertical size={16} />\n <span className=\"sr-only\">Open project actions</span>\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {canArchive && (\n <DropdownMenuItem onClick={handleArchive}>\n <Archive size={16} className=\"mr-2\" />\n {project.archived ? 'Unarchive' : 'Archive'}\n </DropdownMenuItem>\n )}\n {canDelete && (\n <DropdownMenuItem onClick={handleDelete} className=\"text-destructive\">\n Delete\n </DropdownMenuItem>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <div className=\"flex items-center gap-2\">\n <Badge className={`border ${getProjectStatusClasses(project.status)}`}>\n {project.status}\n </Badge>\n {project.archived && (\n <Badge\n variant=\"outline\"\n className=\"border-workspace-archived-border text-workspace-archived-text\"\n >\n <Archive size={12} className=\"mr-1\" />\n Archived\n </Badge>\n )}\n </div>\n <div className=\"text-xs text-muted-foreground\">Created {formatDate(project.createdAt)}</div>\n {projectTags.length > 0 && (\n <div className=\"flex flex-wrap gap-1.5\" aria-label=\"Project tags\">\n {projectTags.slice(0, 4).map((tag) => (\n <span\n key={tag.tagId}\n {...(onTagClick\n ? {\n role: 'button',\n tabIndex: 0,\n onClick: (event: React.MouseEvent<HTMLSpanElement>) => {\n event.stopPropagation();\n onTagClick(tag);\n },\n onKeyDown: (event: React.KeyboardEvent<HTMLSpanElement>) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n event.stopPropagation();\n onTagClick(tag);\n }\n },\n }\n : {})}\n >\n <TagBadge\n id={tag.tagId}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n size=\"sm\"\n className={onTagClick ? 'cursor-pointer' : undefined}\n />\n </span>\n ))}\n {projectTags.length > 4 && (\n <Badge variant=\"outline\" className=\"text-xs\">\n +{projectTags.length - 4}\n </Badge>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
@@ -1,14 +1,14 @@
1
1
  import { useCallback as e, useEffect as t, useRef as n, useState as r } from "react";
2
- import { Loader2 as i, Plus as a, Search as ee, Tag as te, X as o } from "lucide-react";
3
- import { jsx as s, jsxs as c } from "react/jsx-runtime";
4
- import { Command as l, CommandEmpty as u, CommandGroup as d, CommandInput as f, CommandItem as p, CommandList as m, Popover as ne, PopoverContent as re, PopoverTrigger as ie } from "@burdenoff/fe-libs/ui";
2
+ import { Loader2 as i, Plus as ee, Tag as a, X as te } from "lucide-react";
3
+ import { jsx as o, jsxs as s } from "react/jsx-runtime";
4
+ import { Command as ne, CommandEmpty as c, CommandGroup as l, CommandInput as u, CommandItem as d, CommandList as f, Popover as p, PopoverContent as m, PopoverTrigger as re } from "@burdenoff/fe-libs/ui";
5
5
  import { cn as h } from "@burdenoff/fe-libs/shared/utils";
6
- import { useGatewayClient as ae } from "@burdenoff/fe-libs/shared/graphql";
6
+ import { useGatewayClient as ie } from "@burdenoff/fe-libs/shared/graphql";
7
7
  import { gql as g } from "@apollo/client";
8
8
  import { useLazyQuery as _, useMutation as v } from "@apollo/client/react";
9
- import { TagBadge as oe } from "@burdenoff/fe-libs/tag";
9
+ import { TagBadge as y } from "@burdenoff/fe-libs/tag";
10
10
  //#region src/components/tags/ProjectTagsWidget.tsx
11
- var se = g`
11
+ var b = g`
12
12
  query ProjectTagsWidget_SearchTags($query: String!, $vocabularyId: ID, $limit: Int) {
13
13
  searchTags(query: $query, vocabularyId: $vocabularyId, limit: $limit) {
14
14
  id
@@ -26,7 +26,7 @@ var se = g`
26
26
  }
27
27
  }
28
28
  }
29
- `, ce = g`
29
+ `, x = g`
30
30
  query ProjectTagsWidget_GetVocabularyByKey($key: String!) {
31
31
  getVocabularyByKey(key: $key) {
32
32
  id
@@ -34,7 +34,7 @@ var se = g`
34
34
  name
35
35
  }
36
36
  }
37
- `, y = g`
37
+ `, ae = g`
38
38
  mutation ProjectTagsWidget_CreateVocabulary($input: CreateVocabularyInput!) {
39
39
  createVocabulary(input: $input) {
40
40
  id
@@ -42,7 +42,7 @@ var se = g`
42
42
  name
43
43
  }
44
44
  }
45
- `, b = g`
45
+ `, oe = g`
46
46
  mutation ProjectTagsWidget_CreateTag($input: CreateTagInput!) {
47
47
  createTag(input: $input) {
48
48
  id
@@ -59,19 +59,19 @@ var se = g`
59
59
  }
60
60
  }
61
61
  }
62
- `, x = "general";
63
- function S(e) {
62
+ `, S = "general";
63
+ function C(e) {
64
64
  if (!e || typeof e != "object") return !1;
65
65
  let t = e;
66
66
  return typeof t.id == "string" && typeof t.key == "string" && typeof t.label == "string";
67
67
  }
68
- function C({ entityType: g, entityId: C, workspaceId: w, tags: T, onAdd: E, onRemove: D, allowCreate: O = !0, maxTags: k = 20, className: le }) {
69
- let [ue, A] = r(!1), [j, M] = r(""), [N, P] = r(null), [F, I] = r(!1), [L, R] = r(), z = n(null), B = ae("workspace"), V = !w || !C, H = new Set(T.map((e) => e.id)), U = T.length < k, [W, { data: de, loading: G }] = _(se, { client: B }), K = (de?.searchTags ?? []).filter((e) => S(e) && !H.has(e.id)), [q, { loading: J }] = v(b, { client: B }), [Y] = _(ce, { client: B }), [X] = v(y, { client: B }), Z = e(async () => {
68
+ function w({ entityType: g, entityId: w, workspaceId: se, tags: T, onAdd: E, onRemove: D, allowCreate: O = !0, maxTags: k = 20, className: ce }) {
69
+ let [le, A] = r(!1), [j, M] = r(""), [N, P] = r(null), [F, I] = r(!1), [L, R] = r(), z = n(null), B = ie("workspace"), V = !se || !w, H = new Set(T.map((e) => e.id)), U = T.length < k, [W, { data: ue, loading: G }] = _(b, { client: B }), K = (ue?.searchTags ?? []).filter((e) => C(e) && !H.has(e.id)), [q, { loading: J }] = v(oe, { client: B }), [Y] = _(x, { client: B }), [X] = v(ae, { client: B }), Z = e(async () => {
70
70
  if (L) return L;
71
- let e = (await Y({ variables: { key: x } })).data?.getVocabularyByKey;
71
+ let e = (await Y({ variables: { key: S } })).data?.getVocabularyByKey;
72
72
  if (e) return R(e.id), e.id;
73
73
  let t = (await X({ variables: { input: {
74
- key: x,
74
+ key: S,
75
75
  name: "General",
76
76
  description: "Default vocabulary for general-purpose tags",
77
77
  scope: "WORKSPACE",
@@ -83,7 +83,7 @@ function C({ entityType: g, entityId: C, workspaceId: w, tags: T, onAdd: E, onRe
83
83
  L,
84
84
  Y,
85
85
  X
86
- ]), fe = e((e) => {
86
+ ]), de = e((e) => {
87
87
  M(e), N && P(null), z.current && clearTimeout(z.current), e.trim() && (z.current = setTimeout(() => {
88
88
  W({ variables: {
89
89
  query: e,
@@ -115,7 +115,7 @@ function C({ entityType: g, entityId: C, workspaceId: w, tags: T, onAdd: E, onRe
115
115
  E,
116
116
  U,
117
117
  F
118
- ]), pe = e(async () => {
118
+ ]), fe = e(async () => {
119
119
  if (!(!O || !j.trim() || J)) {
120
120
  P(null);
121
121
  try {
@@ -129,7 +129,7 @@ function C({ entityType: g, entityId: C, workspaceId: w, tags: T, onAdd: E, onRe
129
129
  label: e,
130
130
  vocabularyId: await Z()
131
131
  } } })).data?.createTag;
132
- if (!S(n)) throw Error("No data returned");
132
+ if (!C(n)) throw Error("No data returned");
133
133
  await Q(n);
134
134
  } catch (e) {
135
135
  P(e instanceof Error ? e.message : "Failed to create tag");
@@ -142,100 +142,96 @@ function C({ entityType: g, entityId: C, workspaceId: w, tags: T, onAdd: E, onRe
142
142
  Z,
143
143
  q,
144
144
  Q
145
- ]), me = e(async (e) => {
145
+ ]), pe = e(async (e) => {
146
146
  await D(e);
147
147
  }, [D]);
148
148
  if (V) return null;
149
149
  let $ = !!(O && j.trim() && !G && K.length === 0);
150
- return /* @__PURE__ */ c("div", {
151
- className: h("flex flex-wrap items-center gap-1.5", le),
152
- children: [T.map((e) => /* @__PURE__ */ s(oe, {
150
+ return /* @__PURE__ */ s("div", {
151
+ className: h("flex flex-wrap items-center gap-1.5", ce),
152
+ children: [T.map((e) => /* @__PURE__ */ o(y, {
153
153
  id: e.id,
154
154
  label: e.label,
155
155
  color: e.color ?? void 0,
156
156
  icon: e.icon ?? void 0,
157
157
  description: e.description ?? void 0,
158
158
  removable: !0,
159
- onRemove: () => void me(e.id),
159
+ onRemove: () => void pe(e.id),
160
160
  size: "sm"
161
- }, e.id)), U && /* @__PURE__ */ c(ne, {
162
- open: ue,
161
+ }, e.id)), U && /* @__PURE__ */ s(p, {
162
+ open: le,
163
163
  onOpenChange: A,
164
- children: [/* @__PURE__ */ c(ie, {
164
+ children: [/* @__PURE__ */ s(re, {
165
165
  type: "button",
166
166
  "aria-label": "Add tag",
167
167
  className: h("inline-flex items-center gap-1 px-2 py-0.5 rounded-full border border-dashed", "border-border text-muted-foreground text-xs", "hover:border-foreground hover:text-foreground transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"),
168
- children: [/* @__PURE__ */ s(te, { className: "size-3" }), /* @__PURE__ */ s("span", { children: T.length > 0 ? "Add" : "Add tags" })]
169
- }), /* @__PURE__ */ s(re, {
170
- className: "w-64 p-0",
168
+ children: [/* @__PURE__ */ o(a, { className: "size-3" }), /* @__PURE__ */ o("span", { children: T.length > 0 ? "Add" : "Add tags" })]
169
+ }), /* @__PURE__ */ o(m, {
170
+ className: "z-50 w-72 max-w-[calc(100vw-2rem)] border-border/80 bg-popover p-0 shadow-xl",
171
171
  align: "start",
172
172
  side: "bottom",
173
- sideOffset: 4,
174
- children: /* @__PURE__ */ c(l, {
173
+ sideOffset: 8,
174
+ children: /* @__PURE__ */ s(ne, {
175
175
  shouldFilter: !1,
176
176
  children: [
177
- /* @__PURE__ */ c("div", {
178
- className: "flex items-center border-b border-border px-3",
179
- children: [
180
- /* @__PURE__ */ s(ee, { className: "size-4 shrink-0 text-muted-foreground" }),
181
- /* @__PURE__ */ s(f, {
182
- placeholder: "Search or create a tag...",
183
- value: j,
184
- onValueChange: fe,
185
- className: "h-9 flex-1 border-0 bg-transparent px-2 text-sm outline-none placeholder:text-muted-foreground"
186
- }),
187
- G && /* @__PURE__ */ s(i, { className: "size-3.5 shrink-0 animate-spin text-muted-foreground" })
188
- ]
177
+ /* @__PURE__ */ s("div", {
178
+ className: "relative",
179
+ children: [/* @__PURE__ */ o(u, {
180
+ placeholder: "Search or create a tag...",
181
+ value: j,
182
+ onValueChange: de,
183
+ className: "h-10 border-0 bg-transparent pr-9 text-sm outline-none placeholder:text-muted-foreground"
184
+ }), G && /* @__PURE__ */ o(i, { className: "absolute right-3 top-1/2 size-3.5 -translate-y-1/2 animate-spin text-muted-foreground" })]
189
185
  }),
190
- /* @__PURE__ */ c(m, {
186
+ /* @__PURE__ */ s(f, {
191
187
  className: "max-h-48 overflow-y-auto",
192
188
  children: [
193
- j.trim() === "" && /* @__PURE__ */ s(u, {
189
+ j.trim() === "" && /* @__PURE__ */ o(c, {
194
190
  className: "py-4 text-center text-xs text-muted-foreground",
195
191
  children: "Type to search or create a tag"
196
192
  }),
197
- K.length > 0 && /* @__PURE__ */ s(d, { children: K.map((e) => /* @__PURE__ */ c(p, {
193
+ K.length > 0 && /* @__PURE__ */ o(l, { children: K.map((e) => /* @__PURE__ */ s(d, {
198
194
  value: e.id,
199
195
  onSelect: () => void Q(e),
200
196
  className: "flex items-center gap-2 px-3 py-2 cursor-pointer",
201
197
  disabled: F,
202
198
  children: [
203
- e.icon && /* @__PURE__ */ s("span", {
199
+ e.icon && /* @__PURE__ */ o("span", {
204
200
  className: "text-sm",
205
201
  children: e.icon
206
202
  }),
207
- /* @__PURE__ */ s("span", {
203
+ /* @__PURE__ */ o("span", {
208
204
  className: "truncate text-sm",
209
205
  style: e.color ? { color: e.color } : void 0,
210
206
  children: e.label
211
207
  }),
212
- F && /* @__PURE__ */ s(i, { className: "ml-auto size-3 animate-spin" })
208
+ F && /* @__PURE__ */ o(i, { className: "ml-auto size-3 animate-spin" })
213
209
  ]
214
210
  }, e.id)) }),
215
- $ && /* @__PURE__ */ s(d, { children: /* @__PURE__ */ c(p, {
211
+ $ && /* @__PURE__ */ o(l, { children: /* @__PURE__ */ s(d, {
216
212
  value: `__create__${j}`,
217
- onSelect: () => void pe(),
213
+ onSelect: () => void fe(),
218
214
  className: "flex items-center gap-2 px-3 py-2 cursor-pointer text-primary",
219
215
  disabled: J,
220
- children: [J ? /* @__PURE__ */ s(i, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ s(a, { className: "size-3.5" }), /* @__PURE__ */ s("span", {
216
+ children: [J ? /* @__PURE__ */ o(i, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ o(ee, { className: "size-3.5" }), /* @__PURE__ */ o("span", {
221
217
  className: "text-sm truncate",
222
218
  children: J ? "Creating..." : `Create "${j}"`
223
219
  })]
224
220
  }) }),
225
- j.trim() !== "" && !G && K.length === 0 && !$ && /* @__PURE__ */ s(u, {
221
+ j.trim() !== "" && !G && K.length === 0 && !$ && /* @__PURE__ */ o(c, {
226
222
  className: "py-4 text-center text-xs text-muted-foreground",
227
223
  children: "No tags found"
228
224
  }),
229
- N && /* @__PURE__ */ s("div", {
225
+ N && /* @__PURE__ */ o("div", {
230
226
  className: "px-3 py-2 text-xs text-destructive border-t border-border",
231
227
  children: N
232
228
  })
233
229
  ]
234
230
  }),
235
- !U && /* @__PURE__ */ c("div", {
231
+ !U && /* @__PURE__ */ s("div", {
236
232
  className: "border-t border-border px-3 py-2 text-xs text-muted-foreground flex items-center gap-1",
237
233
  children: [
238
- /* @__PURE__ */ s(o, { className: "size-3" }),
234
+ /* @__PURE__ */ o(te, { className: "size-3" }),
239
235
  "Maximum ",
240
236
  k,
241
237
  " tags reached"
@@ -248,6 +244,6 @@ function C({ entityType: g, entityId: C, workspaceId: w, tags: T, onAdd: E, onRe
248
244
  });
249
245
  }
250
246
  //#endregion
251
- export { C as ProjectTagsWidget };
247
+ export { w as ProjectTagsWidget };
252
248
 
253
249
  //# sourceMappingURL=ProjectTagsWidget.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectTagsWidget.js","names":[],"sources":["../../../src/components/tags/ProjectTagsWidget.tsx"],"sourcesContent":["/**\n * ProjectTagsWidget\n *\n * Local replacement for `@burdenoff/fe-libs/tag` `TagsWidget`, used until\n * fe-libs ships a fix for the non-clickable `AddTagTrigger` (the published\n * trigger swallows the props Radix Slot tries to forward via\n * `<PopoverTrigger asChild>`, so the popover never opens).\n *\n * This component bypasses the broken trigger by rendering a regular\n * `<PopoverTrigger>` (no `asChild`) so Radix mounts the click-handling\n * button itself. Internals (search, create, assign, remove) mirror the\n * fe-libs `TagsWidget` so the data path through wspace-tags-svc is\n * identical.\n *\n * TODO(fe-libs): once `@burdenoff/fe-libs/tag` `AddTagTrigger` forwards\n * props/ref to the underlying button, delete this file and switch back\n * to `<TagsWidget>` from `@burdenoff/fe-libs/tag`.\n */\n\nimport { gql, type ApolloClient } from '@apollo/client';\nimport { useLazyQuery, useMutation } from '@apollo/client/react';\nimport { Loader2, Plus, Search, Tag as TagIcon, X } from 'lucide-react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { type TagItem } from '@burdenoff/fe-libs/tag';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { useGatewayClient } from '@burdenoff/fe-libs/shared/graphql';\nimport { cn } from '@burdenoff/fe-libs/shared/utils';\n\n// --- GraphQL ----------------------------------------------------------------\n\ninterface VocabResult {\n id: string;\n key: string;\n name: string;\n}\n\nconst SEARCH_TAGS = gql`\n query ProjectTagsWidget_SearchTags($query: String!, $vocabularyId: ID, $limit: Int) {\n searchTags(query: $query, vocabularyId: $vocabularyId, limit: $limit) {\n id\n key\n label\n value\n vocabularyId\n parentId\n hierarchyPath\n color\n icon\n description\n vocabulary {\n key\n }\n }\n }\n`;\n\nconst GET_VOCABULARY_BY_KEY = gql`\n query ProjectTagsWidget_GetVocabularyByKey($key: String!) {\n getVocabularyByKey(key: $key) {\n id\n key\n name\n }\n }\n`;\n\nconst CREATE_VOCABULARY = gql`\n mutation ProjectTagsWidget_CreateVocabulary($input: CreateVocabularyInput!) {\n createVocabulary(input: $input) {\n id\n key\n name\n }\n }\n`;\n\nconst CREATE_TAG = gql`\n mutation ProjectTagsWidget_CreateTag($input: CreateTagInput!) {\n createTag(input: $input) {\n id\n key\n label\n value\n vocabularyId\n parentId\n hierarchyPath\n color\n icon\n vocabulary {\n key\n }\n }\n }\n`;\n\nconst DEFAULT_VOCAB_KEY = 'general';\n\nfunction isTagItem(value: unknown): value is TagItem {\n if (!value || typeof value !== 'object') return false;\n const tag = value as Partial<Record<keyof TagItem, unknown>>;\n return typeof tag.id === 'string' && typeof tag.key === 'string' && typeof tag.label === 'string';\n}\n\n// --- Props ------------------------------------------------------------------\n\nexport interface ProjectTagsWidgetProps {\n /** Entity type recorded on the assigned-tag projection (e.g. \"Project\"). */\n entityType: string;\n /** Entity id (project id). */\n entityId: string;\n /** Workspace scope — widget renders nothing when empty. */\n workspaceId?: string;\n /** Controlled tag list owned by the parent. */\n tags: TagItem[];\n /** Persist a newly assigned tag on the entity. */\n onAdd: (tag: TagItem) => void | Promise<void>;\n /** Persist a tag removal on the entity. */\n onRemove: (tagId: string) => void | Promise<void>;\n /** Allow creating new tags on the fly. Default: true. */\n allowCreate?: boolean;\n /** Cap on assigned tags. Default 20. */\n maxTags?: number;\n className?: string;\n}\n\n// --- Component --------------------------------------------------------------\n\nexport function ProjectTagsWidget({\n entityType: _entityType,\n entityId,\n workspaceId,\n tags,\n onAdd,\n onRemove,\n allowCreate = true,\n maxTags = 20,\n className,\n}: ProjectTagsWidgetProps) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const [createError, setCreateError] = useState<string | null>(null);\n const [adding, setAdding] = useState(false);\n const [resolvedVocabId, setResolvedVocabId] = useState<string | undefined>();\n const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Bun may install Apollo twice when fe-libs is consumed as a package; the runtime client is compatible.\n const wsClient = useGatewayClient('workspace') as unknown as ApolloClient;\n\n const skip = !workspaceId || !entityId;\n\n const assignedTagIds = new Set(tags.map((t) => t.id));\n const canAddMore = tags.length < maxTags;\n\n const [runSearch, { data: searchData, loading: searching }] = useLazyQuery<{\n searchTags: TagItem[];\n }>(SEARCH_TAGS, { client: wsClient });\n\n const searchResults = (searchData?.searchTags ?? []).filter(\n (tag): tag is TagItem => isTagItem(tag) && !assignedTagIds.has(tag.id)\n );\n\n const [createTag, { loading: creating }] = useMutation<{\n createTag: TagItem;\n }>(CREATE_TAG, { client: wsClient });\n\n const [fetchVocabByKey] = useLazyQuery<{\n getVocabularyByKey: VocabResult | null;\n }>(GET_VOCABULARY_BY_KEY, { client: wsClient });\n\n const [createVocabulary] = useMutation<{\n createVocabulary: VocabResult;\n }>(CREATE_VOCABULARY, { client: wsClient });\n\n const resolveVocabId = useCallback(async (): Promise<string> => {\n if (resolvedVocabId) return resolvedVocabId;\n\n const res = await fetchVocabByKey({ variables: { key: DEFAULT_VOCAB_KEY } });\n const existing = res.data?.getVocabularyByKey;\n if (existing) {\n setResolvedVocabId(existing.id);\n return existing.id;\n }\n\n const created = await createVocabulary({\n variables: {\n input: {\n key: DEFAULT_VOCAB_KEY,\n name: 'General',\n description: 'Default vocabulary for general-purpose tags',\n scope: 'WORKSPACE',\n allowCustomTags: true,\n },\n },\n });\n const newVocab = created.data?.createVocabulary;\n if (!newVocab) throw new Error('Failed to create default vocabulary');\n setResolvedVocabId(newVocab.id);\n return newVocab.id;\n }, [resolvedVocabId, fetchVocabByKey, createVocabulary]);\n\n const handleSearchChange = useCallback(\n (value: string) => {\n setSearch(value);\n if (createError) setCreateError(null);\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (!value.trim()) return;\n debounceRef.current = setTimeout(() => {\n void runSearch({\n variables: { query: value, vocabularyId: resolvedVocabId, limit: 10 },\n });\n }, 250);\n },\n [runSearch, resolvedVocabId, createError]\n );\n\n useEffect(() => {\n return () => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n };\n }, []);\n\n const handleAssign = useCallback(\n async (tag: TagItem) => {\n if (!canAddMore || adding) return;\n setAdding(true);\n try {\n const normalizedTag: TagItem = {\n ...tag,\n vocabularyKey: tag.vocabularyKey ?? tag.vocabulary?.key ?? null,\n };\n await onAdd(normalizedTag);\n setSearch('');\n setOpen(false);\n } finally {\n setAdding(false);\n }\n },\n [onAdd, canAddMore, adding]\n );\n\n const handleCreate = useCallback(async () => {\n if (!allowCreate || !search.trim() || creating) return;\n setCreateError(null);\n try {\n const label = search.trim();\n const key = label\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n if (!key) {\n setCreateError('Must contain at least one letter or number');\n return;\n }\n const vocabId = await resolveVocabId();\n const res = await createTag({\n variables: { input: { key, label, vocabularyId: vocabId } },\n });\n const created = res.data?.createTag;\n if (!isTagItem(created)) throw new Error('No data returned');\n await handleAssign(created);\n } catch (err) {\n setCreateError(err instanceof Error ? err.message : 'Failed to create tag');\n }\n }, [allowCreate, search, creating, resolveVocabId, createTag, handleAssign]);\n\n const handleRemove = useCallback(\n async (tagId: string) => {\n await onRemove(tagId);\n },\n [onRemove]\n );\n\n if (skip) return null;\n\n const showCreateOption = Boolean(\n allowCreate && search.trim() && !searching && searchResults.length === 0\n );\n\n return (\n <div className={cn('flex flex-wrap items-center gap-1.5', className)}>\n {tags.map((tag) => (\n <TagBadge\n key={tag.id}\n id={tag.id}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n removable\n onRemove={() => void handleRemove(tag.id)}\n size=\"sm\"\n />\n ))}\n\n {canAddMore && (\n <Popover open={open} onOpenChange={setOpen}>\n {/*\n Intentionally do NOT use `asChild` here — the fe-libs\n `AddTagTrigger` swallows the props Radix forwards, leaving the\n trigger non-clickable. Letting Radix render its own button\n restores the click handler.\n */}\n <PopoverTrigger\n type=\"button\"\n aria-label=\"Add tag\"\n className={cn(\n 'inline-flex items-center gap-1 px-2 py-0.5 rounded-full border border-dashed',\n 'border-border text-muted-foreground text-xs',\n 'hover:border-foreground hover:text-foreground transition-colors',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n )}\n >\n <TagIcon className=\"size-3\" />\n <span>{tags.length > 0 ? 'Add' : 'Add tags'}</span>\n </PopoverTrigger>\n\n <PopoverContent className=\"w-64 p-0\" align=\"start\" side=\"bottom\" sideOffset={4}>\n <Command shouldFilter={false}>\n <div className=\"flex items-center border-b border-border px-3\">\n <Search className=\"size-4 shrink-0 text-muted-foreground\" />\n <CommandInput\n placeholder=\"Search or create a tag...\"\n value={search}\n onValueChange={handleSearchChange}\n className=\"h-9 flex-1 border-0 bg-transparent px-2 text-sm outline-none placeholder:text-muted-foreground\"\n />\n {searching && (\n <Loader2 className=\"size-3.5 shrink-0 animate-spin text-muted-foreground\" />\n )}\n </div>\n\n <CommandList className=\"max-h-48 overflow-y-auto\">\n {search.trim() === '' && (\n <CommandEmpty className=\"py-4 text-center text-xs text-muted-foreground\">\n Type to search or create a tag\n </CommandEmpty>\n )}\n\n {searchResults.length > 0 && (\n <CommandGroup>\n {searchResults.map((tag) => (\n <CommandItem\n key={tag.id}\n value={tag.id}\n onSelect={() => void handleAssign(tag)}\n className=\"flex items-center gap-2 px-3 py-2 cursor-pointer\"\n disabled={adding}\n >\n {tag.icon && <span className=\"text-sm\">{tag.icon}</span>}\n <span\n className=\"truncate text-sm\"\n style={tag.color ? { color: tag.color } : undefined}\n >\n {tag.label}\n </span>\n {adding && <Loader2 className=\"ml-auto size-3 animate-spin\" />}\n </CommandItem>\n ))}\n </CommandGroup>\n )}\n\n {showCreateOption && (\n <CommandGroup>\n <CommandItem\n value={`__create__${search}`}\n onSelect={() => void handleCreate()}\n className=\"flex items-center gap-2 px-3 py-2 cursor-pointer text-primary\"\n disabled={creating}\n >\n {creating ? (\n <Loader2 className=\"size-3.5 animate-spin\" />\n ) : (\n <Plus className=\"size-3.5\" />\n )}\n <span className=\"text-sm truncate\">\n {creating ? 'Creating...' : `Create \"${search}\"`}\n </span>\n </CommandItem>\n </CommandGroup>\n )}\n\n {search.trim() !== '' &&\n !searching &&\n searchResults.length === 0 &&\n !showCreateOption && (\n <CommandEmpty className=\"py-4 text-center text-xs text-muted-foreground\">\n No tags found\n </CommandEmpty>\n )}\n\n {createError && (\n <div className=\"px-3 py-2 text-xs text-destructive border-t border-border\">\n {createError}\n </div>\n )}\n </CommandList>\n\n {!canAddMore && (\n <div className=\"border-t border-border px-3 py-2 text-xs text-muted-foreground flex items-center gap-1\">\n <X className=\"size-3\" />\n Maximum {maxTags} tags reached\n </div>\n )}\n </Command>\n </PopoverContent>\n </Popover>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ProjectTagsWidget.js","names":[],"sources":["../../../src/components/tags/ProjectTagsWidget.tsx"],"sourcesContent":["/**\n * ProjectTagsWidget\n *\n * Local replacement for `@burdenoff/fe-libs/tag` `TagsWidget`, used until\n * fe-libs ships a fix for the non-clickable `AddTagTrigger` (the published\n * trigger swallows the props Radix Slot tries to forward via\n * `<PopoverTrigger asChild>`, so the popover never opens).\n *\n * This component bypasses the broken trigger by rendering a regular\n * `<PopoverTrigger>` (no `asChild`) so Radix mounts the click-handling\n * button itself. Internals (search, create, assign, remove) mirror the\n * fe-libs `TagsWidget` so the data path through wspace-tags-svc is\n * identical.\n *\n * TODO(fe-libs): once `@burdenoff/fe-libs/tag` `AddTagTrigger` forwards\n * props/ref to the underlying button, delete this file and switch back\n * to `<TagsWidget>` from `@burdenoff/fe-libs/tag`.\n */\n\nimport { gql, type ApolloClient } from '@apollo/client';\nimport { useLazyQuery, useMutation } from '@apollo/client/react';\nimport { Loader2, Plus, Tag as TagIcon, X } from 'lucide-react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { type TagItem } from '@burdenoff/fe-libs/tag';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { useGatewayClient } from '@burdenoff/fe-libs/shared/graphql';\nimport { cn } from '@burdenoff/fe-libs/shared/utils';\n\n// --- GraphQL ----------------------------------------------------------------\n\ninterface VocabResult {\n id: string;\n key: string;\n name: string;\n}\n\nconst SEARCH_TAGS = gql`\n query ProjectTagsWidget_SearchTags($query: String!, $vocabularyId: ID, $limit: Int) {\n searchTags(query: $query, vocabularyId: $vocabularyId, limit: $limit) {\n id\n key\n label\n value\n vocabularyId\n parentId\n hierarchyPath\n color\n icon\n description\n vocabulary {\n key\n }\n }\n }\n`;\n\nconst GET_VOCABULARY_BY_KEY = gql`\n query ProjectTagsWidget_GetVocabularyByKey($key: String!) {\n getVocabularyByKey(key: $key) {\n id\n key\n name\n }\n }\n`;\n\nconst CREATE_VOCABULARY = gql`\n mutation ProjectTagsWidget_CreateVocabulary($input: CreateVocabularyInput!) {\n createVocabulary(input: $input) {\n id\n key\n name\n }\n }\n`;\n\nconst CREATE_TAG = gql`\n mutation ProjectTagsWidget_CreateTag($input: CreateTagInput!) {\n createTag(input: $input) {\n id\n key\n label\n value\n vocabularyId\n parentId\n hierarchyPath\n color\n icon\n vocabulary {\n key\n }\n }\n }\n`;\n\nconst DEFAULT_VOCAB_KEY = 'general';\n\nfunction isTagItem(value: unknown): value is TagItem {\n if (!value || typeof value !== 'object') return false;\n const tag = value as Partial<Record<keyof TagItem, unknown>>;\n return typeof tag.id === 'string' && typeof tag.key === 'string' && typeof tag.label === 'string';\n}\n\n// --- Props ------------------------------------------------------------------\n\nexport interface ProjectTagsWidgetProps {\n /** Entity type recorded on the assigned-tag projection (e.g. \"Project\"). */\n entityType: string;\n /** Entity id (project id). */\n entityId: string;\n /** Workspace scope — widget renders nothing when empty. */\n workspaceId?: string;\n /** Controlled tag list owned by the parent. */\n tags: TagItem[];\n /** Persist a newly assigned tag on the entity. */\n onAdd: (tag: TagItem) => void | Promise<void>;\n /** Persist a tag removal on the entity. */\n onRemove: (tagId: string) => void | Promise<void>;\n /** Allow creating new tags on the fly. Default: true. */\n allowCreate?: boolean;\n /** Cap on assigned tags. Default 20. */\n maxTags?: number;\n className?: string;\n}\n\n// --- Component --------------------------------------------------------------\n\nexport function ProjectTagsWidget({\n entityType: _entityType,\n entityId,\n workspaceId,\n tags,\n onAdd,\n onRemove,\n allowCreate = true,\n maxTags = 20,\n className,\n}: ProjectTagsWidgetProps) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const [createError, setCreateError] = useState<string | null>(null);\n const [adding, setAdding] = useState(false);\n const [resolvedVocabId, setResolvedVocabId] = useState<string | undefined>();\n const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Bun may install Apollo twice when fe-libs is consumed as a package; the runtime client is compatible.\n const wsClient = useGatewayClient('workspace') as unknown as ApolloClient;\n\n const skip = !workspaceId || !entityId;\n\n const assignedTagIds = new Set(tags.map((t) => t.id));\n const canAddMore = tags.length < maxTags;\n\n const [runSearch, { data: searchData, loading: searching }] = useLazyQuery<{\n searchTags: TagItem[];\n }>(SEARCH_TAGS, { client: wsClient });\n\n const searchResults = (searchData?.searchTags ?? []).filter(\n (tag): tag is TagItem => isTagItem(tag) && !assignedTagIds.has(tag.id)\n );\n\n const [createTag, { loading: creating }] = useMutation<{\n createTag: TagItem;\n }>(CREATE_TAG, { client: wsClient });\n\n const [fetchVocabByKey] = useLazyQuery<{\n getVocabularyByKey: VocabResult | null;\n }>(GET_VOCABULARY_BY_KEY, { client: wsClient });\n\n const [createVocabulary] = useMutation<{\n createVocabulary: VocabResult;\n }>(CREATE_VOCABULARY, { client: wsClient });\n\n const resolveVocabId = useCallback(async (): Promise<string> => {\n if (resolvedVocabId) return resolvedVocabId;\n\n const res = await fetchVocabByKey({ variables: { key: DEFAULT_VOCAB_KEY } });\n const existing = res.data?.getVocabularyByKey;\n if (existing) {\n setResolvedVocabId(existing.id);\n return existing.id;\n }\n\n const created = await createVocabulary({\n variables: {\n input: {\n key: DEFAULT_VOCAB_KEY,\n name: 'General',\n description: 'Default vocabulary for general-purpose tags',\n scope: 'WORKSPACE',\n allowCustomTags: true,\n },\n },\n });\n const newVocab = created.data?.createVocabulary;\n if (!newVocab) throw new Error('Failed to create default vocabulary');\n setResolvedVocabId(newVocab.id);\n return newVocab.id;\n }, [resolvedVocabId, fetchVocabByKey, createVocabulary]);\n\n const handleSearchChange = useCallback(\n (value: string) => {\n setSearch(value);\n if (createError) setCreateError(null);\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (!value.trim()) return;\n debounceRef.current = setTimeout(() => {\n void runSearch({\n variables: { query: value, vocabularyId: resolvedVocabId, limit: 10 },\n });\n }, 250);\n },\n [runSearch, resolvedVocabId, createError]\n );\n\n useEffect(() => {\n return () => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n };\n }, []);\n\n const handleAssign = useCallback(\n async (tag: TagItem) => {\n if (!canAddMore || adding) return;\n setAdding(true);\n try {\n const normalizedTag: TagItem = {\n ...tag,\n vocabularyKey: tag.vocabularyKey ?? tag.vocabulary?.key ?? null,\n };\n await onAdd(normalizedTag);\n setSearch('');\n setOpen(false);\n } finally {\n setAdding(false);\n }\n },\n [onAdd, canAddMore, adding]\n );\n\n const handleCreate = useCallback(async () => {\n if (!allowCreate || !search.trim() || creating) return;\n setCreateError(null);\n try {\n const label = search.trim();\n const key = label\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n if (!key) {\n setCreateError('Must contain at least one letter or number');\n return;\n }\n const vocabId = await resolveVocabId();\n const res = await createTag({\n variables: { input: { key, label, vocabularyId: vocabId } },\n });\n const created = res.data?.createTag;\n if (!isTagItem(created)) throw new Error('No data returned');\n await handleAssign(created);\n } catch (err) {\n setCreateError(err instanceof Error ? err.message : 'Failed to create tag');\n }\n }, [allowCreate, search, creating, resolveVocabId, createTag, handleAssign]);\n\n const handleRemove = useCallback(\n async (tagId: string) => {\n await onRemove(tagId);\n },\n [onRemove]\n );\n\n if (skip) return null;\n\n const showCreateOption = Boolean(\n allowCreate && search.trim() && !searching && searchResults.length === 0\n );\n\n return (\n <div className={cn('flex flex-wrap items-center gap-1.5', className)}>\n {tags.map((tag) => (\n <TagBadge\n key={tag.id}\n id={tag.id}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n removable\n onRemove={() => void handleRemove(tag.id)}\n size=\"sm\"\n />\n ))}\n\n {canAddMore && (\n <Popover open={open} onOpenChange={setOpen}>\n {/*\n Intentionally do NOT use `asChild` here — the fe-libs\n `AddTagTrigger` swallows the props Radix forwards, leaving the\n trigger non-clickable. Letting Radix render its own button\n restores the click handler.\n */}\n <PopoverTrigger\n type=\"button\"\n aria-label=\"Add tag\"\n className={cn(\n 'inline-flex items-center gap-1 px-2 py-0.5 rounded-full border border-dashed',\n 'border-border text-muted-foreground text-xs',\n 'hover:border-foreground hover:text-foreground transition-colors',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n )}\n >\n <TagIcon className=\"size-3\" />\n <span>{tags.length > 0 ? 'Add' : 'Add tags'}</span>\n </PopoverTrigger>\n\n <PopoverContent\n className=\"z-50 w-72 max-w-[calc(100vw-2rem)] border-border/80 bg-popover p-0 shadow-xl\"\n align=\"start\"\n side=\"bottom\"\n sideOffset={8}\n >\n <Command shouldFilter={false}>\n <div className=\"relative\">\n <CommandInput\n placeholder=\"Search or create a tag...\"\n value={search}\n onValueChange={handleSearchChange}\n className=\"h-10 border-0 bg-transparent pr-9 text-sm outline-none placeholder:text-muted-foreground\"\n />\n {searching && (\n <Loader2 className=\"absolute right-3 top-1/2 size-3.5 -translate-y-1/2 animate-spin text-muted-foreground\" />\n )}\n </div>\n\n <CommandList className=\"max-h-48 overflow-y-auto\">\n {search.trim() === '' && (\n <CommandEmpty className=\"py-4 text-center text-xs text-muted-foreground\">\n Type to search or create a tag\n </CommandEmpty>\n )}\n\n {searchResults.length > 0 && (\n <CommandGroup>\n {searchResults.map((tag) => (\n <CommandItem\n key={tag.id}\n value={tag.id}\n onSelect={() => void handleAssign(tag)}\n className=\"flex items-center gap-2 px-3 py-2 cursor-pointer\"\n disabled={adding}\n >\n {tag.icon && <span className=\"text-sm\">{tag.icon}</span>}\n <span\n className=\"truncate text-sm\"\n style={tag.color ? { color: tag.color } : undefined}\n >\n {tag.label}\n </span>\n {adding && <Loader2 className=\"ml-auto size-3 animate-spin\" />}\n </CommandItem>\n ))}\n </CommandGroup>\n )}\n\n {showCreateOption && (\n <CommandGroup>\n <CommandItem\n value={`__create__${search}`}\n onSelect={() => void handleCreate()}\n className=\"flex items-center gap-2 px-3 py-2 cursor-pointer text-primary\"\n disabled={creating}\n >\n {creating ? (\n <Loader2 className=\"size-3.5 animate-spin\" />\n ) : (\n <Plus className=\"size-3.5\" />\n )}\n <span className=\"text-sm truncate\">\n {creating ? 'Creating...' : `Create \"${search}\"`}\n </span>\n </CommandItem>\n </CommandGroup>\n )}\n\n {search.trim() !== '' &&\n !searching &&\n searchResults.length === 0 &&\n !showCreateOption && (\n <CommandEmpty className=\"py-4 text-center text-xs text-muted-foreground\">\n No tags found\n </CommandEmpty>\n )}\n\n {createError && (\n <div className=\"px-3 py-2 text-xs text-destructive border-t border-border\">\n {createError}\n </div>\n )}\n </CommandList>\n\n {!canAddMore && (\n <div className=\"border-t border-border px-3 py-2 text-xs text-muted-foreground flex items-center gap-1\">\n <X className=\"size-3\" />\n Maximum {maxTags} tags reached\n </div>\n )}\n </Command>\n </PopoverContent>\n </Popover>\n )}\n </div>\n );\n}\n"],"mappings":"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"}