@burdenoff/microfe-comradecircle 2026.716.1 → 2026.720.1

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 (50) hide show
  1. package/dist/comradecircle/ComradeCircleRoot.js.map +1 -1
  2. package/dist/comradecircle/ComradeCircleRoutes.js.map +1 -1
  3. package/dist/comradecircle/components/Avatar.js.map +1 -1
  4. package/dist/comradecircle/components/AvatarUploadField.js.map +1 -1
  5. package/dist/comradecircle/components/CircleCard.js.map +1 -1
  6. package/dist/comradecircle/components/CommentThread.js.map +1 -1
  7. package/dist/comradecircle/components/CreateCircleDialog.js.map +1 -1
  8. package/dist/comradecircle/components/CreateEventDialog.js.map +1 -1
  9. package/dist/comradecircle/components/EmptyState.js.map +1 -1
  10. package/dist/comradecircle/components/EventCard.js.map +1 -1
  11. package/dist/comradecircle/components/FileImage.js.map +1 -1
  12. package/dist/comradecircle/components/FilterTabs.js.map +1 -1
  13. package/dist/comradecircle/components/ImageAttachments.js.map +1 -1
  14. package/dist/comradecircle/components/MemberIdentity.js.map +1 -1
  15. package/dist/comradecircle/components/MiniBar.js.map +1 -1
  16. package/dist/comradecircle/components/PageLayout.js.map +1 -1
  17. package/dist/comradecircle/components/Pagination.js.map +1 -1
  18. package/dist/comradecircle/components/PollCard.js.map +1 -1
  19. package/dist/comradecircle/components/PostCard.js.map +1 -1
  20. package/dist/comradecircle/components/PostComposer.js.map +1 -1
  21. package/dist/comradecircle/components/ReactionBar.js.map +1 -1
  22. package/dist/comradecircle/components/StateViews.js.map +1 -1
  23. package/dist/comradecircle/components/StatusBadge.js.map +1 -1
  24. package/dist/comradecircle/components/Tabs.js.map +1 -1
  25. package/dist/comradecircle/components/TagInput.js.map +1 -1
  26. package/dist/comradecircle/context/ComradeCircleContext.js.map +1 -1
  27. package/dist/comradecircle/hooks/useComradeCircleApi.js.map +1 -1
  28. package/dist/comradecircle/hooks/useCursorPager.js.map +1 -1
  29. package/dist/comradecircle/hooks/useFiles.js.map +1 -1
  30. package/dist/comradecircle/lib/graphql-cache.js.map +1 -1
  31. package/dist/comradecircle/lib/toast.js.map +1 -1
  32. package/dist/comradecircle/pages/FeedPage.js.map +1 -1
  33. package/dist/comradecircle/pages/circles/CircleDetailPage.js.map +1 -1
  34. package/dist/comradecircle/pages/circles/CirclesPage.js.map +1 -1
  35. package/dist/comradecircle/pages/events/EventDetailPage.js.map +1 -1
  36. package/dist/comradecircle/pages/events/EventsPage.js.map +1 -1
  37. package/dist/comradecircle/pages/members/MemberProfilePage.js.map +1 -1
  38. package/dist/comradecircle/pages/members/MembersPage.js.map +1 -1
  39. package/dist/comradecircle/pages/profile/MyProfilePage.js.map +1 -1
  40. package/dist/comradecircle/utils/cn.js.map +1 -1
  41. package/dist/comradecircle/utils/formatters.js.map +1 -1
  42. package/dist/comradecircle/utils/navigation.js.map +1 -1
  43. package/dist/comradecircle/utils/slug.js.map +1 -1
  44. package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
  45. package/dist/node_modules/sonner/dist/index.js.map +1 -1
  46. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1 -1
  47. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
  48. package/dist/services/createSandboxAssistantTransport.d.ts.map +1 -1
  49. package/dist/services/createSandboxAssistantTransport.js.map +1 -1
  50. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"Pagination.js","names":[],"sources":["../../../src/comradecircle/components/Pagination.tsx"],"sourcesContent":["/**\n * Pagination — cursor-aware Prev/Next control for Com* connection lists.\n * @module comradecircle/components\n *\n * The svc list connections return `pageInfo { cursor, hasMore }` and paginate by\n * cursor (offset is a no-op). Pages track a 0-indexed `page` for display and a\n * cursor stack for navigation; this control exposes Prev/Next, disabling Prev on\n * page 0 and Next when the server reports no more rows.\n */\n\nimport { ChevronLeft, ChevronRight } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface PaginationProps {\n page: number;\n pageSize: number;\n /** Number of rows on the current page. */\n itemCount: number;\n /** Server flag: are there more rows after this page? */\n hasMore: boolean;\n onPageChange: (page: number) => void;\n className?: string;\n}\n\nexport function Pagination({\n page,\n pageSize,\n itemCount,\n hasMore,\n onPageChange,\n className,\n}: PaginationProps) {\n // Nothing to paginate: single page that also has no more rows.\n if (page === 0 && !hasMore) return null;\n\n const from = itemCount === 0 ? 0 : page * pageSize + 1;\n const to = page * pageSize + itemCount;\n\n return (\n <div\n className={cn('mt-4 flex flex-col items-center justify-between gap-3 sm:flex-row', className)}\n >\n <p className=\"text-sm text-text-secondary tabular-nums\" aria-live=\"polite\">\n Showing {from}–{to}\n </p>\n <div className=\"flex items-center gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => onPageChange(page - 1)}\n disabled={page === 0}\n aria-label=\"Previous page\"\n >\n <ChevronLeft className=\"mr-1 h-4 w-4\" /> Previous\n </Button>\n <span className=\"min-w-[4.5rem] text-center text-sm text-text-secondary tabular-nums\">\n Page {page + 1}\n </span>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => onPageChange(page + 1)}\n disabled={!hasMore}\n aria-label=\"Next page\"\n >\n Next <ChevronRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;AAyBA,SAAgB,EAAW,EACzB,SACA,aACA,cACA,YACA,iBACA,gBACkB;AAElB,KAAI,MAAS,KAAK,CAAC,EAAS,QAAO;CAEnC,IAAM,IAAO,MAAc,IAAI,IAAI,IAAO,IAAW,GAC/C,IAAK,IAAO,IAAW;AAE7B,QACE,kBAAC,OAAD;EACE,WAAW,EAAG,qEAAqE,EAAU;YAD/F,CAGE,kBAAC,KAAD;GAAG,WAAU;GAA2C,aAAU;aAAlE;IAA2E;IAChE;IAAK;IAAE;IACd;MACJ,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KACE,SAAQ;KACR,MAAK;KACL,eAAe,EAAa,IAAO,EAAE;KACrC,UAAU,MAAS;KACnB,cAAW;eALb,CAOE,kBAAC,GAAD,EAAa,WAAU,gBAAiB,CAAA,EAAA,YACjC;;IACT,kBAAC,QAAD;KAAM,WAAU;eAAhB,CAAsF,SAC9E,IAAO,EACR;;IACP,kBAAC,GAAD;KACE,SAAQ;KACR,MAAK;KACL,eAAe,EAAa,IAAO,EAAE;KACrC,UAAU,CAAC;KACX,cAAW;eALb,CAMC,SACM,kBAAC,GAAD,EAAc,WAAU,gBAAiB,CAAA,CACvC;;IACL;KACF"}
1
+ {"version":3,"file":"Pagination.js","names":[],"sources":["../../../src/comradecircle/components/Pagination.tsx"],"sourcesContent":["/**\n * Pagination — cursor-aware Prev/Next control for Com* connection lists.\n * @module comradecircle/components\n *\n * The svc list connections return `pageInfo { cursor, hasMore }` and paginate by\n * cursor (offset is a no-op). Pages track a 0-indexed `page` for display and a\n * cursor stack for navigation; this control exposes Prev/Next, disabling Prev on\n * page 0 and Next when the server reports no more rows.\n */\n\nimport { ChevronLeft, ChevronRight } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface PaginationProps {\n page: number;\n pageSize: number;\n /** Number of rows on the current page. */\n itemCount: number;\n /** Server flag: are there more rows after this page? */\n hasMore: boolean;\n onPageChange: (page: number) => void;\n className?: string;\n}\n\nexport function Pagination({\n page,\n pageSize,\n itemCount,\n hasMore,\n onPageChange,\n className,\n}: PaginationProps) {\n // Nothing to paginate: single page that also has no more rows.\n if (page === 0 && !hasMore) return null;\n\n const from = itemCount === 0 ? 0 : page * pageSize + 1;\n const to = page * pageSize + itemCount;\n\n return (\n <div\n className={cn('mt-4 flex flex-col items-center justify-between gap-3 sm:flex-row', className)}\n >\n <p className=\"text-sm text-text-secondary tabular-nums\" aria-live=\"polite\">\n Showing {from}–{to}\n </p>\n <div className=\"flex items-center gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => onPageChange(page - 1)}\n disabled={page === 0}\n aria-label=\"Previous page\"\n >\n <ChevronLeft className=\"mr-1 h-4 w-4\" /> Previous\n </Button>\n <span className=\"min-w-[4.5rem] text-center text-sm text-text-secondary tabular-nums\">\n Page {page + 1}\n </span>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => onPageChange(page + 1)}\n disabled={!hasMore}\n aria-label=\"Next page\"\n >\n Next <ChevronRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;AAyBA,SAAgB,EAAW,EACzB,SACA,aACA,cACA,YACA,iBACA,gBACkB;CAElB,IAAI,MAAS,KAAK,CAAC,GAAS,OAAO;CAEnC,IAAM,IAAO,MAAc,IAAI,IAAI,IAAO,IAAW,GAC/C,IAAK,IAAO,IAAW;CAE7B,OACE,kBAAC,OAAD;EACE,WAAW,EAAG,qEAAqE,CAAS;YAD9F,CAGE,kBAAC,KAAD;GAAG,WAAU;GAA2C,aAAU;aAAlE;IAA2E;IAChE;IAAK;IAAE;GACf;MACH,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KACE,SAAQ;KACR,MAAK;KACL,eAAe,EAAa,IAAO,CAAC;KACpC,UAAU,MAAS;KACnB,cAAW;eALb,CAOE,kBAAC,GAAD,EAAa,WAAU,eAAgB,CAAA,GAAC,WAClC;;IACR,kBAAC,QAAD;KAAM,WAAU;eAAhB,CAAsF,SAC9E,IAAO,CACT;;IACN,kBAAC,GAAD;KACE,SAAQ;KACR,MAAK;KACL,eAAe,EAAa,IAAO,CAAC;KACpC,UAAU,CAAC;KACX,cAAW;eALb,CAMC,SACM,kBAAC,GAAD,EAAc,WAAU,eAAgB,CAAA,CACvC;;GACL;IACF;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"PollCard.js","names":[],"sources":["../../../src/comradecircle/components/PollCard.tsx"],"sourcesContent":["/**\n * PollCard — a circle poll with vote + live results.\n * @module comradecircle/components\n *\n * Writes `comVotePoll(pollId, optionId)` and `comClosePoll`. Options render as\n * clickable result bars; the leading option is emphasised with an accent\n * gradient fill and the caller's own voted option(s) are checkmarked (tracked\n * client-side — the poll query returns aggregate counts, not per-member votes).\n * A closed poll (or one past its close time) is read-only.\n */\n\nimport { useMemo, useState } from 'react';\nimport { CheckCircle2, Lock, Crown } from 'lucide-react';\nimport { GlassCard, Button } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { MiniBar } from './MiniBar';\nimport { StatusBadge } from './StatusBadge';\nimport { useComVotePoll, useComClosePoll } from '../hooks';\nimport { formatNumber, formatRelativeTime } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComPoll } from '../types';\n\ninterface PollCardProps {\n poll: ComPoll;\n /** Whether the caller may close the poll (creator / circle admin). */\n canManage?: boolean;\n}\n\nexport function PollCard({ poll, canManage = false }: PollCardProps) {\n const vote = useComVotePoll();\n const close = useComClosePoll();\n // The poll query returns aggregate voteCounts, not per-member votes, so track\n // the options this caller has voted for in this session for the \"You voted\" cue.\n const [votedIds, setVotedIds] = useState<Set<string>>(new Set());\n\n const totalVotes = poll.options.reduce((sum, o) => sum + o.voteCount, 0);\n const isClosed = poll.status === 'CLOSED';\n const leadingCount = useMemo(\n () => poll.options.reduce((max, o) => Math.max(max, o.voteCount), 0),\n [poll.options]\n );\n const hasVoted = votedIds.size > 0;\n\n const doVote = async (optionId: string) => {\n if (isClosed) return;\n try {\n await vote.mutateAsync({ pollId: poll.id, optionId, circleId: poll.circleId });\n setVotedIds((prev) => new Set(prev).add(optionId));\n notify.success('Vote recorded');\n } catch (err) {\n notify.error('Could not vote', describeError(err));\n }\n };\n\n const doClose = async () => {\n try {\n await close.mutateAsync(poll.id);\n notify.success('Poll closed');\n } catch (err) {\n notify.error('Could not close poll', describeError(err));\n }\n };\n\n return (\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <h3 className=\"text-base font-semibold text-text-primary\">{poll.question}</h3>\n <StatusBadge status={poll.status} />\n </div>\n\n <div className=\"mt-4 flex flex-col gap-2.5\">\n {[...poll.options]\n .sort((a, b) => a.position - b.position)\n .map((opt) => {\n const pct = totalVotes > 0 ? (opt.voteCount / totalVotes) * 100 : 0;\n const isLeading = totalVotes > 0 && opt.voteCount === leadingCount;\n const isMine = votedIds.has(opt.id);\n return (\n <button\n key={opt.id}\n type=\"button\"\n onClick={() => doVote(opt.id)}\n disabled={isClosed || vote.isPending}\n aria-pressed={isMine}\n className={cn(\n 'group w-full rounded-xl border px-3 py-2.5 text-left transition-all duration-200',\n isMine ? 'border-accent bg-[var(--color-accent-soft)]/40' : 'border-border-seam',\n !isClosed &&\n !isMine &&\n 'hover:-translate-y-px hover:border-accent hover:bg-bg-sunken/60',\n isClosed && 'cursor-default'\n )}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex min-w-0 items-center gap-1.5\">\n {isMine && (\n <CheckCircle2 className=\"h-4 w-4 shrink-0 text-accent\" aria-hidden=\"true\" />\n )}\n <span\n className={cn(\n 'truncate text-sm',\n isLeading\n ? 'font-semibold text-text-primary'\n : 'font-medium text-text-secondary'\n )}\n >\n {opt.label}\n </span>\n {isLeading && totalVotes > 1 && (\n <Crown\n className=\"h-3.5 w-3.5 shrink-0 text-status-warning-text\"\n aria-label=\"Leading\"\n />\n )}\n </span>\n <span className=\"shrink-0 text-xs tabular-nums text-text-secondary\">\n {formatNumber(opt.voteCount)} · {Math.round(pct)}%\n </span>\n </div>\n <MiniBar\n value={pct}\n className=\"mt-2\"\n barClassName={\n isLeading\n ? 'bg-gradient-to-r from-accent to-[var(--color-accent-soft)]'\n : 'bg-accent/40'\n }\n />\n </button>\n );\n })}\n </div>\n\n <div className=\"mt-4 flex items-center justify-between gap-3 border-t border-border-seam pt-3 text-xs text-text-tertiary\">\n <span className=\"inline-flex flex-wrap items-center gap-1.5\">\n {poll.allowMultiple ? <CheckCircle2 className=\"h-3.5 w-3.5\" /> : null}\n {formatNumber(totalVotes)} vote{totalVotes === 1 ? '' : 's'}\n {poll.allowMultiple ? ' · multiple choice' : ''}\n {poll.isAnonymous ? ' · anonymous' : ''}\n {poll.closesAt ? ` · closes ${formatRelativeTime(poll.closesAt)}` : ''}\n {hasVoted && <span className=\"font-medium text-accent\">· You voted</span>}\n </span>\n {canManage && !isClosed && (\n <Button size=\"sm\" variant=\"outline\" onClick={doClose} disabled={close.isPending}>\n <Lock className=\"mr-1.5 h-3.5 w-3.5\" /> Close\n </Button>\n )}\n </div>\n </GlassCard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"PollCard.js","names":[],"sources":["../../../src/comradecircle/components/PollCard.tsx"],"sourcesContent":["/**\n * PollCard — a circle poll with vote + live results.\n * @module comradecircle/components\n *\n * Writes `comVotePoll(pollId, optionId)` and `comClosePoll`. Options render as\n * clickable result bars; the leading option is emphasised with an accent\n * gradient fill and the caller's own voted option(s) are checkmarked (tracked\n * client-side — the poll query returns aggregate counts, not per-member votes).\n * A closed poll (or one past its close time) is read-only.\n */\n\nimport { useMemo, useState } from 'react';\nimport { CheckCircle2, Lock, Crown } from 'lucide-react';\nimport { GlassCard, Button } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { MiniBar } from './MiniBar';\nimport { StatusBadge } from './StatusBadge';\nimport { useComVotePoll, useComClosePoll } from '../hooks';\nimport { formatNumber, formatRelativeTime } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComPoll } from '../types';\n\ninterface PollCardProps {\n poll: ComPoll;\n /** Whether the caller may close the poll (creator / circle admin). */\n canManage?: boolean;\n}\n\nexport function PollCard({ poll, canManage = false }: PollCardProps) {\n const vote = useComVotePoll();\n const close = useComClosePoll();\n // The poll query returns aggregate voteCounts, not per-member votes, so track\n // the options this caller has voted for in this session for the \"You voted\" cue.\n const [votedIds, setVotedIds] = useState<Set<string>>(new Set());\n\n const totalVotes = poll.options.reduce((sum, o) => sum + o.voteCount, 0);\n const isClosed = poll.status === 'CLOSED';\n const leadingCount = useMemo(\n () => poll.options.reduce((max, o) => Math.max(max, o.voteCount), 0),\n [poll.options]\n );\n const hasVoted = votedIds.size > 0;\n\n const doVote = async (optionId: string) => {\n if (isClosed) return;\n try {\n await vote.mutateAsync({ pollId: poll.id, optionId, circleId: poll.circleId });\n setVotedIds((prev) => new Set(prev).add(optionId));\n notify.success('Vote recorded');\n } catch (err) {\n notify.error('Could not vote', describeError(err));\n }\n };\n\n const doClose = async () => {\n try {\n await close.mutateAsync(poll.id);\n notify.success('Poll closed');\n } catch (err) {\n notify.error('Could not close poll', describeError(err));\n }\n };\n\n return (\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <h3 className=\"text-base font-semibold text-text-primary\">{poll.question}</h3>\n <StatusBadge status={poll.status} />\n </div>\n\n <div className=\"mt-4 flex flex-col gap-2.5\">\n {[...poll.options]\n .sort((a, b) => a.position - b.position)\n .map((opt) => {\n const pct = totalVotes > 0 ? (opt.voteCount / totalVotes) * 100 : 0;\n const isLeading = totalVotes > 0 && opt.voteCount === leadingCount;\n const isMine = votedIds.has(opt.id);\n return (\n <button\n key={opt.id}\n type=\"button\"\n onClick={() => doVote(opt.id)}\n disabled={isClosed || vote.isPending}\n aria-pressed={isMine}\n className={cn(\n 'group w-full rounded-xl border px-3 py-2.5 text-left transition-all duration-200',\n isMine ? 'border-accent bg-[var(--color-accent-soft)]/40' : 'border-border-seam',\n !isClosed &&\n !isMine &&\n 'hover:-translate-y-px hover:border-accent hover:bg-bg-sunken/60',\n isClosed && 'cursor-default'\n )}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex min-w-0 items-center gap-1.5\">\n {isMine && (\n <CheckCircle2 className=\"h-4 w-4 shrink-0 text-accent\" aria-hidden=\"true\" />\n )}\n <span\n className={cn(\n 'truncate text-sm',\n isLeading\n ? 'font-semibold text-text-primary'\n : 'font-medium text-text-secondary'\n )}\n >\n {opt.label}\n </span>\n {isLeading && totalVotes > 1 && (\n <Crown\n className=\"h-3.5 w-3.5 shrink-0 text-status-warning-text\"\n aria-label=\"Leading\"\n />\n )}\n </span>\n <span className=\"shrink-0 text-xs tabular-nums text-text-secondary\">\n {formatNumber(opt.voteCount)} · {Math.round(pct)}%\n </span>\n </div>\n <MiniBar\n value={pct}\n className=\"mt-2\"\n barClassName={\n isLeading\n ? 'bg-gradient-to-r from-accent to-[var(--color-accent-soft)]'\n : 'bg-accent/40'\n }\n />\n </button>\n );\n })}\n </div>\n\n <div className=\"mt-4 flex items-center justify-between gap-3 border-t border-border-seam pt-3 text-xs text-text-tertiary\">\n <span className=\"inline-flex flex-wrap items-center gap-1.5\">\n {poll.allowMultiple ? <CheckCircle2 className=\"h-3.5 w-3.5\" /> : null}\n {formatNumber(totalVotes)} vote{totalVotes === 1 ? '' : 's'}\n {poll.allowMultiple ? ' · multiple choice' : ''}\n {poll.isAnonymous ? ' · anonymous' : ''}\n {poll.closesAt ? ` · closes ${formatRelativeTime(poll.closesAt)}` : ''}\n {hasVoted && <span className=\"font-medium text-accent\">· You voted</span>}\n </span>\n {canManage && !isClosed && (\n <Button size=\"sm\" variant=\"outline\" onClick={doClose} disabled={close.isPending}>\n <Lock className=\"mr-1.5 h-3.5 w-3.5\" /> Close\n </Button>\n )}\n </div>\n </GlassCard>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"PostCard.js","names":[],"sources":["../../../src/comradecircle/components/PostCard.tsx"],"sourcesContent":["/**\n * PostCard — a single post with reactions and a collapsible comment thread.\n * @module comradecircle/components\n *\n * Reads its data from a `ComPost`; writes via `comUpdatePost` (edit / pin) and\n * `comRemovePost` (delete). Reactions + comments are delegated to\n * {@link ReactionBar} and {@link CommentThread}. Author-only actions (edit, pin,\n * delete) are gated on `authorId === currentUser.id`.\n */\n\nimport { useState } from 'react';\nimport { MessageSquare, MoreHorizontal, Pin, Pencil, Trash2 } from 'lucide-react';\nimport {\n GlassCard,\n Button,\n Textarea,\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n} from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { MemberIdentity } from './MemberIdentity';\nimport { ReactionBar } from './ReactionBar';\nimport { CommentThread } from './CommentThread';\nimport { AttachmentGrid } from './ImageAttachments';\nimport { useComUpdatePost, useComRemovePost } from '../hooks';\nimport { useComradeCircle } from '../context';\nimport { formatRelativeTime } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComPost } from '../types';\n\ninterface PostCardProps {\n post: ComPost;\n /** Start with the comment thread open. */\n defaultExpanded?: boolean;\n}\n\nexport function PostCard({ post, defaultExpanded = false }: PostCardProps) {\n const { currentUser } = useComradeCircle();\n const updatePost = useComUpdatePost();\n const removePost = useComRemovePost();\n const [expanded, setExpanded] = useState(defaultExpanded);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(post.body);\n\n const isAuthor = Boolean(currentUser?.id && currentUser.id === post.authorId);\n\n const saveEdit = async () => {\n const text = draft.trim();\n if (!text || text === post.body) {\n setEditing(false);\n return;\n }\n try {\n await updatePost.mutateAsync({ id: post.id, input: { body: text } });\n setEditing(false);\n notify.success('Post updated');\n } catch (err) {\n notify.error('Could not update post', describeError(err));\n }\n };\n\n const togglePin = async () => {\n try {\n await updatePost.mutateAsync({ id: post.id, input: { isPinned: !post.isPinned } });\n notify.success(post.isPinned ? 'Post unpinned' : 'Post pinned');\n } catch (err) {\n notify.error('Could not update post', describeError(err));\n }\n };\n\n const remove = async () => {\n try {\n await removePost.mutateAsync({ id: post.id, channelId: post.channelId });\n notify.success('Post removed');\n } catch (err) {\n notify.error('Could not remove post', describeError(err));\n }\n };\n\n return (\n <GlassCard\n treatment=\"plain\"\n className={cn(\n 'p-4 transition-all duration-200 motion-safe:hover:-translate-y-0.5 hover:border-[var(--color-accent-soft)]',\n post.isPinned && 'ring-1 ring-accent/40'\n )}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <MemberIdentity\n memberId={post.authorId}\n variant=\"row\"\n size=\"md\"\n suffix={formatRelativeTime(post.createdAt)}\n showPresence\n />\n <div className=\"flex items-center gap-1\">\n {post.isPinned && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n <Pin className=\"h-3 w-3\" /> Pinned\n </span>\n )}\n {isAuthor && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n aria-label=\"Post actions\"\n className=\"rounded-lg p-1.5 text-text-tertiary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => setEditing(true)}>\n <Pencil className=\"mr-2 h-4 w-4\" /> Edit\n </DropdownMenuItem>\n <DropdownMenuItem onClick={togglePin}>\n <Pin className=\"mr-2 h-4 w-4\" /> {post.isPinned ? 'Unpin' : 'Pin'}\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onClick={remove}\n className=\"text-status-error-text focus:text-status-error-text\"\n >\n <Trash2 className=\"mr-2 h-4 w-4\" /> Delete\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n </div>\n\n {editing ? (\n <div className=\"mt-3\">\n <Textarea\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n rows={3}\n className=\"resize-none\"\n />\n <div className=\"mt-2 flex justify-end gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n setDraft(post.body);\n setEditing(false);\n }}\n >\n Cancel\n </Button>\n <Button size=\"sm\" onClick={saveEdit} disabled={updatePost.isPending}>\n Save\n </Button>\n </div>\n </div>\n ) : (\n <p className=\"mt-3 whitespace-pre-wrap break-words text-sm text-text-primary\">\n {post.body}\n </p>\n )}\n\n {post.tags.length > 0 && (\n <div className=\"mt-2 flex flex-wrap gap-1.5\">\n {post.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\"\n >\n #{tag}\n </span>\n ))}\n </div>\n )}\n\n {post.attachmentFileIds.length > 0 && <AttachmentGrid fileIds={post.attachmentFileIds} />}\n\n <div className=\"mt-3 flex items-center justify-between gap-3 border-t border-border-seam pt-3\">\n <ReactionBar post={post} />\n <button\n type=\"button\"\n onClick={() => setExpanded((s) => !s)}\n aria-expanded={expanded}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium transition-colors',\n expanded\n ? 'bg-[var(--color-accent-soft)] text-text-primary'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n )}\n >\n <MessageSquare className=\"h-3.5 w-3.5\" />\n <span className=\"tabular-nums\">{post.commentCount}</span>\n <span className=\"hidden sm:inline\">\n {post.commentCount === 1 ? 'comment' : 'comments'}\n </span>\n </button>\n </div>\n\n {expanded && <CommentThread postId={post.id} />}\n </GlassCard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"PostCard.js","names":[],"sources":["../../../src/comradecircle/components/PostCard.tsx"],"sourcesContent":["/**\n * PostCard — a single post with reactions and a collapsible comment thread.\n * @module comradecircle/components\n *\n * Reads its data from a `ComPost`; writes via `comUpdatePost` (edit / pin) and\n * `comRemovePost` (delete). Reactions + comments are delegated to\n * {@link ReactionBar} and {@link CommentThread}. Author-only actions (edit, pin,\n * delete) are gated on `authorId === currentUser.id`.\n */\n\nimport { useState } from 'react';\nimport { MessageSquare, MoreHorizontal, Pin, Pencil, Trash2 } from 'lucide-react';\nimport {\n GlassCard,\n Button,\n Textarea,\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n} from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { MemberIdentity } from './MemberIdentity';\nimport { ReactionBar } from './ReactionBar';\nimport { CommentThread } from './CommentThread';\nimport { AttachmentGrid } from './ImageAttachments';\nimport { useComUpdatePost, useComRemovePost } from '../hooks';\nimport { useComradeCircle } from '../context';\nimport { formatRelativeTime } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComPost } from '../types';\n\ninterface PostCardProps {\n post: ComPost;\n /** Start with the comment thread open. */\n defaultExpanded?: boolean;\n}\n\nexport function PostCard({ post, defaultExpanded = false }: PostCardProps) {\n const { currentUser } = useComradeCircle();\n const updatePost = useComUpdatePost();\n const removePost = useComRemovePost();\n const [expanded, setExpanded] = useState(defaultExpanded);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(post.body);\n\n const isAuthor = Boolean(currentUser?.id && currentUser.id === post.authorId);\n\n const saveEdit = async () => {\n const text = draft.trim();\n if (!text || text === post.body) {\n setEditing(false);\n return;\n }\n try {\n await updatePost.mutateAsync({ id: post.id, input: { body: text } });\n setEditing(false);\n notify.success('Post updated');\n } catch (err) {\n notify.error('Could not update post', describeError(err));\n }\n };\n\n const togglePin = async () => {\n try {\n await updatePost.mutateAsync({ id: post.id, input: { isPinned: !post.isPinned } });\n notify.success(post.isPinned ? 'Post unpinned' : 'Post pinned');\n } catch (err) {\n notify.error('Could not update post', describeError(err));\n }\n };\n\n const remove = async () => {\n try {\n await removePost.mutateAsync({ id: post.id, channelId: post.channelId });\n notify.success('Post removed');\n } catch (err) {\n notify.error('Could not remove post', describeError(err));\n }\n };\n\n return (\n <GlassCard\n treatment=\"plain\"\n className={cn(\n 'p-4 transition-all duration-200 motion-safe:hover:-translate-y-0.5 hover:border-[var(--color-accent-soft)]',\n post.isPinned && 'ring-1 ring-accent/40'\n )}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <MemberIdentity\n memberId={post.authorId}\n variant=\"row\"\n size=\"md\"\n suffix={formatRelativeTime(post.createdAt)}\n showPresence\n />\n <div className=\"flex items-center gap-1\">\n {post.isPinned && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n <Pin className=\"h-3 w-3\" /> Pinned\n </span>\n )}\n {isAuthor && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n aria-label=\"Post actions\"\n className=\"rounded-lg p-1.5 text-text-tertiary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => setEditing(true)}>\n <Pencil className=\"mr-2 h-4 w-4\" /> Edit\n </DropdownMenuItem>\n <DropdownMenuItem onClick={togglePin}>\n <Pin className=\"mr-2 h-4 w-4\" /> {post.isPinned ? 'Unpin' : 'Pin'}\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onClick={remove}\n className=\"text-status-error-text focus:text-status-error-text\"\n >\n <Trash2 className=\"mr-2 h-4 w-4\" /> Delete\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n </div>\n\n {editing ? (\n <div className=\"mt-3\">\n <Textarea\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n rows={3}\n className=\"resize-none\"\n />\n <div className=\"mt-2 flex justify-end gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n setDraft(post.body);\n setEditing(false);\n }}\n >\n Cancel\n </Button>\n <Button size=\"sm\" onClick={saveEdit} disabled={updatePost.isPending}>\n Save\n </Button>\n </div>\n </div>\n ) : (\n <p className=\"mt-3 whitespace-pre-wrap break-words text-sm text-text-primary\">\n {post.body}\n </p>\n )}\n\n {post.tags.length > 0 && (\n <div className=\"mt-2 flex flex-wrap gap-1.5\">\n {post.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\"\n >\n #{tag}\n </span>\n ))}\n </div>\n )}\n\n {post.attachmentFileIds.length > 0 && <AttachmentGrid fileIds={post.attachmentFileIds} />}\n\n <div className=\"mt-3 flex items-center justify-between gap-3 border-t border-border-seam pt-3\">\n <ReactionBar post={post} />\n <button\n type=\"button\"\n onClick={() => setExpanded((s) => !s)}\n aria-expanded={expanded}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium transition-colors',\n expanded\n ? 'bg-[var(--color-accent-soft)] text-text-primary'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n )}\n >\n <MessageSquare className=\"h-3.5 w-3.5\" />\n <span className=\"tabular-nums\">{post.commentCount}</span>\n <span className=\"hidden sm:inline\">\n {post.commentCount === 1 ? 'comment' : 'comments'}\n </span>\n </button>\n </div>\n\n {expanded && <CommentThread postId={post.id} />}\n </GlassCard>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"PostComposer.js","names":[],"sources":["../../../src/comradecircle/components/PostComposer.tsx"],"sourcesContent":["/**\n * PostComposer — compose and publish a post to a channel.\n * @module comradecircle/components\n *\n * Writes `comCreatePost({ channelId, body, tags })`. Text + tags only (file\n * attachments are a future enhancement — the svc accepts attachmentFileIds but\n * file upload/preview is out of scope for v1).\n */\n\nimport { useState } from 'react';\nimport { Send } from 'lucide-react';\nimport { Button, Textarea, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { TagInput } from './TagInput';\nimport { Avatar } from './Avatar';\nimport { AttachmentPicker } from './ImageAttachments';\nimport { useComCreatePost } from '../hooks';\nimport { useComradeCircle } from '../context';\nimport { notify, describeError } from '../lib/toast';\n\ninterface PostComposerProps {\n channelId: string;\n channelName?: string;\n onPosted?: () => void;\n}\n\nexport function PostComposer({ channelId, channelName, onPosted }: PostComposerProps) {\n const { currentUser } = useComradeCircle();\n const createPost = useComCreatePost();\n const [body, setBody] = useState('');\n const [tags, setTags] = useState<string[]>([]);\n const [showTags, setShowTags] = useState(false);\n const [attachmentFileIds, setAttachmentFileIds] = useState<string[]>([]);\n\n const submit = async () => {\n const text = body.trim();\n if (!text && attachmentFileIds.length === 0) return;\n try {\n await createPost.mutateAsync({\n channelId,\n body: text,\n tags: tags.length ? tags : null,\n attachmentFileIds: attachmentFileIds.length ? attachmentFileIds : null,\n });\n setBody('');\n setTags([]);\n setAttachmentFileIds([]);\n setShowTags(false);\n notify.success('Posted', channelName ? `Shared to #${channelName}` : undefined);\n onPosted?.();\n } catch (err) {\n notify.error('Could not publish post', describeError(err));\n }\n };\n\n return (\n <GlassCard treatment=\"plain\" className=\"p-4\">\n <div className=\"flex gap-3\">\n <Avatar name={currentUser?.name ?? currentUser?.email ?? 'You'} size=\"md\" />\n <div className=\"flex-1\">\n <Textarea\n value={body}\n onChange={(e) => setBody(e.target.value)}\n placeholder={channelName ? `Share something in #${channelName}…` : 'Share something…'}\n rows={2}\n className=\"resize-none border-0 bg-transparent px-0 shadow-none focus-visible:ring-0\"\n onKeyDown={(e) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n void submit();\n }\n }}\n />\n {showTags && (\n <div className=\"mt-2\">\n <TagInput value={tags} onChange={setTags} placeholder=\"Add topics…\" />\n </div>\n )}\n <div className=\"mt-2\">\n <AttachmentPicker\n fileIds={attachmentFileIds}\n onChange={setAttachmentFileIds}\n disabled={createPost.isPending}\n />\n </div>\n <div className=\"mt-3 flex items-center justify-between\">\n <button\n type=\"button\"\n onClick={() => setShowTags((s) => !s)}\n className=\"text-xs font-medium text-text-secondary transition-colors hover:text-text-primary\"\n >\n {showTags ? 'Hide topics' : '# Add topics'}\n </button>\n <Button\n size=\"sm\"\n onClick={submit}\n disabled={(!body.trim() && attachmentFileIds.length === 0) || createPost.isPending}\n >\n <Send className=\"mr-1.5 h-4 w-4\" /> Post\n </Button>\n </div>\n </div>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAyBA,SAAgB,EAAa,EAAE,cAAW,gBAAa,eAA+B;CACpF,IAAM,EAAE,mBAAgB,GAAkB,EACpC,IAAa,GAAkB,EAC/B,CAAC,GAAM,KAAW,EAAS,GAAG,EAC9B,CAAC,GAAM,KAAW,EAAmB,EAAE,CAAC,EACxC,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,CAAC,GAAmB,KAAwB,EAAmB,EAAE,CAAC,EAElE,IAAS,YAAY;EACzB,IAAM,IAAO,EAAK,MAAM;AACpB,SAAC,KAAQ,EAAkB,WAAW,GAC1C,KAAI;AAYF,GAXA,MAAM,EAAW,YAAY;IAC3B;IACA,MAAM;IACN,MAAM,EAAK,SAAS,IAAO;IAC3B,mBAAmB,EAAkB,SAAS,IAAoB;IACnE,CAAC,EACF,EAAQ,GAAG,EACX,EAAQ,EAAE,CAAC,EACX,EAAqB,EAAE,CAAC,EACxB,EAAY,GAAM,EAClB,EAAO,QAAQ,UAAU,IAAc,cAAc,MAAgB,KAAA,EAAU,EAC/E,KAAY;WACL,GAAK;AACZ,KAAO,MAAM,0BAA0B,EAAc,EAAI,CAAC;;;AAI9D,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YACrC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IAAQ,MAAM,GAAa,QAAQ,GAAa,SAAS;IAAO,MAAK;IAAO,CAAA,EAC5E,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,OAAO;MACP,WAAW,MAAM,EAAQ,EAAE,OAAO,MAAM;MACxC,aAAa,IAAc,uBAAuB,EAAY,KAAK;MACnE,MAAM;MACN,WAAU;MACV,YAAY,MAAM;AAChB,OAAI,EAAE,QAAQ,YAAY,EAAE,WAAW,EAAE,aACvC,EAAE,gBAAgB,EACb,GAAQ;;MAGjB,CAAA;KACD,KACC,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAU,OAAO;OAAM,UAAU;OAAS,aAAY;OAAgB,CAAA;MAClE,CAAA;KAER,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OACE,SAAS;OACT,UAAU;OACV,UAAU,EAAW;OACrB,CAAA;MACE,CAAA;KACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,GAAa,MAAM,CAAC,EAAE;OACrC,WAAU;iBAET,IAAW,gBAAgB;OACrB,CAAA,EACT,kBAAC,GAAD;OACE,MAAK;OACL,SAAS;OACT,UAAW,CAAC,EAAK,MAAM,IAAI,EAAkB,WAAW,KAAM,EAAW;iBAH3E,CAKE,kBAAC,GAAD,EAAM,WAAU,kBAAmB,CAAA,EAAA,QAC5B;SACL;;KACF;MACF;;EACI,CAAA"}
1
+ {"version":3,"file":"PostComposer.js","names":[],"sources":["../../../src/comradecircle/components/PostComposer.tsx"],"sourcesContent":["/**\n * PostComposer — compose and publish a post to a channel.\n * @module comradecircle/components\n *\n * Writes `comCreatePost({ channelId, body, tags })`. Text + tags only (file\n * attachments are a future enhancement — the svc accepts attachmentFileIds but\n * file upload/preview is out of scope for v1).\n */\n\nimport { useState } from 'react';\nimport { Send } from 'lucide-react';\nimport { Button, Textarea, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { TagInput } from './TagInput';\nimport { Avatar } from './Avatar';\nimport { AttachmentPicker } from './ImageAttachments';\nimport { useComCreatePost } from '../hooks';\nimport { useComradeCircle } from '../context';\nimport { notify, describeError } from '../lib/toast';\n\ninterface PostComposerProps {\n channelId: string;\n channelName?: string;\n onPosted?: () => void;\n}\n\nexport function PostComposer({ channelId, channelName, onPosted }: PostComposerProps) {\n const { currentUser } = useComradeCircle();\n const createPost = useComCreatePost();\n const [body, setBody] = useState('');\n const [tags, setTags] = useState<string[]>([]);\n const [showTags, setShowTags] = useState(false);\n const [attachmentFileIds, setAttachmentFileIds] = useState<string[]>([]);\n\n const submit = async () => {\n const text = body.trim();\n if (!text && attachmentFileIds.length === 0) return;\n try {\n await createPost.mutateAsync({\n channelId,\n body: text,\n tags: tags.length ? tags : null,\n attachmentFileIds: attachmentFileIds.length ? attachmentFileIds : null,\n });\n setBody('');\n setTags([]);\n setAttachmentFileIds([]);\n setShowTags(false);\n notify.success('Posted', channelName ? `Shared to #${channelName}` : undefined);\n onPosted?.();\n } catch (err) {\n notify.error('Could not publish post', describeError(err));\n }\n };\n\n return (\n <GlassCard treatment=\"plain\" className=\"p-4\">\n <div className=\"flex gap-3\">\n <Avatar name={currentUser?.name ?? currentUser?.email ?? 'You'} size=\"md\" />\n <div className=\"flex-1\">\n <Textarea\n value={body}\n onChange={(e) => setBody(e.target.value)}\n placeholder={channelName ? `Share something in #${channelName}…` : 'Share something…'}\n rows={2}\n className=\"resize-none border-0 bg-transparent px-0 shadow-none focus-visible:ring-0\"\n onKeyDown={(e) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n void submit();\n }\n }}\n />\n {showTags && (\n <div className=\"mt-2\">\n <TagInput value={tags} onChange={setTags} placeholder=\"Add topics…\" />\n </div>\n )}\n <div className=\"mt-2\">\n <AttachmentPicker\n fileIds={attachmentFileIds}\n onChange={setAttachmentFileIds}\n disabled={createPost.isPending}\n />\n </div>\n <div className=\"mt-3 flex items-center justify-between\">\n <button\n type=\"button\"\n onClick={() => setShowTags((s) => !s)}\n className=\"text-xs font-medium text-text-secondary transition-colors hover:text-text-primary\"\n >\n {showTags ? 'Hide topics' : '# Add topics'}\n </button>\n <Button\n size=\"sm\"\n onClick={submit}\n disabled={(!body.trim() && attachmentFileIds.length === 0) || createPost.isPending}\n >\n <Send className=\"mr-1.5 h-4 w-4\" /> Post\n </Button>\n </div>\n </div>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAyBA,SAAgB,EAAa,EAAE,cAAW,gBAAa,eAA+B;CACpF,IAAM,EAAE,mBAAgB,EAAiB,GACnC,IAAa,EAAiB,GAC9B,CAAC,GAAM,KAAW,EAAS,EAAE,GAC7B,CAAC,GAAM,KAAW,EAAmB,CAAC,CAAC,GACvC,CAAC,GAAU,KAAe,EAAS,EAAK,GACxC,CAAC,GAAmB,KAAwB,EAAmB,CAAC,CAAC,GAEjE,IAAS,YAAY;EACzB,IAAM,IAAO,EAAK,KAAK;EACnB,OAAC,KAAQ,EAAkB,WAAW,IAC1C,IAAI;GAYF,AAXA,MAAM,EAAW,YAAY;IAC3B;IACA,MAAM;IACN,MAAM,EAAK,SAAS,IAAO;IAC3B,mBAAmB,EAAkB,SAAS,IAAoB;GACpE,CAAC,GACD,EAAQ,EAAE,GACV,EAAQ,CAAC,CAAC,GACV,EAAqB,CAAC,CAAC,GACvB,EAAY,EAAK,GACjB,EAAO,QAAQ,UAAU,IAAc,cAAc,MAAgB,KAAA,CAAS,GAC9E,IAAW;EACb,SAAS,GAAK;GACZ,EAAO,MAAM,0BAA0B,EAAc,CAAG,CAAC;EAC3D;CACF;CAEA,OACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YACrC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IAAQ,MAAM,GAAa,QAAQ,GAAa,SAAS;IAAO,MAAK;GAAM,CAAA,GAC3E,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,OAAO;MACP,WAAW,MAAM,EAAQ,EAAE,OAAO,KAAK;MACvC,aAAa,IAAc,uBAAuB,EAAY,KAAK;MACnE,MAAM;MACN,WAAU;MACV,YAAY,MAAM;OAChB,AAAI,EAAE,QAAQ,YAAY,EAAE,WAAW,EAAE,aACvC,EAAE,eAAe,GACjB,EAAY;MAEhB;KACD,CAAA;KACA,KACC,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAU,OAAO;OAAM,UAAU;OAAS,aAAY;MAAe,CAAA;KAClE,CAAA;KAEP,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OACE,SAAS;OACT,UAAU;OACV,UAAU,EAAW;MACtB,CAAA;KACE,CAAA;KACL,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,GAAa,MAAM,CAAC,CAAC;OACpC,WAAU;iBAET,IAAW,gBAAgB;MACtB,CAAA,GACR,kBAAC,GAAD;OACE,MAAK;OACL,SAAS;OACT,UAAW,CAAC,EAAK,KAAK,KAAK,EAAkB,WAAW,KAAM,EAAW;iBAH3E,CAKE,kBAAC,GAAD,EAAM,WAAU,iBAAkB,CAAA,GAAC,OAC7B;QACL;;IACF;KACF;;CACI,CAAA;AAEf"}
@@ -1 +1 @@
1
- {"version":3,"file":"ReactionBar.js","names":[],"sources":["../../../src/comradecircle/components/ReactionBar.tsx"],"sourcesContent":["/**\n * ReactionBar — Slack-style multi-reaction control for a post.\n * @module comradecircle/components\n *\n * `comReactToPost(postId, type)` is an idempotent toggle for one typed reaction,\n * so a member may hold several distinct reactions on a post. We render a chip\n * per reaction type that has a count (highlighted when the caller reacted) plus\n * a popover to add any of the six types.\n */\n\nimport { useState } from 'react';\nimport { SmilePlus } from 'lucide-react';\nimport { Popover, PopoverContent, PopoverTrigger } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { useComReactToPost } from '../hooks';\nimport { useComradeCircle } from '../context';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComPost, ComReactionType } from '../types';\n\nconst REACTIONS: { type: ComReactionType; emoji: string; label: string }[] = [\n { type: 'LIKE', emoji: '👍', label: 'Like' },\n { type: 'LOVE', emoji: '❤️', label: 'Love' },\n { type: 'CELEBRATE', emoji: '🎉', label: 'Celebrate' },\n { type: 'INSIGHTFUL', emoji: '💡', label: 'Insightful' },\n { type: 'LAUGH', emoji: '😄', label: 'Funny' },\n { type: 'SUPPORT', emoji: '🤝', label: 'Support' },\n];\n\ninterface ReactionBarProps {\n post: ComPost;\n}\n\nexport function ReactionBar({ post }: ReactionBarProps) {\n const { currentUser } = useComradeCircle();\n const react = useComReactToPost();\n const [open, setOpen] = useState(false);\n\n const meId = currentUser?.id;\n const counts = new Map<ComReactionType, { count: number; mine: boolean }>();\n for (const r of post.reactions ?? []) {\n const cur = counts.get(r.type) ?? { count: 0, mine: false };\n cur.count += 1;\n if (meId && r.memberId === meId) cur.mine = true;\n counts.set(r.type, cur);\n }\n\n const toggle = async (type: ComReactionType) => {\n setOpen(false);\n try {\n await react.mutateAsync({ postId: post.id, type });\n } catch (err) {\n notify.error('Could not react', describeError(err));\n }\n };\n\n const active = REACTIONS.filter((r) => (counts.get(r.type)?.count ?? 0) > 0);\n\n return (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {active.map((r) => {\n const info = counts.get(r.type)!;\n return (\n <button\n key={r.type}\n type=\"button\"\n onClick={() => toggle(r.type)}\n disabled={react.isPending}\n aria-pressed={info.mine}\n aria-label={`${r.label} (${info.count})`}\n className={cn(\n 'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium transition-transform duration-150 active:scale-90',\n info.mine\n ? 'border-accent bg-[var(--color-accent-soft)] text-text-primary shadow-[0_0_0_2px_var(--color-accent-soft)]'\n : 'border-border-seam bg-bg-surface text-text-secondary hover:-translate-y-px hover:bg-bg-sunken'\n )}\n >\n <span aria-hidden=\"true\">{r.emoji}</span>\n <span className=\"tabular-nums\">{info.count}</span>\n </button>\n );\n })}\n\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n aria-label=\"Add reaction\"\n className=\"inline-flex items-center gap-1 rounded-full border border-border-seam bg-bg-surface px-2 py-0.5 text-xs text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <SmilePlus className=\"h-3.5 w-3.5\" />\n </button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto p-1.5\" align=\"start\">\n <div className=\"flex items-center gap-0.5\">\n {REACTIONS.map((r) => {\n const mine = counts.get(r.type)?.mine ?? false;\n return (\n <button\n key={r.type}\n type=\"button\"\n onClick={() => toggle(r.type)}\n disabled={react.isPending}\n title={r.label}\n aria-label={r.label}\n className={cn(\n 'flex h-9 w-9 items-center justify-center rounded-lg text-lg transition-transform hover:scale-110 hover:bg-bg-sunken',\n mine && 'bg-[var(--color-accent-soft)]'\n )}\n >\n <span aria-hidden=\"true\">{r.emoji}</span>\n </button>\n );\n })}\n </div>\n </PopoverContent>\n </Popover>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AAmBA,IAAM,IAAuE;CAC3E;EAAE,MAAM;EAAQ,OAAO;EAAM,OAAO;EAAQ;CAC5C;EAAE,MAAM;EAAQ,OAAO;EAAM,OAAO;EAAQ;CAC5C;EAAE,MAAM;EAAa,OAAO;EAAM,OAAO;EAAa;CACtD;EAAE,MAAM;EAAc,OAAO;EAAM,OAAO;EAAc;CACxD;EAAE,MAAM;EAAS,OAAO;EAAM,OAAO;EAAS;CAC9C;EAAE,MAAM;EAAW,OAAO;EAAM,OAAO;EAAW;CACnD;AAMD,SAAgB,EAAY,EAAE,WAA0B;CACtD,IAAM,EAAE,mBAAgB,GAAkB,EACpC,IAAQ,GAAmB,EAC3B,CAAC,GAAM,KAAW,EAAS,GAAM,EAEjC,IAAO,GAAa,IACpB,oBAAS,IAAI,KAAwD;AAC3E,MAAK,IAAM,KAAK,EAAK,aAAa,EAAE,EAAE;EACpC,IAAM,IAAM,EAAO,IAAI,EAAE,KAAK,IAAI;GAAE,OAAO;GAAG,MAAM;GAAO;AAG3D,EAFA,EAAI,SAAS,GACT,KAAQ,EAAE,aAAa,MAAM,EAAI,OAAO,KAC5C,EAAO,IAAI,EAAE,MAAM,EAAI;;CAGzB,IAAM,IAAS,OAAO,MAA0B;AAC9C,IAAQ,GAAM;AACd,MAAI;AACF,SAAM,EAAM,YAAY;IAAE,QAAQ,EAAK;IAAI;IAAM,CAAC;WAC3C,GAAK;AACZ,KAAO,MAAM,mBAAmB,EAAc,EAAI,CAAC;;;AAMvD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CAHa,EAAU,QAAQ,OAAO,EAAO,IAAI,EAAE,KAAK,EAAE,SAAS,KAAK,EAAE,CAIhE,KAAK,MAAM;GACjB,IAAM,IAAO,EAAO,IAAI,EAAE,KAAK;AAC/B,UACE,kBAAC,UAAD;IAEE,MAAK;IACL,eAAe,EAAO,EAAE,KAAK;IAC7B,UAAU,EAAM;IAChB,gBAAc,EAAK;IACnB,cAAY,GAAG,EAAE,MAAM,IAAI,EAAK,MAAM;IACtC,WAAW,EACT,wIACA,EAAK,OACD,8GACA,gGACL;cAZH,CAcE,kBAAC,QAAD;KAAM,eAAY;eAAQ,EAAE;KAAa,CAAA,EACzC,kBAAC,QAAD;KAAM,WAAU;eAAgB,EAAK;KAAa,CAAA,CAC3C;MAfF,EAAE,KAeA;IAEX,EAEF,kBAAC,GAAD;GAAe;GAAM,cAAc;aAAnC,CACE,kBAAC,GAAD;IAAgB,SAAA;cACd,kBAAC,UAAD;KACE,MAAK;KACL,cAAW;KACX,WAAU;eAEV,kBAAC,GAAD,EAAW,WAAU,eAAgB,CAAA;KAC9B,CAAA;IACM,CAAA,EACjB,kBAAC,GAAD;IAAgB,WAAU;IAAe,OAAM;cAC7C,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAU,KAAK,MAAM;MACpB,IAAM,IAAO,EAAO,IAAI,EAAE,KAAK,EAAE,QAAQ;AACzC,aACE,kBAAC,UAAD;OAEE,MAAK;OACL,eAAe,EAAO,EAAE,KAAK;OAC7B,UAAU,EAAM;OAChB,OAAO,EAAE;OACT,cAAY,EAAE;OACd,WAAW,EACT,uHACA,KAAQ,gCACT;iBAED,kBAAC,QAAD;QAAM,eAAY;kBAAQ,EAAE;QAAa,CAAA;OAClC,EAZF,EAAE,KAYA;OAEX;KACE,CAAA;IACS,CAAA,CACT;KACN"}
1
+ {"version":3,"file":"ReactionBar.js","names":[],"sources":["../../../src/comradecircle/components/ReactionBar.tsx"],"sourcesContent":["/**\n * ReactionBar — Slack-style multi-reaction control for a post.\n * @module comradecircle/components\n *\n * `comReactToPost(postId, type)` is an idempotent toggle for one typed reaction,\n * so a member may hold several distinct reactions on a post. We render a chip\n * per reaction type that has a count (highlighted when the caller reacted) plus\n * a popover to add any of the six types.\n */\n\nimport { useState } from 'react';\nimport { SmilePlus } from 'lucide-react';\nimport { Popover, PopoverContent, PopoverTrigger } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { useComReactToPost } from '../hooks';\nimport { useComradeCircle } from '../context';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComPost, ComReactionType } from '../types';\n\nconst REACTIONS: { type: ComReactionType; emoji: string; label: string }[] = [\n { type: 'LIKE', emoji: '👍', label: 'Like' },\n { type: 'LOVE', emoji: '❤️', label: 'Love' },\n { type: 'CELEBRATE', emoji: '🎉', label: 'Celebrate' },\n { type: 'INSIGHTFUL', emoji: '💡', label: 'Insightful' },\n { type: 'LAUGH', emoji: '😄', label: 'Funny' },\n { type: 'SUPPORT', emoji: '🤝', label: 'Support' },\n];\n\ninterface ReactionBarProps {\n post: ComPost;\n}\n\nexport function ReactionBar({ post }: ReactionBarProps) {\n const { currentUser } = useComradeCircle();\n const react = useComReactToPost();\n const [open, setOpen] = useState(false);\n\n const meId = currentUser?.id;\n const counts = new Map<ComReactionType, { count: number; mine: boolean }>();\n for (const r of post.reactions ?? []) {\n const cur = counts.get(r.type) ?? { count: 0, mine: false };\n cur.count += 1;\n if (meId && r.memberId === meId) cur.mine = true;\n counts.set(r.type, cur);\n }\n\n const toggle = async (type: ComReactionType) => {\n setOpen(false);\n try {\n await react.mutateAsync({ postId: post.id, type });\n } catch (err) {\n notify.error('Could not react', describeError(err));\n }\n };\n\n const active = REACTIONS.filter((r) => (counts.get(r.type)?.count ?? 0) > 0);\n\n return (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {active.map((r) => {\n const info = counts.get(r.type)!;\n return (\n <button\n key={r.type}\n type=\"button\"\n onClick={() => toggle(r.type)}\n disabled={react.isPending}\n aria-pressed={info.mine}\n aria-label={`${r.label} (${info.count})`}\n className={cn(\n 'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium transition-transform duration-150 active:scale-90',\n info.mine\n ? 'border-accent bg-[var(--color-accent-soft)] text-text-primary shadow-[0_0_0_2px_var(--color-accent-soft)]'\n : 'border-border-seam bg-bg-surface text-text-secondary hover:-translate-y-px hover:bg-bg-sunken'\n )}\n >\n <span aria-hidden=\"true\">{r.emoji}</span>\n <span className=\"tabular-nums\">{info.count}</span>\n </button>\n );\n })}\n\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n aria-label=\"Add reaction\"\n className=\"inline-flex items-center gap-1 rounded-full border border-border-seam bg-bg-surface px-2 py-0.5 text-xs text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <SmilePlus className=\"h-3.5 w-3.5\" />\n </button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto p-1.5\" align=\"start\">\n <div className=\"flex items-center gap-0.5\">\n {REACTIONS.map((r) => {\n const mine = counts.get(r.type)?.mine ?? false;\n return (\n <button\n key={r.type}\n type=\"button\"\n onClick={() => toggle(r.type)}\n disabled={react.isPending}\n title={r.label}\n aria-label={r.label}\n className={cn(\n 'flex h-9 w-9 items-center justify-center rounded-lg text-lg transition-transform hover:scale-110 hover:bg-bg-sunken',\n mine && 'bg-[var(--color-accent-soft)]'\n )}\n >\n <span aria-hidden=\"true\">{r.emoji}</span>\n </button>\n );\n })}\n </div>\n </PopoverContent>\n </Popover>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AAmBA,IAAM,IAAuE;CAC3E;EAAE,MAAM;EAAQ,OAAO;EAAM,OAAO;CAAO;CAC3C;EAAE,MAAM;EAAQ,OAAO;EAAM,OAAO;CAAO;CAC3C;EAAE,MAAM;EAAa,OAAO;EAAM,OAAO;CAAY;CACrD;EAAE,MAAM;EAAc,OAAO;EAAM,OAAO;CAAa;CACvD;EAAE,MAAM;EAAS,OAAO;EAAM,OAAO;CAAQ;CAC7C;EAAE,MAAM;EAAW,OAAO;EAAM,OAAO;CAAU;AACnD;AAMA,SAAgB,EAAY,EAAE,WAA0B;CACtD,IAAM,EAAE,mBAAgB,EAAiB,GACnC,IAAQ,EAAkB,GAC1B,CAAC,GAAM,KAAW,EAAS,EAAK,GAEhC,IAAO,GAAa,IACpB,oBAAS,IAAI,IAAuD;CAC1E,KAAK,IAAM,KAAK,EAAK,aAAa,CAAC,GAAG;EACpC,IAAM,IAAM,EAAO,IAAI,EAAE,IAAI,KAAK;GAAE,OAAO;GAAG,MAAM;EAAM;EAG1D,AAFA,EAAI,SAAS,GACT,KAAQ,EAAE,aAAa,MAAM,EAAI,OAAO,KAC5C,EAAO,IAAI,EAAE,MAAM,CAAG;CACxB;CAEA,IAAM,IAAS,OAAO,MAA0B;EAC9C,EAAQ,EAAK;EACb,IAAI;GACF,MAAM,EAAM,YAAY;IAAE,QAAQ,EAAK;IAAI;GAAK,CAAC;EACnD,SAAS,GAAK;GACZ,EAAO,MAAM,mBAAmB,EAAc,CAAG,CAAC;EACpD;CACF;CAIA,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CAHa,EAAU,QAAQ,OAAO,EAAO,IAAI,EAAE,IAAI,GAAG,SAAS,KAAK,CAIrE,EAAO,KAAK,MAAM;GACjB,IAAM,IAAO,EAAO,IAAI,EAAE,IAAI;GAC9B,OACE,kBAAC,UAAD;IAEE,MAAK;IACL,eAAe,EAAO,EAAE,IAAI;IAC5B,UAAU,EAAM;IAChB,gBAAc,EAAK;IACnB,cAAY,GAAG,EAAE,MAAM,IAAI,EAAK,MAAM;IACtC,WAAW,EACT,wIACA,EAAK,OACD,8GACA,+FACN;cAZF,CAcE,kBAAC,QAAD;KAAM,eAAY;eAAQ,EAAE;IAAY,CAAA,GACxC,kBAAC,QAAD;KAAM,WAAU;eAAgB,EAAK;IAAY,CAAA,CAC3C;MAfD,EAAE,IAeD;EAEZ,CAAC,GAED,kBAAC,GAAD;GAAe;GAAM,cAAc;aAAnC,CACE,kBAAC,GAAD;IAAgB,SAAA;cACd,kBAAC,UAAD;KACE,MAAK;KACL,cAAW;KACX,WAAU;eAEV,kBAAC,GAAD,EAAW,WAAU,cAAe,CAAA;IAC9B,CAAA;GACM,CAAA,GAChB,kBAAC,GAAD;IAAgB,WAAU;IAAe,OAAM;cAC7C,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAU,KAAK,MAAM;MACpB,IAAM,IAAO,EAAO,IAAI,EAAE,IAAI,GAAG,QAAQ;MACzC,OACE,kBAAC,UAAD;OAEE,MAAK;OACL,eAAe,EAAO,EAAE,IAAI;OAC5B,UAAU,EAAM;OAChB,OAAO,EAAE;OACT,cAAY,EAAE;OACd,WAAW,EACT,uHACA,KAAQ,+BACV;iBAEA,kBAAC,QAAD;QAAM,eAAY;kBAAQ,EAAE;OAAY,CAAA;MAClC,GAZD,EAAE,IAYD;KAEZ,CAAC;IACE,CAAA;GACS,CAAA,CACT;IACN;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"StateViews.js","names":[],"sources":["../../../src/comradecircle/components/StateViews.tsx"],"sourcesContent":["/**\n * Shared loading / error / section state views\n * @module comradecircle/components\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { AlertCircle } from 'lucide-react';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface LoadingViewProps {\n label?: string;\n className?: string;\n}\n\nexport const LoadingView: FC<LoadingViewProps> = ({ label = 'Loading…', className }) => (\n <div className={cn('flex min-h-[40vh] flex-col items-center justify-center gap-3', className)}>\n <Spinner size=\"lg\" />\n <p className=\"text-sm text-text-secondary\">{label}</p>\n </div>\n);\n\ninterface ErrorViewProps {\n title?: string;\n message?: string;\n onRetry?: () => void;\n className?: string;\n}\n\nexport const ErrorView: FC<ErrorViewProps> = ({\n title = 'Something went wrong',\n message,\n onRetry,\n className,\n}) => (\n <div\n className={cn(\n 'flex min-h-[40vh] flex-col items-center justify-center gap-4 text-center',\n className\n )}\n >\n <AlertCircle className=\"h-10 w-10 text-status-error-text\" />\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">{title}</h2>\n {message && <p className=\"mt-1 max-w-md text-sm text-text-secondary\">{message}</p>}\n </div>\n {onRetry && (\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n Retry\n </button>\n )}\n </div>\n);\n\ninterface SectionProps {\n title: string;\n description?: string;\n action?: ReactNode;\n children: ReactNode;\n className?: string;\n}\n\nexport const Section: FC<SectionProps> = ({ title, description, action, children, className }) => (\n <section className={cn('mt-8 first:mt-0', className)}>\n <div className=\"mb-3 flex items-end justify-between gap-3\">\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">{title}</h2>\n {description && <p className=\"mt-0.5 text-sm text-text-secondary\">{description}</p>}\n </div>\n {action && <div className=\"shrink-0\">{action}</div>}\n </div>\n {children}\n </section>\n);\n"],"mappings":";;;;;AAeA,IAAa,KAAqC,EAAE,WAAQ,YAAY,mBACtE,kBAAC,OAAD;CAAK,WAAW,EAAG,gEAAgE,EAAU;WAA7F,CACE,kBAAC,GAAD,EAAS,MAAK,MAAO,CAAA,EACrB,kBAAC,KAAD;EAAG,WAAU;YAA+B;EAAU,CAAA,CAClD;IAUK,KAAiC,EAC5C,WAAQ,wBACR,YACA,YACA,mBAEA,kBAAC,OAAD;CACE,WAAW,EACT,4EACA,EACD;WAJH;EAME,kBAAC,GAAD,EAAa,WAAU,oCAAqC,CAAA;EAC5D,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;GAAI,WAAU;aAA2C;GAAW,CAAA,EACnE,KAAW,kBAAC,KAAD;GAAG,WAAU;aAA6C;GAAY,CAAA,CAC9E,EAAA,CAAA;EACL,KACC,kBAAC,UAAD;GACE,MAAK;GACL,SAAS;GACT,WAAU;aACX;GAEQ,CAAA;EAEP"}
1
+ {"version":3,"file":"StateViews.js","names":[],"sources":["../../../src/comradecircle/components/StateViews.tsx"],"sourcesContent":["/**\n * Shared loading / error / section state views\n * @module comradecircle/components\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { AlertCircle } from 'lucide-react';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface LoadingViewProps {\n label?: string;\n className?: string;\n}\n\nexport const LoadingView: FC<LoadingViewProps> = ({ label = 'Loading…', className }) => (\n <div className={cn('flex min-h-[40vh] flex-col items-center justify-center gap-3', className)}>\n <Spinner size=\"lg\" />\n <p className=\"text-sm text-text-secondary\">{label}</p>\n </div>\n);\n\ninterface ErrorViewProps {\n title?: string;\n message?: string;\n onRetry?: () => void;\n className?: string;\n}\n\nexport const ErrorView: FC<ErrorViewProps> = ({\n title = 'Something went wrong',\n message,\n onRetry,\n className,\n}) => (\n <div\n className={cn(\n 'flex min-h-[40vh] flex-col items-center justify-center gap-4 text-center',\n className\n )}\n >\n <AlertCircle className=\"h-10 w-10 text-status-error-text\" />\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">{title}</h2>\n {message && <p className=\"mt-1 max-w-md text-sm text-text-secondary\">{message}</p>}\n </div>\n {onRetry && (\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n Retry\n </button>\n )}\n </div>\n);\n\ninterface SectionProps {\n title: string;\n description?: string;\n action?: ReactNode;\n children: ReactNode;\n className?: string;\n}\n\nexport const Section: FC<SectionProps> = ({ title, description, action, children, className }) => (\n <section className={cn('mt-8 first:mt-0', className)}>\n <div className=\"mb-3 flex items-end justify-between gap-3\">\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">{title}</h2>\n {description && <p className=\"mt-0.5 text-sm text-text-secondary\">{description}</p>}\n </div>\n {action && <div className=\"shrink-0\">{action}</div>}\n </div>\n {children}\n </section>\n);\n"],"mappings":";;;;;AAeA,IAAa,KAAqC,EAAE,WAAQ,YAAY,mBACtE,kBAAC,OAAD;CAAK,WAAW,EAAG,gEAAgE,CAAS;WAA5F,CACE,kBAAC,GAAD,EAAS,MAAK,KAAM,CAAA,GACpB,kBAAC,KAAD;EAAG,WAAU;YAA+B;CAAS,CAAA,CAClD;IAUM,KAAiC,EAC5C,WAAQ,wBACR,YACA,YACA,mBAEA,kBAAC,OAAD;CACE,WAAW,EACT,4EACA,CACF;WAJF;EAME,kBAAC,GAAD,EAAa,WAAU,mCAAoC,CAAA;EAC3D,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;GAAI,WAAU;aAA2C;EAAU,CAAA,GAClE,KAAW,kBAAC,KAAD;GAAG,WAAU;aAA6C;EAAW,CAAA,CAC9E,EAAA,CAAA;EACJ,KACC,kBAAC,UAAD;GACE,MAAK;GACL,SAAS;GACT,WAAU;aACX;EAEO,CAAA;CAEP"}
@@ -1 +1 @@
1
- {"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/comradecircle/components/StatusBadge.tsx"],"sourcesContent":["/**\n * Status Badge Component\n * @module comradecircle/components\n *\n * A colored badge for ComradeCircle domain statuses (circle status/visibility,\n * event status/mode, membership status, member role, poll status, post status,\n * presence, RSVP status) with semantic color mapping. Accepts the raw server\n * enum value.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\nimport { formatEnumLabel } from '../utils/formatters';\n\ninterface StatusBadgeProps {\n status: string;\n className?: string;\n}\n\ntype BadgeVariant = 'success' | 'warning' | 'destructive' | 'secondary' | 'info' | 'outline';\n\nconst variantStyles: Record<BadgeVariant, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n destructive: 'bg-status-error-bg-subtle text-status-error-text',\n secondary: 'bg-bg-sunken text-text-secondary',\n info: 'bg-[var(--color-accent-soft)] text-text-primary',\n outline: 'border border-border-seam text-text-primary',\n};\n\n// Statuses that represent a \"live\" / active condition get a subtle opacity-only\n// glow pulse (reduced-motion safe via motion-safe:).\nconst liveStatuses = new Set(['LIVE', 'ONLINE', 'ACTIVE']);\n\nconst statusVariantMap: Record<string, BadgeVariant> = {\n // Circle status\n ACTIVE: 'success',\n ARCHIVED: 'outline',\n SUSPENDED: 'destructive',\n\n // Circle visibility\n PUBLIC: 'success',\n PRIVATE: 'secondary',\n SECRET: 'warning',\n\n // Channel visibility\n CIRCLE: 'info',\n\n // Channel type\n ANNOUNCEMENT: 'warning',\n FORUM: 'info',\n TEXT: 'secondary',\n VOICE: 'info',\n\n // Event status\n SCHEDULED: 'info',\n LIVE: 'success',\n COMPLETED: 'secondary',\n CANCELLED: 'destructive',\n\n // Event mode\n ONLINE: 'success',\n IN_PERSON: 'success',\n HYBRID: 'warning',\n\n // Membership status\n PENDING: 'warning',\n BANNED: 'destructive',\n LEFT: 'outline',\n\n // Member role\n OWNER: 'info',\n ADMIN: 'warning',\n MODERATOR: 'info',\n MEMBER: 'secondary',\n\n // Poll status\n OPEN: 'success',\n CLOSED: 'outline',\n\n // Post status\n PUBLISHED: 'success',\n DRAFT: 'secondary',\n HIDDEN: 'warning',\n REMOVED: 'destructive',\n\n // Presence (ONLINE handled by ACTIVE→success above; keep the green convention)\n AWAY: 'warning',\n BUSY: 'destructive',\n OFFLINE: 'secondary',\n\n // RSVP status\n GOING: 'success',\n INTERESTED: 'info',\n WAITLISTED: 'warning',\n NOT_GOING: 'outline',\n};\n\nexport const StatusBadge: FC<StatusBadgeProps> = ({ status, className }) => {\n const key = status.toUpperCase();\n const variant = statusVariantMap[key] ?? 'secondary';\n const isLive = liveStatuses.has(key);\n\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',\n variantStyles[variant],\n className\n )}\n >\n {isLive && (\n <span\n aria-hidden=\"true\"\n className=\"size-1.5 rounded-full bg-current motion-safe:animate-vc-glow-pulse\"\n />\n )}\n {formatEnumLabel(status)}\n </span>\n );\n};\n"],"mappings":";;;;AAqBA,IAAM,IAA8C;CAClD,SAAS;CACT,SAAS;CACT,aAAa;CACb,WAAW;CACX,MAAM;CACN,SAAS;CACV,EAIK,IAAe,IAAI,IAAI;CAAC;CAAQ;CAAU;CAAS,CAAC,EAEpD,IAAiD;CAErD,QAAQ;CACR,UAAU;CACV,WAAW;CAGX,QAAQ;CACR,SAAS;CACT,QAAQ;CAGR,QAAQ;CAGR,cAAc;CACd,OAAO;CACP,MAAM;CACN,OAAO;CAGP,WAAW;CACX,MAAM;CACN,WAAW;CACX,WAAW;CAGX,QAAQ;CACR,WAAW;CACX,QAAQ;CAGR,SAAS;CACT,QAAQ;CACR,MAAM;CAGN,OAAO;CACP,OAAO;CACP,WAAW;CACX,QAAQ;CAGR,MAAM;CACN,QAAQ;CAGR,WAAW;CACX,OAAO;CACP,QAAQ;CACR,SAAS;CAGT,MAAM;CACN,MAAM;CACN,SAAS;CAGT,OAAO;CACP,YAAY;CACZ,YAAY;CACZ,WAAW;CACZ,EAEY,KAAqC,EAAE,WAAQ,mBAAgB;CAC1E,IAAM,IAAM,EAAO,aAAa,EAC1B,IAAU,EAAiB,MAAQ,aACnC,IAAS,EAAa,IAAI,EAAI;AAEpC,QACE,kBAAC,QAAD;EACE,WAAW,EACT,mFACA,EAAc,IACd,EACD;YALH,CAOG,KACC,kBAAC,QAAD;GACE,eAAY;GACZ,WAAU;GACV,CAAA,EAEH,EAAgB,EAAO,CACnB"}
1
+ {"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/comradecircle/components/StatusBadge.tsx"],"sourcesContent":["/**\n * Status Badge Component\n * @module comradecircle/components\n *\n * A colored badge for ComradeCircle domain statuses (circle status/visibility,\n * event status/mode, membership status, member role, poll status, post status,\n * presence, RSVP status) with semantic color mapping. Accepts the raw server\n * enum value.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\nimport { formatEnumLabel } from '../utils/formatters';\n\ninterface StatusBadgeProps {\n status: string;\n className?: string;\n}\n\ntype BadgeVariant = 'success' | 'warning' | 'destructive' | 'secondary' | 'info' | 'outline';\n\nconst variantStyles: Record<BadgeVariant, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n destructive: 'bg-status-error-bg-subtle text-status-error-text',\n secondary: 'bg-bg-sunken text-text-secondary',\n info: 'bg-[var(--color-accent-soft)] text-text-primary',\n outline: 'border border-border-seam text-text-primary',\n};\n\n// Statuses that represent a \"live\" / active condition get a subtle opacity-only\n// glow pulse (reduced-motion safe via motion-safe:).\nconst liveStatuses = new Set(['LIVE', 'ONLINE', 'ACTIVE']);\n\nconst statusVariantMap: Record<string, BadgeVariant> = {\n // Circle status\n ACTIVE: 'success',\n ARCHIVED: 'outline',\n SUSPENDED: 'destructive',\n\n // Circle visibility\n PUBLIC: 'success',\n PRIVATE: 'secondary',\n SECRET: 'warning',\n\n // Channel visibility\n CIRCLE: 'info',\n\n // Channel type\n ANNOUNCEMENT: 'warning',\n FORUM: 'info',\n TEXT: 'secondary',\n VOICE: 'info',\n\n // Event status\n SCHEDULED: 'info',\n LIVE: 'success',\n COMPLETED: 'secondary',\n CANCELLED: 'destructive',\n\n // Event mode\n ONLINE: 'success',\n IN_PERSON: 'success',\n HYBRID: 'warning',\n\n // Membership status\n PENDING: 'warning',\n BANNED: 'destructive',\n LEFT: 'outline',\n\n // Member role\n OWNER: 'info',\n ADMIN: 'warning',\n MODERATOR: 'info',\n MEMBER: 'secondary',\n\n // Poll status\n OPEN: 'success',\n CLOSED: 'outline',\n\n // Post status\n PUBLISHED: 'success',\n DRAFT: 'secondary',\n HIDDEN: 'warning',\n REMOVED: 'destructive',\n\n // Presence (ONLINE handled by ACTIVE→success above; keep the green convention)\n AWAY: 'warning',\n BUSY: 'destructive',\n OFFLINE: 'secondary',\n\n // RSVP status\n GOING: 'success',\n INTERESTED: 'info',\n WAITLISTED: 'warning',\n NOT_GOING: 'outline',\n};\n\nexport const StatusBadge: FC<StatusBadgeProps> = ({ status, className }) => {\n const key = status.toUpperCase();\n const variant = statusVariantMap[key] ?? 'secondary';\n const isLive = liveStatuses.has(key);\n\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',\n variantStyles[variant],\n className\n )}\n >\n {isLive && (\n <span\n aria-hidden=\"true\"\n className=\"size-1.5 rounded-full bg-current motion-safe:animate-vc-glow-pulse\"\n />\n )}\n {formatEnumLabel(status)}\n </span>\n );\n};\n"],"mappings":";;;;AAqBA,IAAM,IAA8C;CAClD,SAAS;CACT,SAAS;CACT,aAAa;CACb,WAAW;CACX,MAAM;CACN,SAAS;AACX,GAIM,IAAe,IAAI,IAAI;CAAC;CAAQ;CAAU;AAAQ,CAAC,GAEnD,IAAiD;CAErD,QAAQ;CACR,UAAU;CACV,WAAW;CAGX,QAAQ;CACR,SAAS;CACT,QAAQ;CAGR,QAAQ;CAGR,cAAc;CACd,OAAO;CACP,MAAM;CACN,OAAO;CAGP,WAAW;CACX,MAAM;CACN,WAAW;CACX,WAAW;CAGX,QAAQ;CACR,WAAW;CACX,QAAQ;CAGR,SAAS;CACT,QAAQ;CACR,MAAM;CAGN,OAAO;CACP,OAAO;CACP,WAAW;CACX,QAAQ;CAGR,MAAM;CACN,QAAQ;CAGR,WAAW;CACX,OAAO;CACP,QAAQ;CACR,SAAS;CAGT,MAAM;CACN,MAAM;CACN,SAAS;CAGT,OAAO;CACP,YAAY;CACZ,YAAY;CACZ,WAAW;AACb,GAEa,KAAqC,EAAE,WAAQ,mBAAgB;CAC1E,IAAM,IAAM,EAAO,YAAY,GACzB,IAAU,EAAiB,MAAQ,aACnC,IAAS,EAAa,IAAI,CAAG;CAEnC,OACE,kBAAC,QAAD;EACE,WAAW,EACT,mFACA,EAAc,IACd,CACF;YALF,CAOG,KACC,kBAAC,QAAD;GACE,eAAY;GACZ,WAAU;EACX,CAAA,GAEF,EAAgB,CAAM,CACnB;;AAEV"}
@@ -1 +1 @@
1
- {"version":3,"file":"Tabs.js","names":[],"sources":["../../../src/comradecircle/components/Tabs.tsx"],"sourcesContent":["/**\n * Tabs — a top-level segmented navigation control for multi-section pages\n * (Monitoring: Sensors/Measurements/Thresholds; Carbon: Ledger/Factors/Targets).\n * @module comradecircle/components\n *\n * Visually heavier than {@link FilterTabs} (underline-style page tabs vs a pill\n * filter). Purely presentational — the parent owns the active-tab state.\n */\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../utils/cn';\n\nexport interface TabItem<V extends string> {\n value: V;\n label: string;\n icon?: ReactNode;\n count?: number;\n}\n\ninterface TabsProps<V extends string> {\n items: TabItem<V>[];\n value: V;\n onChange: (value: V) => void;\n className?: string;\n}\n\nexport function Tabs<V extends string>({ items, value, onChange, className }: TabsProps<V>) {\n return (\n <div\n role=\"tablist\"\n className={cn('flex flex-wrap items-center gap-1 border-b border-border-seam', className)}\n >\n {items.map((item) => {\n const active = item.value === value;\n return (\n <button\n key={item.value}\n type=\"button\"\n role=\"tab\"\n aria-selected={active}\n onClick={() => onChange(item.value)}\n className={cn(\n 'relative -mb-px inline-flex items-center gap-2 border-b-2 px-4 py-2.5 text-sm font-medium transition-colors',\n active\n ? 'border-accent text-text-primary'\n : 'border-transparent text-text-secondary hover:text-text-primary'\n )}\n >\n {item.icon}\n {item.label}\n {typeof item.count === 'number' && (\n <span\n className={cn(\n 'rounded-full px-1.5 py-0.5 text-xs tabular-nums',\n active\n ? 'bg-[var(--color-accent-soft)] text-text-primary'\n : 'bg-bg-sunken text-text-secondary'\n )}\n >\n {item.count}\n </span>\n )}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;AA0BA,SAAgB,EAAuB,EAAE,UAAO,UAAO,aAAU,gBAA2B;AAC1F,QACE,kBAAC,OAAD;EACE,MAAK;EACL,WAAW,EAAG,iEAAiE,EAAU;YAExF,EAAM,KAAK,MAAS;GACnB,IAAM,IAAS,EAAK,UAAU;AAC9B,UACE,kBAAC,UAAD;IAEE,MAAK;IACL,MAAK;IACL,iBAAe;IACf,eAAe,EAAS,EAAK,MAAM;IACnC,WAAW,EACT,+GACA,IACI,oCACA,iEACL;cAXH;KAaG,EAAK;KACL,EAAK;KACL,OAAO,EAAK,SAAU,YACrB,kBAAC,QAAD;MACE,WAAW,EACT,mDACA,IACI,oDACA,mCACL;gBAEA,EAAK;MACD,CAAA;KAEF;MA1BF,EAAK,MA0BH;IAEX;EACE,CAAA"}
1
+ {"version":3,"file":"Tabs.js","names":[],"sources":["../../../src/comradecircle/components/Tabs.tsx"],"sourcesContent":["/**\n * Tabs — a top-level segmented navigation control for multi-section pages\n * (Monitoring: Sensors/Measurements/Thresholds; Carbon: Ledger/Factors/Targets).\n * @module comradecircle/components\n *\n * Visually heavier than {@link FilterTabs} (underline-style page tabs vs a pill\n * filter). Purely presentational — the parent owns the active-tab state.\n */\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../utils/cn';\n\nexport interface TabItem<V extends string> {\n value: V;\n label: string;\n icon?: ReactNode;\n count?: number;\n}\n\ninterface TabsProps<V extends string> {\n items: TabItem<V>[];\n value: V;\n onChange: (value: V) => void;\n className?: string;\n}\n\nexport function Tabs<V extends string>({ items, value, onChange, className }: TabsProps<V>) {\n return (\n <div\n role=\"tablist\"\n className={cn('flex flex-wrap items-center gap-1 border-b border-border-seam', className)}\n >\n {items.map((item) => {\n const active = item.value === value;\n return (\n <button\n key={item.value}\n type=\"button\"\n role=\"tab\"\n aria-selected={active}\n onClick={() => onChange(item.value)}\n className={cn(\n 'relative -mb-px inline-flex items-center gap-2 border-b-2 px-4 py-2.5 text-sm font-medium transition-colors',\n active\n ? 'border-accent text-text-primary'\n : 'border-transparent text-text-secondary hover:text-text-primary'\n )}\n >\n {item.icon}\n {item.label}\n {typeof item.count === 'number' && (\n <span\n className={cn(\n 'rounded-full px-1.5 py-0.5 text-xs tabular-nums',\n active\n ? 'bg-[var(--color-accent-soft)] text-text-primary'\n : 'bg-bg-sunken text-text-secondary'\n )}\n >\n {item.count}\n </span>\n )}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;AA0BA,SAAgB,EAAuB,EAAE,UAAO,UAAO,aAAU,gBAA2B;CAC1F,OACE,kBAAC,OAAD;EACE,MAAK;EACL,WAAW,EAAG,iEAAiE,CAAS;YAEvF,EAAM,KAAK,MAAS;GACnB,IAAM,IAAS,EAAK,UAAU;GAC9B,OACE,kBAAC,UAAD;IAEE,MAAK;IACL,MAAK;IACL,iBAAe;IACf,eAAe,EAAS,EAAK,KAAK;IAClC,WAAW,EACT,+GACA,IACI,oCACA,gEACN;cAXF;KAaG,EAAK;KACL,EAAK;KACL,OAAO,EAAK,SAAU,YACrB,kBAAC,QAAD;MACE,WAAW,EACT,mDACA,IACI,oDACA,kCACN;gBAEC,EAAK;KACF,CAAA;IAEF;MA1BD,EAAK,KA0BJ;EAEZ,CAAC;CACE,CAAA;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"TagInput.js","names":[],"sources":["../../../src/comradecircle/components/TagInput.tsx"],"sourcesContent":["/**\n * TagInput — a lightweight chip-based tag editor.\n * @module comradecircle/components\n *\n * Enter or comma commits a tag; Backspace on an empty field removes the last.\n * Purely controlled — the parent owns the string[] value.\n */\n\nimport { useState, type KeyboardEvent } from 'react';\nimport { X } from 'lucide-react';\nimport { Input } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface TagInputProps {\n value: string[];\n onChange: (tags: string[]) => void;\n placeholder?: string;\n max?: number;\n className?: string;\n id?: string;\n}\n\nexport function TagInput({\n value,\n onChange,\n placeholder = 'Add a tag…',\n max = 12,\n className,\n id,\n}: TagInputProps) {\n const [draft, setDraft] = useState('');\n\n const commit = (raw: string) => {\n const tag = raw.trim().replace(/^#/, '').toLowerCase();\n if (!tag) return;\n if (value.includes(tag)) {\n setDraft('');\n return;\n }\n if (value.length >= max) return;\n onChange([...value, tag]);\n setDraft('');\n };\n\n const remove = (tag: string) => onChange(value.filter((t) => t !== tag));\n\n const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter' || e.key === ',') {\n e.preventDefault();\n commit(draft);\n } else if (e.key === 'Backspace' && !draft && value.length > 0) {\n remove(value[value.length - 1]!);\n }\n };\n\n return (\n <div\n className={cn(\n 'flex flex-wrap items-center gap-1.5 rounded-xl border border-border-seam bg-bg-surface px-2 py-1.5',\n className\n )}\n >\n {value.map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\"\n >\n #{tag}\n <button\n type=\"button\"\n onClick={() => remove(tag)}\n aria-label={`Remove ${tag}`}\n className=\"text-text-tertiary hover:text-text-primary\"\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n ))}\n <Input\n id={id}\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={onKeyDown}\n onBlur={() => commit(draft)}\n placeholder={value.length === 0 ? placeholder : ''}\n className=\"h-7 min-w-[8rem] flex-1 border-0 bg-transparent px-1 shadow-none focus-visible:ring-0\"\n />\n </div>\n );\n}\n"],"mappings":";;;;;;AAsBA,SAAgB,EAAS,EACvB,UACA,aACA,iBAAc,cACd,SAAM,IACN,cACA,SACgB;CAChB,IAAM,CAAC,GAAO,KAAY,EAAS,GAAG,EAEhC,KAAU,MAAgB;EAC9B,IAAM,IAAM,EAAI,MAAM,CAAC,QAAQ,MAAM,GAAG,CAAC,aAAa;AACjD,SACL;OAAI,EAAM,SAAS,EAAI,EAAE;AACvB,MAAS,GAAG;AACZ;;AAEE,KAAM,UAAU,MACpB,EAAS,CAAC,GAAG,GAAO,EAAI,CAAC,EACzB,EAAS,GAAG;;IAGR,KAAU,MAAgB,EAAS,EAAM,QAAQ,MAAM,MAAM,EAAI,CAAC;AAWxE,QACE,kBAAC,OAAD;EACE,WAAW,EACT,sGACA,EACD;YAJH,CAMG,EAAM,KAAK,MACV,kBAAC,QAAD;GAEE,WAAU;aAFZ;IAGC;IACG;IACF,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAO,EAAI;KAC1B,cAAY,UAAU;KACtB,WAAU;eAEV,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;KAClB,CAAA;IACJ;KAZA,EAYA,CACP,EACF,kBAAC,GAAD;GACM;GACJ,OAAO;GACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;GAC9B,YApCE,MAAuC;AACxD,IAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,OACjC,EAAE,gBAAgB,EAClB,EAAO,EAAM,IACJ,EAAE,QAAQ,eAAe,CAAC,KAAS,EAAM,SAAS,KAC3D,EAAO,EAAM,EAAM,SAAS,GAAI;;GAgC9B,cAAc,EAAO,EAAM;GAC3B,aAAa,EAAM,WAAW,IAAI,IAAc;GAChD,WAAU;GACV,CAAA,CACE"}
1
+ {"version":3,"file":"TagInput.js","names":[],"sources":["../../../src/comradecircle/components/TagInput.tsx"],"sourcesContent":["/**\n * TagInput — a lightweight chip-based tag editor.\n * @module comradecircle/components\n *\n * Enter or comma commits a tag; Backspace on an empty field removes the last.\n * Purely controlled — the parent owns the string[] value.\n */\n\nimport { useState, type KeyboardEvent } from 'react';\nimport { X } from 'lucide-react';\nimport { Input } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface TagInputProps {\n value: string[];\n onChange: (tags: string[]) => void;\n placeholder?: string;\n max?: number;\n className?: string;\n id?: string;\n}\n\nexport function TagInput({\n value,\n onChange,\n placeholder = 'Add a tag…',\n max = 12,\n className,\n id,\n}: TagInputProps) {\n const [draft, setDraft] = useState('');\n\n const commit = (raw: string) => {\n const tag = raw.trim().replace(/^#/, '').toLowerCase();\n if (!tag) return;\n if (value.includes(tag)) {\n setDraft('');\n return;\n }\n if (value.length >= max) return;\n onChange([...value, tag]);\n setDraft('');\n };\n\n const remove = (tag: string) => onChange(value.filter((t) => t !== tag));\n\n const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter' || e.key === ',') {\n e.preventDefault();\n commit(draft);\n } else if (e.key === 'Backspace' && !draft && value.length > 0) {\n remove(value[value.length - 1]!);\n }\n };\n\n return (\n <div\n className={cn(\n 'flex flex-wrap items-center gap-1.5 rounded-xl border border-border-seam bg-bg-surface px-2 py-1.5',\n className\n )}\n >\n {value.map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\"\n >\n #{tag}\n <button\n type=\"button\"\n onClick={() => remove(tag)}\n aria-label={`Remove ${tag}`}\n className=\"text-text-tertiary hover:text-text-primary\"\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n ))}\n <Input\n id={id}\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={onKeyDown}\n onBlur={() => commit(draft)}\n placeholder={value.length === 0 ? placeholder : ''}\n className=\"h-7 min-w-[8rem] flex-1 border-0 bg-transparent px-1 shadow-none focus-visible:ring-0\"\n />\n </div>\n );\n}\n"],"mappings":";;;;;;AAsBA,SAAgB,EAAS,EACvB,UACA,aACA,iBAAc,cACd,SAAM,IACN,cACA,SACgB;CAChB,IAAM,CAAC,GAAO,KAAY,EAAS,EAAE,GAE/B,KAAU,MAAgB;EAC9B,IAAM,IAAM,EAAI,KAAK,EAAE,QAAQ,MAAM,EAAE,EAAE,YAAY;EAChD,OACL;OAAI,EAAM,SAAS,CAAG,GAAG;IACvB,EAAS,EAAE;IACX;GACF;GACI,EAAM,UAAU,MACpB,EAAS,CAAC,GAAG,GAAO,CAAG,CAAC,GACxB,EAAS,EAAE;EAHX;CAIF,GAEM,KAAU,MAAgB,EAAS,EAAM,QAAQ,MAAM,MAAM,CAAG,CAAC;CAWvE,OACE,kBAAC,OAAD;EACE,WAAW,EACT,sGACA,CACF;YAJF,CAMG,EAAM,KAAK,MACV,kBAAC,QAAD;GAEE,WAAU;aAFZ;IAGC;IACG;IACF,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAO,CAAG;KACzB,cAAY,UAAU;KACtB,WAAU;eAEV,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;IAClB,CAAA;GACJ;KAZC,CAYD,CACP,GACD,kBAAC,GAAD;GACM;GACJ,OAAO;GACP,WAAW,MAAM,EAAS,EAAE,OAAO,KAAK;GAC7B,YApCE,MAAuC;IACxD,AAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,OACjC,EAAE,eAAe,GACjB,EAAO,CAAK,KACH,EAAE,QAAQ,eAAe,CAAC,KAAS,EAAM,SAAS,KAC3D,EAAO,EAAM,EAAM,SAAS,EAAG;GAEnC;GA8BM,cAAc,EAAO,CAAK;GAC1B,aAAa,EAAM,WAAW,IAAI,IAAc;GAChD,WAAU;EACX,CAAA,CACE;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"ComradeCircleContext.js","names":[],"sources":["../../../src/comradecircle/context/ComradeCircleContext.tsx"],"sourcesContent":["/**\n * ComradeCircle Context Provider\n * @module comradecircle/context\n *\n * Splits standalone mode (wraps its own MemoryRouter + useNavigate) from\n * integrated mode (the host shell supplies `navigate`). Exposes gateway/auth\n * context + relative navigation helpers to every page.\n */\n\nimport type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo, useCallback } from 'react';\nimport { MemoryRouter, useNavigate as useRouterNavigate } from 'react-router-dom';\nimport type { ComradeCircleRootProps } from '../types';\nimport { joinPath } from '../utils/navigation';\n\ninterface ComradeCircleContextValue extends Omit<\n ComradeCircleRootProps,\n 'registerNavItems' | 'registerFooterItems' | 'navigate'\n> {\n isStandalone: boolean;\n /** Shell's navigate function (only available in integrated mode). */\n navigate?: (path: string) => void;\n /** Navigate to a path relative to basePath. */\n navigateTo: (relativePath: string) => void;\n /** Get the full path for a relative path. */\n getPath: (relativePath: string) => string;\n}\n\nconst ComradeCircleContext = createContext<ComradeCircleContextValue | null>(null);\n\nexport function useComradeCircle(): ComradeCircleContextValue {\n const context = useContext(ComradeCircleContext);\n if (!context) {\n throw new Error('useComradeCircle must be used within a ComradeCircleProvider');\n }\n return context;\n}\n\ntype ProviderProps = Omit<ComradeCircleRootProps, 'registerNavItems' | 'registerFooterItems'> & {\n isStandalone: boolean;\n};\n\nconst StandaloneProviderInner: FC<PropsWithChildren<ProviderProps>> = ({\n children,\n basePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n}) => {\n const routerNavigate = useRouterNavigate();\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n routerNavigate(joinPath(effectiveBasePath, relativePath));\n },\n [effectiveBasePath, routerNavigate]\n );\n\n const contextValue = useMemo<ComradeCircleContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate: undefined,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n isStandalone: true,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n navigateTo,\n getPath,\n ]\n );\n\n return (\n <ComradeCircleContext.Provider value={contextValue}>{children}</ComradeCircleContext.Provider>\n );\n};\n\nconst IntegratedProviderInner: FC<\n PropsWithChildren<ProviderProps & { navigate: (path: string) => void }>\n> = ({\n children,\n basePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n}) => {\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n navigate(joinPath(effectiveBasePath, relativePath));\n },\n [effectiveBasePath, navigate]\n );\n\n const contextValue = useMemo<ComradeCircleContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n isStandalone: false,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n navigateTo,\n getPath,\n ]\n );\n\n return (\n <ComradeCircleContext.Provider value={contextValue}>{children}</ComradeCircleContext.Provider>\n );\n};\n\nexport const ComradeCircleProvider: FC<PropsWithChildren<ComradeCircleRootProps>> = (props) => {\n const { children, navigate } = props;\n const isStandalone = !navigate;\n\n if (isStandalone) {\n return (\n <MemoryRouter>\n <StandaloneProviderInner {...props} isStandalone={true}>\n {children}\n </StandaloneProviderInner>\n </MemoryRouter>\n );\n }\n\n return (\n <IntegratedProviderInner {...props} navigate={navigate} isStandalone={false}>\n {children}\n </IntegratedProviderInner>\n );\n};\n"],"mappings":";;;;;AA4BA,IAAM,IAAuB,EAAgD,KAAK;AAElF,SAAgB,IAA8C;CAC5D,IAAM,IAAU,EAAW,EAAqB;AAChD,KAAI,CAAC,EACH,OAAU,MAAM,+DAA+D;AAEjF,QAAO;;AAOT,IAAM,KAAiE,EACrE,aACA,aACA,gBACA,aACA,gBACA,kBACA,mBACI;CACJ,IAAM,IAAiB,GAAmB,EACpC,IAAoB,KAAY,KAEhC,IAAU,GACb,MAAyB,EAAS,GAAmB,EAAa,EACnE,CAAC,EAAkB,CACpB,EAEK,IAAa,GAChB,MAAyB;AACxB,IAAe,EAAS,GAAmB,EAAa,CAAC;IAE3D,CAAC,GAAmB,EAAe,CACpC,EAEK,IAAe,SACZ;EACL,UAAU;EACV,UAAU,KAAA;EACV;EACA;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;EACD,GACD;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF;AAED,QACE,kBAAC,EAAqB,UAAtB;EAA+B,OAAO;EAAe;EAAyC,CAAA;GAI5F,KAED,EACH,aACA,aACA,aACA,gBACA,aACA,gBACA,kBACA,mBACI;CACJ,IAAM,IAAoB,KAAY,KAEhC,IAAU,GACb,MAAyB,EAAS,GAAmB,EAAa,EACnE,CAAC,EAAkB,CACpB,EAEK,IAAa,GAChB,MAAyB;AACxB,IAAS,EAAS,GAAmB,EAAa,CAAC;IAErD,CAAC,GAAmB,EAAS,CAC9B,EAEK,IAAe,SACZ;EACL,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;EACD,GACD;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF;AAED,QACE,kBAAC,EAAqB,UAAtB;EAA+B,OAAO;EAAe;EAAyC,CAAA;GAIrF,KAAwE,MAAU;CAC7F,IAAM,EAAE,aAAU,gBAAa;AAa/B,QAZsB,IAapB,kBAAC,GAAD;EAAyB,GAAI;EAAiB;EAAU,cAAc;EACnE;EACuB,CAAA,GAXxB,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;EAAyB,GAAI;EAAO,cAAc;EAC/C;EACuB,CAAA,EACb,CAAA"}
1
+ {"version":3,"file":"ComradeCircleContext.js","names":[],"sources":["../../../src/comradecircle/context/ComradeCircleContext.tsx"],"sourcesContent":["/**\n * ComradeCircle Context Provider\n * @module comradecircle/context\n *\n * Splits standalone mode (wraps its own MemoryRouter + useNavigate) from\n * integrated mode (the host shell supplies `navigate`). Exposes gateway/auth\n * context + relative navigation helpers to every page.\n */\n\nimport type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo, useCallback } from 'react';\nimport { MemoryRouter, useNavigate as useRouterNavigate } from 'react-router-dom';\nimport type { ComradeCircleRootProps } from '../types';\nimport { joinPath } from '../utils/navigation';\n\ninterface ComradeCircleContextValue extends Omit<\n ComradeCircleRootProps,\n 'registerNavItems' | 'registerFooterItems' | 'navigate'\n> {\n isStandalone: boolean;\n /** Shell's navigate function (only available in integrated mode). */\n navigate?: (path: string) => void;\n /** Navigate to a path relative to basePath. */\n navigateTo: (relativePath: string) => void;\n /** Get the full path for a relative path. */\n getPath: (relativePath: string) => string;\n}\n\nconst ComradeCircleContext = createContext<ComradeCircleContextValue | null>(null);\n\nexport function useComradeCircle(): ComradeCircleContextValue {\n const context = useContext(ComradeCircleContext);\n if (!context) {\n throw new Error('useComradeCircle must be used within a ComradeCircleProvider');\n }\n return context;\n}\n\ntype ProviderProps = Omit<ComradeCircleRootProps, 'registerNavItems' | 'registerFooterItems'> & {\n isStandalone: boolean;\n};\n\nconst StandaloneProviderInner: FC<PropsWithChildren<ProviderProps>> = ({\n children,\n basePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n}) => {\n const routerNavigate = useRouterNavigate();\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n routerNavigate(joinPath(effectiveBasePath, relativePath));\n },\n [effectiveBasePath, routerNavigate]\n );\n\n const contextValue = useMemo<ComradeCircleContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate: undefined,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n isStandalone: true,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n navigateTo,\n getPath,\n ]\n );\n\n return (\n <ComradeCircleContext.Provider value={contextValue}>{children}</ComradeCircleContext.Provider>\n );\n};\n\nconst IntegratedProviderInner: FC<\n PropsWithChildren<ProviderProps & { navigate: (path: string) => void }>\n> = ({\n children,\n basePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n}) => {\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n navigate(joinPath(effectiveBasePath, relativePath));\n },\n [effectiveBasePath, navigate]\n );\n\n const contextValue = useMemo<ComradeCircleContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n isStandalone: false,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n navigateTo,\n getPath,\n ]\n );\n\n return (\n <ComradeCircleContext.Provider value={contextValue}>{children}</ComradeCircleContext.Provider>\n );\n};\n\nexport const ComradeCircleProvider: FC<PropsWithChildren<ComradeCircleRootProps>> = (props) => {\n const { children, navigate } = props;\n const isStandalone = !navigate;\n\n if (isStandalone) {\n return (\n <MemoryRouter>\n <StandaloneProviderInner {...props} isStandalone={true}>\n {children}\n </StandaloneProviderInner>\n </MemoryRouter>\n );\n }\n\n return (\n <IntegratedProviderInner {...props} navigate={navigate} isStandalone={false}>\n {children}\n </IntegratedProviderInner>\n );\n};\n"],"mappings":";;;;;AA4BA,IAAM,IAAuB,EAAgD,IAAI;AAEjF,SAAgB,IAA8C;CAC5D,IAAM,IAAU,EAAW,CAAoB;CAC/C,IAAI,CAAC,GACH,MAAU,MAAM,8DAA8D;CAEhF,OAAO;AACT;AAMA,IAAM,KAAiE,EACrE,aACA,aACA,gBACA,aACA,gBACA,kBACA,mBACI;CACJ,IAAM,IAAiB,EAAkB,GACnC,IAAoB,KAAY,KAEhC,IAAU,GACb,MAAyB,EAAS,GAAmB,CAAY,GAClE,CAAC,CAAiB,CACpB,GAEM,IAAa,GAChB,MAAyB;EACxB,EAAe,EAAS,GAAmB,CAAY,CAAC;CAC1D,GACA,CAAC,GAAmB,CAAc,CACpC,GAEM,IAAe,SACZ;EACL,UAAU;EACV,UAAU,KAAA;EACV;EACA;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;CACF,IACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CACF;CAEA,OACE,kBAAC,EAAqB,UAAtB;EAA+B,OAAO;EAAe;CAAwC,CAAA;AAEjG,GAEM,KAED,EACH,aACA,aACA,aACA,gBACA,aACA,gBACA,kBACA,mBACI;CACJ,IAAM,IAAoB,KAAY,KAEhC,IAAU,GACb,MAAyB,EAAS,GAAmB,CAAY,GAClE,CAAC,CAAiB,CACpB,GAEM,IAAa,GAChB,MAAyB;EACxB,EAAS,EAAS,GAAmB,CAAY,CAAC;CACpD,GACA,CAAC,GAAmB,CAAQ,CAC9B,GAEM,IAAe,SACZ;EACL,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;CACF,IACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CACF;CAEA,OACE,kBAAC,EAAqB,UAAtB;EAA+B,OAAO;EAAe;CAAwC,CAAA;AAEjG,GAEa,KAAwE,MAAU;CAC7F,IAAM,EAAE,aAAU,gBAAa;CAa/B,OAZsB,IAapB,kBAAC,GAAD;EAAyB,GAAI;EAAiB;EAAU,cAAc;EACnE;CACsB,CAAA,IAXvB,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;EAAyB,GAAI;EAAO,cAAc;EAC/C;CACsB,CAAA,EACb,CAAA;AASpB"}
@@ -1 +1 @@
1
- {"version":3,"file":"useComradeCircleApi.js","names":[],"sources":["../../../src/comradecircle/hooks/useComradeCircleApi.ts"],"sourcesContent":["/**\n * ComradeCircle API hooks\n * @module comradecircle/hooks\n *\n * React-Query hooks wired DIRECTLY to the real `wspace-comradecircle-svc`\n * federated GraphQL contract (the `com*` operations). Every query/mutation\n * below uses an inline gql string whose fields match the svc\n * `.hive-schema.graphql` exactly. No mock data.\n *\n * Transport: `graphqlFetch({ gateway: 'workspace', workspaceToken: true })` —\n * the host shell supplies the gateway base URL + a workspace-scoped token.\n *\n * IMPORTANT: `User`/`File` are id-only federation stubs in this subgraph. We\n * never select `author { firstName }` or `avatar { url }`. Display identity is\n * resolved through `ComProfile` (see `useComProfile`).\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\nimport { useComradeCircle } from '../context';\nimport { clearGraphqlResponseCache } from '../lib/graphql-cache';\nimport type {\n ComConnection,\n ComCircle,\n ComCircleStatus,\n ComCircleVisibility,\n ComChannel,\n ComComment,\n ComPost,\n ComPostStatus,\n ComPoll,\n ComPollStatus,\n ComPollOption,\n ComEvent,\n ComEventStatus,\n ComEventRsvp,\n ComRsvpStatus,\n ComProfile,\n ComMembership,\n ComMembershipStatus,\n ComMemberRole,\n ComFeedFollow,\n ComFollowTargetType,\n ComReaction,\n ComReactionType,\n ComCreateCircleInput,\n ComUpdateCircleInput,\n ComCreateChannelInput,\n ComUpdateChannelInput,\n ComCreatePostInput,\n ComUpdatePostInput,\n ComCreateCommentInput,\n ComCreateEventInput,\n ComUpdateEventInput,\n ComCreatePollInput,\n ComFollowInput,\n ComUpsertProfileInput,\n PaginationInput,\n} from '../types';\n\n// ============================================================================\n// Transport\n// ============================================================================\n\nconst isMutation = (query: string): boolean => /^\\s*mutation\\b/.test(query);\n\nconst comQuery = async <TData>(\n query: string,\n variables: Record<string, unknown>,\n baseUrl?: string\n): Promise<TData> => {\n // A mutation's own request must never be served from (or coalesced onto) the\n // read cache, and the resolved read cache must be dropped once it commits so\n // the React-Query refetch that follows sees the new state — not a stale\n // pre-write response held for up to 15s. Clear before AND after the write.\n const mutating = isMutation(query);\n if (mutating) clearGraphqlResponseCache();\n\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl,\n query,\n variables,\n workspaceToken: true,\n });\n\n if (mutating) clearGraphqlResponseCache();\n\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n return response.data;\n};\n\n// React-Query key roots (one namespace per domain entity).\nconst KEY = {\n circles: ['comradecircle', 'circles'] as const,\n circle: (id: string) => ['comradecircle', 'circle', id] as const,\n channels: (circleId: string) => ['comradecircle', 'channels', circleId] as const,\n posts: (channelId: string) => ['comradecircle', 'posts', channelId] as const,\n post: (id: string) => ['comradecircle', 'post', id] as const,\n comments: (postId: string) => ['comradecircle', 'comments', postId] as const,\n feed: ['comradecircle', 'feed'] as const,\n follows: ['comradecircle', 'follows'] as const,\n events: ['comradecircle', 'events'] as const,\n event: (id: string) => ['comradecircle', 'event', id] as const,\n polls: (circleId: string) => ['comradecircle', 'polls', circleId] as const,\n poll: (id: string) => ['comradecircle', 'poll', id] as const,\n profiles: ['comradecircle', 'profiles'] as const,\n profile: (memberId: string) => ['comradecircle', 'profile', memberId] as const,\n memberships: ['comradecircle', 'memberships'] as const,\n};\n\nconst DEFAULT_PAGE: PaginationInput = { limit: 50 };\n\n/** Drop `cursor` when it is nullish so the svc never sees an explicit null. */\nconst page = (p?: PaginationInput): PaginationInput => {\n const base = p ?? DEFAULT_PAGE;\n const result: PaginationInput = { limit: base.limit ?? DEFAULT_PAGE.limit };\n if (base.offset != null) result.offset = base.offset;\n if (base.cursor != null) result.cursor = base.cursor;\n return result;\n};\n\n// ============================================================================\n// Reusable gql field selections (codegen-free strictness)\n// ============================================================================\n\nconst CHANNEL_FIELDS = `\n id workspaceId circleId name slug description type visibility position isArchived\n createdAt updatedAt\n`;\n\nconst CIRCLE_FIELDS = `\n id workspaceId name slug description topic tags status visibility memberCount\n avatar { id } createdAt updatedAt\n`;\n\nconst REACTION_FIELDS = `id postId memberId type createdAt`;\n\nconst COMMENT_FIELDS = `\n id workspaceId postId parentId authorId body isHidden createdAt updatedAt\n`;\n\nconst POST_FIELDS = `\n id workspaceId channelId authorId body status tags attachmentFileIds isPinned\n reactionCount commentCount reactions { ${REACTION_FIELDS} } createdAt updatedAt\n`;\n\nconst POLL_OPTION_FIELDS = `id workspaceId pollId label position voteCount createdAt`;\n\nconst POLL_FIELDS = `\n id workspaceId circleId question allowMultiple isAnonymous status closesAt\n options { ${POLL_OPTION_FIELDS} } createdAt updatedAt\n`;\n\nconst RSVP_FIELDS = `\n id workspaceId eventId memberId status note createdAt updatedAt\n`;\n\nconst EVENT_FIELDS = `\n id workspaceId circleId hostId title description mode status location joinUrl\n capacity startsAt endsAt tags rsvpCount createdAt updatedAt\n`;\n\nconst PROFILE_FIELDS = `\n id workspaceId memberId displayName headline bio avatar { id } interests skills\n presence discoverable createdAt updatedAt\n`;\n\nconst MEMBERSHIP_FIELDS = `\n id workspaceId circleId memberId role status joinedAt createdAt updatedAt\n`;\n\nconst FOLLOW_FIELDS = `\n id workspaceId followerId targetId targetType muted createdAt updatedAt\n`;\n\n// ============================================================================\n// Circles\n// ============================================================================\n\nexport interface CircleFilters {\n status?: ComCircleStatus;\n visibility?: ComCircleVisibility;\n pagination?: PaginationInput;\n}\n\nexport const useComCircles = (filters: CircleFilters = {}) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: [...KEY.circles, filters],\n queryFn: async () => {\n const result = await comQuery<{ comListCircles: ComConnection<ComCircle> }>(\n `query ComListCircles($status: ComCircleStatus, $visibility: ComCircleVisibility, $pagination: PaginationInput) {\n comListCircles(status: $status, visibility: $visibility, pagination: $pagination) {\n items { ${CIRCLE_FIELDS} channels { id } }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n status: filters.status,\n visibility: filters.visibility,\n pagination: page(filters.pagination),\n },\n apiGatewayUrl\n );\n return result.comListCircles;\n },\n });\n};\n\nexport const useComCircle = (id: string | undefined) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: KEY.circle(id ?? ''),\n enabled: Boolean(id),\n queryFn: async () => {\n const result = await comQuery<{ comCircle: ComCircle | null }>(\n `query ComCircle($id: ID!) {\n comCircle(id: $id) {\n ${CIRCLE_FIELDS}\n channels { ${CHANNEL_FIELDS} }\n memberships { ${MEMBERSHIP_FIELDS} }\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.comCircle;\n },\n });\n};\n\nexport const useComCreateCircle = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComCreateCircleInput) => {\n const result = await comQuery<{ comCreateCircle: ComCircle }>(\n `mutation ComCreateCircle($input: ComCreateCircleInput!) {\n comCreateCircle(input: $input) { ${CIRCLE_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.comCreateCircle;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.circles }),\n });\n};\n\nexport const useComUpdateCircle = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; input: ComUpdateCircleInput }) => {\n const result = await comQuery<{ comUpdateCircle: ComCircle }>(\n `mutation ComUpdateCircle($id: ID!, $input: ComUpdateCircleInput!) {\n comUpdateCircle(id: $id, input: $input) { ${CIRCLE_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.comUpdateCircle;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.circles });\n qc.invalidateQueries({ queryKey: KEY.circle(data.id) });\n },\n });\n};\n\nexport const useComArchiveCircle = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await comQuery<{ comArchiveCircle: boolean }>(\n `mutation ComArchiveCircle($id: ID!) { comArchiveCircle(id: $id) }`,\n { id },\n apiGatewayUrl\n );\n return result.comArchiveCircle;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.circles }),\n });\n};\n\nexport const useComJoinCircle = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (circleId: string) => {\n const result = await comQuery<{ comJoinCircle: ComMembership }>(\n `mutation ComJoinCircle($circleId: ID!) {\n comJoinCircle(circleId: $circleId) { ${MEMBERSHIP_FIELDS} }\n }`,\n { circleId },\n apiGatewayUrl\n );\n return result.comJoinCircle;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.circles });\n qc.invalidateQueries({ queryKey: KEY.circle(data.circleId) });\n qc.invalidateQueries({ queryKey: KEY.memberships });\n },\n });\n};\n\nexport const useComLeaveCircle = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (circleId: string) => {\n const result = await comQuery<{ comLeaveCircle: boolean }>(\n `mutation ComLeaveCircle($circleId: ID!) { comLeaveCircle(circleId: $circleId) }`,\n { circleId },\n apiGatewayUrl\n );\n return result.comLeaveCircle;\n },\n onSuccess: (_data, circleId) => {\n qc.invalidateQueries({ queryKey: KEY.circles });\n qc.invalidateQueries({ queryKey: KEY.circle(circleId) });\n qc.invalidateQueries({ queryKey: KEY.memberships });\n },\n });\n};\n\nexport const useComSetMemberRole = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { membershipId: string; role: ComMemberRole; circleId?: string }) => {\n const result = await comQuery<{ comSetMemberRole: ComMembership }>(\n `mutation ComSetMemberRole($membershipId: ID!, $role: ComMemberRole!) {\n comSetMemberRole(membershipId: $membershipId, role: $role) { ${MEMBERSHIP_FIELDS} }\n }`,\n { membershipId: vars.membershipId, role: vars.role },\n apiGatewayUrl\n );\n return result.comSetMemberRole;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.circle(data.circleId) });\n qc.invalidateQueries({ queryKey: KEY.memberships });\n },\n });\n};\n\nexport interface MembershipFilters {\n circleId?: string;\n status?: ComMembershipStatus;\n pagination?: PaginationInput;\n}\n\nexport const useComMemberships = (filters: MembershipFilters = {}) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: [...KEY.memberships, filters],\n queryFn: async () => {\n const result = await comQuery<{ comListMemberships: ComConnection<ComMembership> }>(\n `query ComListMemberships($circleId: ID, $status: ComMembershipStatus, $pagination: PaginationInput) {\n comListMemberships(circleId: $circleId, status: $status, pagination: $pagination) {\n items { ${MEMBERSHIP_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n circleId: filters.circleId,\n status: filters.status,\n pagination: page(filters.pagination),\n },\n apiGatewayUrl\n );\n return result.comListMemberships;\n },\n });\n};\n\n// ============================================================================\n// Channels\n// ============================================================================\n\nexport const useComChannels = (circleId: string | undefined) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: KEY.channels(circleId ?? ''),\n enabled: Boolean(circleId),\n queryFn: async () => {\n const result = await comQuery<{ comListChannels: ComChannel[] }>(\n `query ComListChannels($circleId: ID!) {\n comListChannels(circleId: $circleId) { ${CHANNEL_FIELDS} }\n }`,\n { circleId },\n apiGatewayUrl\n );\n return result.comListChannels;\n },\n });\n};\n\nexport const useComCreateChannel = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComCreateChannelInput) => {\n const result = await comQuery<{ comCreateChannel: ComChannel }>(\n `mutation ComCreateChannel($input: ComCreateChannelInput!) {\n comCreateChannel(input: $input) { ${CHANNEL_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.comCreateChannel;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.channels(data.circleId) });\n qc.invalidateQueries({ queryKey: KEY.circle(data.circleId) });\n },\n });\n};\n\nexport const useComUpdateChannel = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; input: ComUpdateChannelInput; circleId: string }) => {\n const result = await comQuery<{ comUpdateChannel: ComChannel }>(\n `mutation ComUpdateChannel($id: ID!, $input: ComUpdateChannelInput!) {\n comUpdateChannel(id: $id, input: $input) { ${CHANNEL_FIELDS} }\n }`,\n { id: vars.id, input: vars.input },\n apiGatewayUrl\n );\n return result.comUpdateChannel;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.channels(data.circleId) });\n qc.invalidateQueries({ queryKey: KEY.circle(data.circleId) });\n },\n });\n};\n\n// ============================================================================\n// Posts / Reactions / Comments\n// ============================================================================\n\nexport interface PostFilters {\n channelId?: string;\n status?: ComPostStatus;\n pagination?: PaginationInput;\n}\n\nexport const useComPosts = (filters: PostFilters = {}) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: [...KEY.posts(filters.channelId ?? 'all'), filters.status, filters.pagination],\n enabled: Boolean(filters.channelId),\n queryFn: async () => {\n const result = await comQuery<{ comListPosts: ComConnection<ComPost> }>(\n `query ComListPosts($channelId: ID, $status: ComPostStatus, $pagination: PaginationInput) {\n comListPosts(channelId: $channelId, status: $status, pagination: $pagination) {\n items { ${POST_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n channelId: filters.channelId,\n status: filters.status,\n pagination: page(filters.pagination),\n },\n apiGatewayUrl\n );\n return result.comListPosts;\n },\n });\n};\n\nexport const useComPost = (id: string | undefined) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: KEY.post(id ?? ''),\n enabled: Boolean(id),\n queryFn: async () => {\n const result = await comQuery<{ comPost: ComPost | null }>(\n `query ComPost($id: ID!) {\n comPost(id: $id) {\n ${POST_FIELDS}\n comments { ${COMMENT_FIELDS} }\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.comPost;\n },\n });\n};\n\nexport const useComMyFeed = (pagination?: PaginationInput) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: [...KEY.feed, pagination],\n queryFn: async () => {\n const result = await comQuery<{ comMyFeed: ComConnection<ComPost> }>(\n `query ComMyFeed($pagination: PaginationInput) {\n comMyFeed(pagination: $pagination) {\n items { ${POST_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { pagination: page(pagination) },\n apiGatewayUrl\n );\n return result.comMyFeed;\n },\n });\n};\n\nexport const useComCreatePost = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComCreatePostInput) => {\n const result = await comQuery<{ comCreatePost: ComPost }>(\n `mutation ComCreatePost($input: ComCreatePostInput!) {\n comCreatePost(input: $input) { ${POST_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.comCreatePost;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.posts(data.channelId) });\n qc.invalidateQueries({ queryKey: KEY.feed });\n },\n });\n};\n\nexport const useComUpdatePost = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; input: ComUpdatePostInput }) => {\n const result = await comQuery<{ comUpdatePost: ComPost }>(\n `mutation ComUpdatePost($id: ID!, $input: ComUpdatePostInput!) {\n comUpdatePost(id: $id, input: $input) { ${POST_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.comUpdatePost;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.posts(data.channelId) });\n qc.invalidateQueries({ queryKey: KEY.post(data.id) });\n qc.invalidateQueries({ queryKey: KEY.feed });\n },\n });\n};\n\nexport const useComRemovePost = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; channelId?: string }) => {\n const result = await comQuery<{ comRemovePost: boolean }>(\n `mutation ComRemovePost($id: ID!) { comRemovePost(id: $id) }`,\n { id: vars.id },\n apiGatewayUrl\n );\n return result.comRemovePost;\n },\n onSuccess: (_d, vars) => {\n if (vars.channelId) qc.invalidateQueries({ queryKey: KEY.posts(vars.channelId) });\n qc.invalidateQueries({ queryKey: KEY.feed });\n },\n });\n};\n\nexport const useComReactToPost = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { postId: string; type: ComReactionType }) => {\n const result = await comQuery<{ comReactToPost: ComPost }>(\n `mutation ComReactToPost($postId: ID!, $type: ComReactionType!) {\n comReactToPost(postId: $postId, type: $type) { ${POST_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.comReactToPost;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.posts(data.channelId) });\n qc.invalidateQueries({ queryKey: KEY.post(data.id) });\n qc.invalidateQueries({ queryKey: KEY.feed });\n },\n });\n};\n\nexport const useComComments = (postId: string | undefined) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: KEY.comments(postId ?? ''),\n enabled: Boolean(postId),\n queryFn: async () => {\n const result = await comQuery<{ comListComments: ComComment[] }>(\n `query ComListComments($postId: ID!) {\n comListComments(postId: $postId) { ${COMMENT_FIELDS} }\n }`,\n { postId },\n apiGatewayUrl\n );\n return result.comListComments;\n },\n });\n};\n\nexport const useComAddComment = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComCreateCommentInput) => {\n const result = await comQuery<{ comAddComment: ComComment }>(\n `mutation ComAddComment($input: ComCreateCommentInput!) {\n comAddComment(input: $input) { ${COMMENT_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.comAddComment;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.comments(data.postId) });\n qc.invalidateQueries({ queryKey: KEY.post(data.postId) });\n qc.invalidateQueries({ queryKey: KEY.feed });\n },\n });\n};\n\n// ============================================================================\n// Feed follows\n// ============================================================================\n\nexport const useComMyFollows = (targetType?: ComFollowTargetType) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: [...KEY.follows, targetType ?? 'all'],\n queryFn: async () => {\n const result = await comQuery<{ comMyFollows: ComFeedFollow[] }>(\n `query ComMyFollows($targetType: ComFollowTargetType) {\n comMyFollows(targetType: $targetType) { ${FOLLOW_FIELDS} }\n }`,\n { targetType },\n apiGatewayUrl\n );\n return result.comMyFollows;\n },\n });\n};\n\nexport const useComFollow = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComFollowInput) => {\n const result = await comQuery<{ comFollow: ComFeedFollow }>(\n `mutation ComFollow($input: ComFollowInput!) {\n comFollow(input: $input) { ${FOLLOW_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.comFollow;\n },\n onSuccess: () => {\n qc.invalidateQueries({ queryKey: KEY.follows });\n qc.invalidateQueries({ queryKey: KEY.feed });\n },\n });\n};\n\nexport const useComUnfollow = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComFollowInput) => {\n const result = await comQuery<{ comUnfollow: boolean }>(\n `mutation ComUnfollow($input: ComFollowInput!) { comUnfollow(input: $input) }`,\n { input },\n apiGatewayUrl\n );\n return result.comUnfollow;\n },\n onSuccess: () => {\n qc.invalidateQueries({ queryKey: KEY.follows });\n qc.invalidateQueries({ queryKey: KEY.feed });\n },\n });\n};\n\nexport const useComMuteFollow = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; muted: boolean }) => {\n const result = await comQuery<{ comMuteFollow: ComFeedFollow }>(\n `mutation ComMuteFollow($id: ID!, $muted: Boolean!) {\n comMuteFollow(id: $id, muted: $muted) { ${FOLLOW_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.comMuteFollow;\n },\n onSuccess: () => {\n qc.invalidateQueries({ queryKey: KEY.follows });\n qc.invalidateQueries({ queryKey: KEY.feed });\n },\n });\n};\n\n// ============================================================================\n// Events / RSVPs\n// ============================================================================\n\nexport interface EventFilters {\n circleId?: string;\n status?: ComEventStatus;\n pagination?: PaginationInput;\n}\n\nexport const useComEvents = (filters: EventFilters = {}) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: [...KEY.events, filters],\n queryFn: async () => {\n const result = await comQuery<{ comListEvents: ComConnection<ComEvent> }>(\n `query ComListEvents($circleId: ID, $status: ComEventStatus, $pagination: PaginationInput) {\n comListEvents(circleId: $circleId, status: $status, pagination: $pagination) {\n items { ${EVENT_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n circleId: filters.circleId,\n status: filters.status,\n pagination: page(filters.pagination),\n },\n apiGatewayUrl\n );\n return result.comListEvents;\n },\n });\n};\n\nexport const useComEvent = (id: string | undefined) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: KEY.event(id ?? ''),\n enabled: Boolean(id),\n queryFn: async () => {\n const result = await comQuery<{ comEvent: ComEvent | null }>(\n `query ComEvent($id: ID!) {\n comEvent(id: $id) {\n ${EVENT_FIELDS}\n rsvps { ${RSVP_FIELDS} }\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.comEvent;\n },\n });\n};\n\nexport const useComCreateEvent = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComCreateEventInput) => {\n const result = await comQuery<{ comCreateEvent: ComEvent }>(\n `mutation ComCreateEvent($input: ComCreateEventInput!) {\n comCreateEvent(input: $input) { ${EVENT_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.comCreateEvent;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.events }),\n });\n};\n\nexport const useComUpdateEvent = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; input: ComUpdateEventInput }) => {\n const result = await comQuery<{ comUpdateEvent: ComEvent }>(\n `mutation ComUpdateEvent($id: ID!, $input: ComUpdateEventInput!) {\n comUpdateEvent(id: $id, input: $input) { ${EVENT_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.comUpdateEvent;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.events });\n qc.invalidateQueries({ queryKey: KEY.event(data.id) });\n },\n });\n};\n\nexport const useComTransitionEvent = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; status: ComEventStatus }) => {\n const result = await comQuery<{ comTransitionEvent: ComEvent }>(\n `mutation ComTransitionEvent($id: ID!, $status: ComEventStatus!) {\n comTransitionEvent(id: $id, status: $status) { ${EVENT_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.comTransitionEvent;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.events });\n qc.invalidateQueries({ queryKey: KEY.event(data.id) });\n },\n });\n};\n\nexport const useComRsvpEvent = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { eventId: string; status: ComRsvpStatus; note?: string | null }) => {\n const result = await comQuery<{ comRsvpEvent: ComEventRsvp }>(\n `mutation ComRsvpEvent($eventId: ID!, $status: ComRsvpStatus!, $note: String) {\n comRsvpEvent(eventId: $eventId, status: $status, note: $note) { ${RSVP_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.comRsvpEvent;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.events });\n qc.invalidateQueries({ queryKey: KEY.event(data.eventId) });\n },\n });\n};\n\n// ============================================================================\n// Polls / Votes\n// ============================================================================\n\nexport const useComPolls = (circleId: string | undefined, status?: ComPollStatus) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: [...KEY.polls(circleId ?? ''), status ?? 'all'],\n enabled: Boolean(circleId),\n queryFn: async () => {\n const result = await comQuery<{ comListPolls: ComPoll[] }>(\n `query ComListPolls($circleId: ID!, $status: ComPollStatus) {\n comListPolls(circleId: $circleId, status: $status) { ${POLL_FIELDS} }\n }`,\n { circleId, status },\n apiGatewayUrl\n );\n return result.comListPolls;\n },\n });\n};\n\nexport const useComCreatePoll = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComCreatePollInput) => {\n const result = await comQuery<{ comCreatePoll: ComPoll }>(\n `mutation ComCreatePoll($input: ComCreatePollInput!) {\n comCreatePoll(input: $input) { ${POLL_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.comCreatePoll;\n },\n onSuccess: (data) => qc.invalidateQueries({ queryKey: KEY.polls(data.circleId) }),\n });\n};\n\nexport const useComClosePoll = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await comQuery<{ comClosePoll: ComPoll }>(\n `mutation ComClosePoll($id: ID!) { comClosePoll(id: $id) { ${POLL_FIELDS} } }`,\n { id },\n apiGatewayUrl\n );\n return result.comClosePoll;\n },\n onSuccess: (data) => qc.invalidateQueries({ queryKey: KEY.polls(data.circleId) }),\n });\n};\n\nexport const useComVotePoll = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { pollId: string; optionId: string; circleId?: string }) => {\n const result = await comQuery<{ comVotePoll: ComPollOption }>(\n `mutation ComVotePoll($pollId: ID!, $optionId: ID!) {\n comVotePoll(pollId: $pollId, optionId: $optionId) { ${POLL_OPTION_FIELDS} }\n }`,\n { pollId: vars.pollId, optionId: vars.optionId },\n apiGatewayUrl\n );\n return result.comVotePoll;\n },\n onSuccess: (_data, vars) => {\n if (vars.circleId) qc.invalidateQueries({ queryKey: KEY.polls(vars.circleId) });\n qc.invalidateQueries({ queryKey: KEY.poll(vars.pollId) });\n },\n });\n};\n\n// ============================================================================\n// Profiles (social identity layer)\n// ============================================================================\n\nexport interface ProfileFilters {\n discoverable?: boolean;\n pagination?: PaginationInput;\n}\n\nexport const useComProfiles = (filters: ProfileFilters = {}) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: [...KEY.profiles, filters],\n queryFn: async () => {\n const result = await comQuery<{ comListProfiles: ComConnection<ComProfile> }>(\n `query ComListProfiles($discoverable: Boolean, $pagination: PaginationInput) {\n comListProfiles(discoverable: $discoverable, pagination: $pagination) {\n items { ${PROFILE_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { discoverable: filters.discoverable, pagination: page(filters.pagination) },\n apiGatewayUrl\n );\n return result.comListProfiles;\n },\n });\n};\n\nexport const useComProfile = (memberId: string | undefined) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: KEY.profile(memberId ?? ''),\n enabled: Boolean(memberId),\n // Profiles change rarely; cache aggressively so feed identity lookups are cheap.\n staleTime: 10 * 60 * 1000,\n queryFn: async () => {\n const result = await comQuery<{ comProfile: ComProfile | null }>(\n `query ComProfile($memberId: ID!) {\n comProfile(memberId: $memberId) { ${PROFILE_FIELDS} }\n }`,\n { memberId },\n apiGatewayUrl\n );\n return result.comProfile;\n },\n });\n};\n\nexport const useComUpsertProfile = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComUpsertProfileInput) => {\n const result = await comQuery<{ comUpsertProfile: ComProfile }>(\n `mutation ComUpsertProfile($input: ComUpsertProfileInput!) {\n comUpsertProfile(input: $input) { ${PROFILE_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.comUpsertProfile;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.profiles });\n qc.invalidateQueries({ queryKey: KEY.profile(data.memberId) });\n },\n });\n};\n\n// Re-export commonly used reaction type for convenience.\nexport type { ComReaction };\n"],"mappings":"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"}
1
+ {"version":3,"file":"useComradeCircleApi.js","names":[],"sources":["../../../src/comradecircle/hooks/useComradeCircleApi.ts"],"sourcesContent":["/**\n * ComradeCircle API hooks\n * @module comradecircle/hooks\n *\n * React-Query hooks wired DIRECTLY to the real `wspace-comradecircle-svc`\n * federated GraphQL contract (the `com*` operations). Every query/mutation\n * below uses an inline gql string whose fields match the svc\n * `.hive-schema.graphql` exactly. No mock data.\n *\n * Transport: `graphqlFetch({ gateway: 'workspace', workspaceToken: true })` —\n * the host shell supplies the gateway base URL + a workspace-scoped token.\n *\n * IMPORTANT: `User`/`File` are id-only federation stubs in this subgraph. We\n * never select `author { firstName }` or `avatar { url }`. Display identity is\n * resolved through `ComProfile` (see `useComProfile`).\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\nimport { useComradeCircle } from '../context';\nimport { clearGraphqlResponseCache } from '../lib/graphql-cache';\nimport type {\n ComConnection,\n ComCircle,\n ComCircleStatus,\n ComCircleVisibility,\n ComChannel,\n ComComment,\n ComPost,\n ComPostStatus,\n ComPoll,\n ComPollStatus,\n ComPollOption,\n ComEvent,\n ComEventStatus,\n ComEventRsvp,\n ComRsvpStatus,\n ComProfile,\n ComMembership,\n ComMembershipStatus,\n ComMemberRole,\n ComFeedFollow,\n ComFollowTargetType,\n ComReaction,\n ComReactionType,\n ComCreateCircleInput,\n ComUpdateCircleInput,\n ComCreateChannelInput,\n ComUpdateChannelInput,\n ComCreatePostInput,\n ComUpdatePostInput,\n ComCreateCommentInput,\n ComCreateEventInput,\n ComUpdateEventInput,\n ComCreatePollInput,\n ComFollowInput,\n ComUpsertProfileInput,\n PaginationInput,\n} from '../types';\n\n// ============================================================================\n// Transport\n// ============================================================================\n\nconst isMutation = (query: string): boolean => /^\\s*mutation\\b/.test(query);\n\nconst comQuery = async <TData>(\n query: string,\n variables: Record<string, unknown>,\n baseUrl?: string\n): Promise<TData> => {\n // A mutation's own request must never be served from (or coalesced onto) the\n // read cache, and the resolved read cache must be dropped once it commits so\n // the React-Query refetch that follows sees the new state — not a stale\n // pre-write response held for up to 15s. Clear before AND after the write.\n const mutating = isMutation(query);\n if (mutating) clearGraphqlResponseCache();\n\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl,\n query,\n variables,\n workspaceToken: true,\n });\n\n if (mutating) clearGraphqlResponseCache();\n\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n return response.data;\n};\n\n// React-Query key roots (one namespace per domain entity).\nconst KEY = {\n circles: ['comradecircle', 'circles'] as const,\n circle: (id: string) => ['comradecircle', 'circle', id] as const,\n channels: (circleId: string) => ['comradecircle', 'channels', circleId] as const,\n posts: (channelId: string) => ['comradecircle', 'posts', channelId] as const,\n post: (id: string) => ['comradecircle', 'post', id] as const,\n comments: (postId: string) => ['comradecircle', 'comments', postId] as const,\n feed: ['comradecircle', 'feed'] as const,\n follows: ['comradecircle', 'follows'] as const,\n events: ['comradecircle', 'events'] as const,\n event: (id: string) => ['comradecircle', 'event', id] as const,\n polls: (circleId: string) => ['comradecircle', 'polls', circleId] as const,\n poll: (id: string) => ['comradecircle', 'poll', id] as const,\n profiles: ['comradecircle', 'profiles'] as const,\n profile: (memberId: string) => ['comradecircle', 'profile', memberId] as const,\n memberships: ['comradecircle', 'memberships'] as const,\n};\n\nconst DEFAULT_PAGE: PaginationInput = { limit: 50 };\n\n/** Drop `cursor` when it is nullish so the svc never sees an explicit null. */\nconst page = (p?: PaginationInput): PaginationInput => {\n const base = p ?? DEFAULT_PAGE;\n const result: PaginationInput = { limit: base.limit ?? DEFAULT_PAGE.limit };\n if (base.offset != null) result.offset = base.offset;\n if (base.cursor != null) result.cursor = base.cursor;\n return result;\n};\n\n// ============================================================================\n// Reusable gql field selections (codegen-free strictness)\n// ============================================================================\n\nconst CHANNEL_FIELDS = `\n id workspaceId circleId name slug description type visibility position isArchived\n createdAt updatedAt\n`;\n\nconst CIRCLE_FIELDS = `\n id workspaceId name slug description topic tags status visibility memberCount\n avatar { id } createdAt updatedAt\n`;\n\nconst REACTION_FIELDS = `id postId memberId type createdAt`;\n\nconst COMMENT_FIELDS = `\n id workspaceId postId parentId authorId body isHidden createdAt updatedAt\n`;\n\nconst POST_FIELDS = `\n id workspaceId channelId authorId body status tags attachmentFileIds isPinned\n reactionCount commentCount reactions { ${REACTION_FIELDS} } createdAt updatedAt\n`;\n\nconst POLL_OPTION_FIELDS = `id workspaceId pollId label position voteCount createdAt`;\n\nconst POLL_FIELDS = `\n id workspaceId circleId question allowMultiple isAnonymous status closesAt\n options { ${POLL_OPTION_FIELDS} } createdAt updatedAt\n`;\n\nconst RSVP_FIELDS = `\n id workspaceId eventId memberId status note createdAt updatedAt\n`;\n\nconst EVENT_FIELDS = `\n id workspaceId circleId hostId title description mode status location joinUrl\n capacity startsAt endsAt tags rsvpCount createdAt updatedAt\n`;\n\nconst PROFILE_FIELDS = `\n id workspaceId memberId displayName headline bio avatar { id } interests skills\n presence discoverable createdAt updatedAt\n`;\n\nconst MEMBERSHIP_FIELDS = `\n id workspaceId circleId memberId role status joinedAt createdAt updatedAt\n`;\n\nconst FOLLOW_FIELDS = `\n id workspaceId followerId targetId targetType muted createdAt updatedAt\n`;\n\n// ============================================================================\n// Circles\n// ============================================================================\n\nexport interface CircleFilters {\n status?: ComCircleStatus;\n visibility?: ComCircleVisibility;\n pagination?: PaginationInput;\n}\n\nexport const useComCircles = (filters: CircleFilters = {}) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: [...KEY.circles, filters],\n queryFn: async () => {\n const result = await comQuery<{ comListCircles: ComConnection<ComCircle> }>(\n `query ComListCircles($status: ComCircleStatus, $visibility: ComCircleVisibility, $pagination: PaginationInput) {\n comListCircles(status: $status, visibility: $visibility, pagination: $pagination) {\n items { ${CIRCLE_FIELDS} channels { id } }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n status: filters.status,\n visibility: filters.visibility,\n pagination: page(filters.pagination),\n },\n apiGatewayUrl\n );\n return result.comListCircles;\n },\n });\n};\n\nexport const useComCircle = (id: string | undefined) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: KEY.circle(id ?? ''),\n enabled: Boolean(id),\n queryFn: async () => {\n const result = await comQuery<{ comCircle: ComCircle | null }>(\n `query ComCircle($id: ID!) {\n comCircle(id: $id) {\n ${CIRCLE_FIELDS}\n channels { ${CHANNEL_FIELDS} }\n memberships { ${MEMBERSHIP_FIELDS} }\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.comCircle;\n },\n });\n};\n\nexport const useComCreateCircle = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComCreateCircleInput) => {\n const result = await comQuery<{ comCreateCircle: ComCircle }>(\n `mutation ComCreateCircle($input: ComCreateCircleInput!) {\n comCreateCircle(input: $input) { ${CIRCLE_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.comCreateCircle;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.circles }),\n });\n};\n\nexport const useComUpdateCircle = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; input: ComUpdateCircleInput }) => {\n const result = await comQuery<{ comUpdateCircle: ComCircle }>(\n `mutation ComUpdateCircle($id: ID!, $input: ComUpdateCircleInput!) {\n comUpdateCircle(id: $id, input: $input) { ${CIRCLE_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.comUpdateCircle;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.circles });\n qc.invalidateQueries({ queryKey: KEY.circle(data.id) });\n },\n });\n};\n\nexport const useComArchiveCircle = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await comQuery<{ comArchiveCircle: boolean }>(\n `mutation ComArchiveCircle($id: ID!) { comArchiveCircle(id: $id) }`,\n { id },\n apiGatewayUrl\n );\n return result.comArchiveCircle;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.circles }),\n });\n};\n\nexport const useComJoinCircle = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (circleId: string) => {\n const result = await comQuery<{ comJoinCircle: ComMembership }>(\n `mutation ComJoinCircle($circleId: ID!) {\n comJoinCircle(circleId: $circleId) { ${MEMBERSHIP_FIELDS} }\n }`,\n { circleId },\n apiGatewayUrl\n );\n return result.comJoinCircle;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.circles });\n qc.invalidateQueries({ queryKey: KEY.circle(data.circleId) });\n qc.invalidateQueries({ queryKey: KEY.memberships });\n },\n });\n};\n\nexport const useComLeaveCircle = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (circleId: string) => {\n const result = await comQuery<{ comLeaveCircle: boolean }>(\n `mutation ComLeaveCircle($circleId: ID!) { comLeaveCircle(circleId: $circleId) }`,\n { circleId },\n apiGatewayUrl\n );\n return result.comLeaveCircle;\n },\n onSuccess: (_data, circleId) => {\n qc.invalidateQueries({ queryKey: KEY.circles });\n qc.invalidateQueries({ queryKey: KEY.circle(circleId) });\n qc.invalidateQueries({ queryKey: KEY.memberships });\n },\n });\n};\n\nexport const useComSetMemberRole = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { membershipId: string; role: ComMemberRole; circleId?: string }) => {\n const result = await comQuery<{ comSetMemberRole: ComMembership }>(\n `mutation ComSetMemberRole($membershipId: ID!, $role: ComMemberRole!) {\n comSetMemberRole(membershipId: $membershipId, role: $role) { ${MEMBERSHIP_FIELDS} }\n }`,\n { membershipId: vars.membershipId, role: vars.role },\n apiGatewayUrl\n );\n return result.comSetMemberRole;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.circle(data.circleId) });\n qc.invalidateQueries({ queryKey: KEY.memberships });\n },\n });\n};\n\nexport interface MembershipFilters {\n circleId?: string;\n status?: ComMembershipStatus;\n pagination?: PaginationInput;\n}\n\nexport const useComMemberships = (filters: MembershipFilters = {}) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: [...KEY.memberships, filters],\n queryFn: async () => {\n const result = await comQuery<{ comListMemberships: ComConnection<ComMembership> }>(\n `query ComListMemberships($circleId: ID, $status: ComMembershipStatus, $pagination: PaginationInput) {\n comListMemberships(circleId: $circleId, status: $status, pagination: $pagination) {\n items { ${MEMBERSHIP_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n circleId: filters.circleId,\n status: filters.status,\n pagination: page(filters.pagination),\n },\n apiGatewayUrl\n );\n return result.comListMemberships;\n },\n });\n};\n\n// ============================================================================\n// Channels\n// ============================================================================\n\nexport const useComChannels = (circleId: string | undefined) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: KEY.channels(circleId ?? ''),\n enabled: Boolean(circleId),\n queryFn: async () => {\n const result = await comQuery<{ comListChannels: ComChannel[] }>(\n `query ComListChannels($circleId: ID!) {\n comListChannels(circleId: $circleId) { ${CHANNEL_FIELDS} }\n }`,\n { circleId },\n apiGatewayUrl\n );\n return result.comListChannels;\n },\n });\n};\n\nexport const useComCreateChannel = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComCreateChannelInput) => {\n const result = await comQuery<{ comCreateChannel: ComChannel }>(\n `mutation ComCreateChannel($input: ComCreateChannelInput!) {\n comCreateChannel(input: $input) { ${CHANNEL_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.comCreateChannel;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.channels(data.circleId) });\n qc.invalidateQueries({ queryKey: KEY.circle(data.circleId) });\n },\n });\n};\n\nexport const useComUpdateChannel = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; input: ComUpdateChannelInput; circleId: string }) => {\n const result = await comQuery<{ comUpdateChannel: ComChannel }>(\n `mutation ComUpdateChannel($id: ID!, $input: ComUpdateChannelInput!) {\n comUpdateChannel(id: $id, input: $input) { ${CHANNEL_FIELDS} }\n }`,\n { id: vars.id, input: vars.input },\n apiGatewayUrl\n );\n return result.comUpdateChannel;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.channels(data.circleId) });\n qc.invalidateQueries({ queryKey: KEY.circle(data.circleId) });\n },\n });\n};\n\n// ============================================================================\n// Posts / Reactions / Comments\n// ============================================================================\n\nexport interface PostFilters {\n channelId?: string;\n status?: ComPostStatus;\n pagination?: PaginationInput;\n}\n\nexport const useComPosts = (filters: PostFilters = {}) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: [...KEY.posts(filters.channelId ?? 'all'), filters.status, filters.pagination],\n enabled: Boolean(filters.channelId),\n queryFn: async () => {\n const result = await comQuery<{ comListPosts: ComConnection<ComPost> }>(\n `query ComListPosts($channelId: ID, $status: ComPostStatus, $pagination: PaginationInput) {\n comListPosts(channelId: $channelId, status: $status, pagination: $pagination) {\n items { ${POST_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n channelId: filters.channelId,\n status: filters.status,\n pagination: page(filters.pagination),\n },\n apiGatewayUrl\n );\n return result.comListPosts;\n },\n });\n};\n\nexport const useComPost = (id: string | undefined) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: KEY.post(id ?? ''),\n enabled: Boolean(id),\n queryFn: async () => {\n const result = await comQuery<{ comPost: ComPost | null }>(\n `query ComPost($id: ID!) {\n comPost(id: $id) {\n ${POST_FIELDS}\n comments { ${COMMENT_FIELDS} }\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.comPost;\n },\n });\n};\n\nexport const useComMyFeed = (pagination?: PaginationInput) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: [...KEY.feed, pagination],\n queryFn: async () => {\n const result = await comQuery<{ comMyFeed: ComConnection<ComPost> }>(\n `query ComMyFeed($pagination: PaginationInput) {\n comMyFeed(pagination: $pagination) {\n items { ${POST_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { pagination: page(pagination) },\n apiGatewayUrl\n );\n return result.comMyFeed;\n },\n });\n};\n\nexport const useComCreatePost = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComCreatePostInput) => {\n const result = await comQuery<{ comCreatePost: ComPost }>(\n `mutation ComCreatePost($input: ComCreatePostInput!) {\n comCreatePost(input: $input) { ${POST_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.comCreatePost;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.posts(data.channelId) });\n qc.invalidateQueries({ queryKey: KEY.feed });\n },\n });\n};\n\nexport const useComUpdatePost = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; input: ComUpdatePostInput }) => {\n const result = await comQuery<{ comUpdatePost: ComPost }>(\n `mutation ComUpdatePost($id: ID!, $input: ComUpdatePostInput!) {\n comUpdatePost(id: $id, input: $input) { ${POST_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.comUpdatePost;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.posts(data.channelId) });\n qc.invalidateQueries({ queryKey: KEY.post(data.id) });\n qc.invalidateQueries({ queryKey: KEY.feed });\n },\n });\n};\n\nexport const useComRemovePost = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; channelId?: string }) => {\n const result = await comQuery<{ comRemovePost: boolean }>(\n `mutation ComRemovePost($id: ID!) { comRemovePost(id: $id) }`,\n { id: vars.id },\n apiGatewayUrl\n );\n return result.comRemovePost;\n },\n onSuccess: (_d, vars) => {\n if (vars.channelId) qc.invalidateQueries({ queryKey: KEY.posts(vars.channelId) });\n qc.invalidateQueries({ queryKey: KEY.feed });\n },\n });\n};\n\nexport const useComReactToPost = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { postId: string; type: ComReactionType }) => {\n const result = await comQuery<{ comReactToPost: ComPost }>(\n `mutation ComReactToPost($postId: ID!, $type: ComReactionType!) {\n comReactToPost(postId: $postId, type: $type) { ${POST_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.comReactToPost;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.posts(data.channelId) });\n qc.invalidateQueries({ queryKey: KEY.post(data.id) });\n qc.invalidateQueries({ queryKey: KEY.feed });\n },\n });\n};\n\nexport const useComComments = (postId: string | undefined) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: KEY.comments(postId ?? ''),\n enabled: Boolean(postId),\n queryFn: async () => {\n const result = await comQuery<{ comListComments: ComComment[] }>(\n `query ComListComments($postId: ID!) {\n comListComments(postId: $postId) { ${COMMENT_FIELDS} }\n }`,\n { postId },\n apiGatewayUrl\n );\n return result.comListComments;\n },\n });\n};\n\nexport const useComAddComment = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComCreateCommentInput) => {\n const result = await comQuery<{ comAddComment: ComComment }>(\n `mutation ComAddComment($input: ComCreateCommentInput!) {\n comAddComment(input: $input) { ${COMMENT_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.comAddComment;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.comments(data.postId) });\n qc.invalidateQueries({ queryKey: KEY.post(data.postId) });\n qc.invalidateQueries({ queryKey: KEY.feed });\n },\n });\n};\n\n// ============================================================================\n// Feed follows\n// ============================================================================\n\nexport const useComMyFollows = (targetType?: ComFollowTargetType) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: [...KEY.follows, targetType ?? 'all'],\n queryFn: async () => {\n const result = await comQuery<{ comMyFollows: ComFeedFollow[] }>(\n `query ComMyFollows($targetType: ComFollowTargetType) {\n comMyFollows(targetType: $targetType) { ${FOLLOW_FIELDS} }\n }`,\n { targetType },\n apiGatewayUrl\n );\n return result.comMyFollows;\n },\n });\n};\n\nexport const useComFollow = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComFollowInput) => {\n const result = await comQuery<{ comFollow: ComFeedFollow }>(\n `mutation ComFollow($input: ComFollowInput!) {\n comFollow(input: $input) { ${FOLLOW_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.comFollow;\n },\n onSuccess: () => {\n qc.invalidateQueries({ queryKey: KEY.follows });\n qc.invalidateQueries({ queryKey: KEY.feed });\n },\n });\n};\n\nexport const useComUnfollow = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComFollowInput) => {\n const result = await comQuery<{ comUnfollow: boolean }>(\n `mutation ComUnfollow($input: ComFollowInput!) { comUnfollow(input: $input) }`,\n { input },\n apiGatewayUrl\n );\n return result.comUnfollow;\n },\n onSuccess: () => {\n qc.invalidateQueries({ queryKey: KEY.follows });\n qc.invalidateQueries({ queryKey: KEY.feed });\n },\n });\n};\n\nexport const useComMuteFollow = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; muted: boolean }) => {\n const result = await comQuery<{ comMuteFollow: ComFeedFollow }>(\n `mutation ComMuteFollow($id: ID!, $muted: Boolean!) {\n comMuteFollow(id: $id, muted: $muted) { ${FOLLOW_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.comMuteFollow;\n },\n onSuccess: () => {\n qc.invalidateQueries({ queryKey: KEY.follows });\n qc.invalidateQueries({ queryKey: KEY.feed });\n },\n });\n};\n\n// ============================================================================\n// Events / RSVPs\n// ============================================================================\n\nexport interface EventFilters {\n circleId?: string;\n status?: ComEventStatus;\n pagination?: PaginationInput;\n}\n\nexport const useComEvents = (filters: EventFilters = {}) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: [...KEY.events, filters],\n queryFn: async () => {\n const result = await comQuery<{ comListEvents: ComConnection<ComEvent> }>(\n `query ComListEvents($circleId: ID, $status: ComEventStatus, $pagination: PaginationInput) {\n comListEvents(circleId: $circleId, status: $status, pagination: $pagination) {\n items { ${EVENT_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n circleId: filters.circleId,\n status: filters.status,\n pagination: page(filters.pagination),\n },\n apiGatewayUrl\n );\n return result.comListEvents;\n },\n });\n};\n\nexport const useComEvent = (id: string | undefined) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: KEY.event(id ?? ''),\n enabled: Boolean(id),\n queryFn: async () => {\n const result = await comQuery<{ comEvent: ComEvent | null }>(\n `query ComEvent($id: ID!) {\n comEvent(id: $id) {\n ${EVENT_FIELDS}\n rsvps { ${RSVP_FIELDS} }\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.comEvent;\n },\n });\n};\n\nexport const useComCreateEvent = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComCreateEventInput) => {\n const result = await comQuery<{ comCreateEvent: ComEvent }>(\n `mutation ComCreateEvent($input: ComCreateEventInput!) {\n comCreateEvent(input: $input) { ${EVENT_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.comCreateEvent;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.events }),\n });\n};\n\nexport const useComUpdateEvent = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; input: ComUpdateEventInput }) => {\n const result = await comQuery<{ comUpdateEvent: ComEvent }>(\n `mutation ComUpdateEvent($id: ID!, $input: ComUpdateEventInput!) {\n comUpdateEvent(id: $id, input: $input) { ${EVENT_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.comUpdateEvent;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.events });\n qc.invalidateQueries({ queryKey: KEY.event(data.id) });\n },\n });\n};\n\nexport const useComTransitionEvent = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { id: string; status: ComEventStatus }) => {\n const result = await comQuery<{ comTransitionEvent: ComEvent }>(\n `mutation ComTransitionEvent($id: ID!, $status: ComEventStatus!) {\n comTransitionEvent(id: $id, status: $status) { ${EVENT_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.comTransitionEvent;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.events });\n qc.invalidateQueries({ queryKey: KEY.event(data.id) });\n },\n });\n};\n\nexport const useComRsvpEvent = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { eventId: string; status: ComRsvpStatus; note?: string | null }) => {\n const result = await comQuery<{ comRsvpEvent: ComEventRsvp }>(\n `mutation ComRsvpEvent($eventId: ID!, $status: ComRsvpStatus!, $note: String) {\n comRsvpEvent(eventId: $eventId, status: $status, note: $note) { ${RSVP_FIELDS} }\n }`,\n vars,\n apiGatewayUrl\n );\n return result.comRsvpEvent;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.events });\n qc.invalidateQueries({ queryKey: KEY.event(data.eventId) });\n },\n });\n};\n\n// ============================================================================\n// Polls / Votes\n// ============================================================================\n\nexport const useComPolls = (circleId: string | undefined, status?: ComPollStatus) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: [...KEY.polls(circleId ?? ''), status ?? 'all'],\n enabled: Boolean(circleId),\n queryFn: async () => {\n const result = await comQuery<{ comListPolls: ComPoll[] }>(\n `query ComListPolls($circleId: ID!, $status: ComPollStatus) {\n comListPolls(circleId: $circleId, status: $status) { ${POLL_FIELDS} }\n }`,\n { circleId, status },\n apiGatewayUrl\n );\n return result.comListPolls;\n },\n });\n};\n\nexport const useComCreatePoll = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComCreatePollInput) => {\n const result = await comQuery<{ comCreatePoll: ComPoll }>(\n `mutation ComCreatePoll($input: ComCreatePollInput!) {\n comCreatePoll(input: $input) { ${POLL_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.comCreatePoll;\n },\n onSuccess: (data) => qc.invalidateQueries({ queryKey: KEY.polls(data.circleId) }),\n });\n};\n\nexport const useComClosePoll = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await comQuery<{ comClosePoll: ComPoll }>(\n `mutation ComClosePoll($id: ID!) { comClosePoll(id: $id) { ${POLL_FIELDS} } }`,\n { id },\n apiGatewayUrl\n );\n return result.comClosePoll;\n },\n onSuccess: (data) => qc.invalidateQueries({ queryKey: KEY.polls(data.circleId) }),\n });\n};\n\nexport const useComVotePoll = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (vars: { pollId: string; optionId: string; circleId?: string }) => {\n const result = await comQuery<{ comVotePoll: ComPollOption }>(\n `mutation ComVotePoll($pollId: ID!, $optionId: ID!) {\n comVotePoll(pollId: $pollId, optionId: $optionId) { ${POLL_OPTION_FIELDS} }\n }`,\n { pollId: vars.pollId, optionId: vars.optionId },\n apiGatewayUrl\n );\n return result.comVotePoll;\n },\n onSuccess: (_data, vars) => {\n if (vars.circleId) qc.invalidateQueries({ queryKey: KEY.polls(vars.circleId) });\n qc.invalidateQueries({ queryKey: KEY.poll(vars.pollId) });\n },\n });\n};\n\n// ============================================================================\n// Profiles (social identity layer)\n// ============================================================================\n\nexport interface ProfileFilters {\n discoverable?: boolean;\n pagination?: PaginationInput;\n}\n\nexport const useComProfiles = (filters: ProfileFilters = {}) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: [...KEY.profiles, filters],\n queryFn: async () => {\n const result = await comQuery<{ comListProfiles: ComConnection<ComProfile> }>(\n `query ComListProfiles($discoverable: Boolean, $pagination: PaginationInput) {\n comListProfiles(discoverable: $discoverable, pagination: $pagination) {\n items { ${PROFILE_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { discoverable: filters.discoverable, pagination: page(filters.pagination) },\n apiGatewayUrl\n );\n return result.comListProfiles;\n },\n });\n};\n\nexport const useComProfile = (memberId: string | undefined) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: KEY.profile(memberId ?? ''),\n enabled: Boolean(memberId),\n // Profiles change rarely; cache aggressively so feed identity lookups are cheap.\n staleTime: 10 * 60 * 1000,\n queryFn: async () => {\n const result = await comQuery<{ comProfile: ComProfile | null }>(\n `query ComProfile($memberId: ID!) {\n comProfile(memberId: $memberId) { ${PROFILE_FIELDS} }\n }`,\n { memberId },\n apiGatewayUrl\n );\n return result.comProfile;\n },\n });\n};\n\nexport const useComUpsertProfile = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: ComUpsertProfileInput) => {\n const result = await comQuery<{ comUpsertProfile: ComProfile }>(\n `mutation ComUpsertProfile($input: ComUpsertProfileInput!) {\n comUpsertProfile(input: $input) { ${PROFILE_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.comUpsertProfile;\n },\n onSuccess: (data) => {\n qc.invalidateQueries({ queryKey: KEY.profiles });\n qc.invalidateQueries({ queryKey: KEY.profile(data.memberId) });\n },\n });\n};\n\n// Re-export commonly used reaction type for convenience.\nexport type { ComReaction };\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useCursorPager.js","names":[],"sources":["../../../src/comradecircle/hooks/useCursorPager.ts"],"sourcesContent":["/**\n * useCursorPager — forward/back cursor pagination for Com* list connections.\n * @module comradecircle/hooks\n *\n * The svc paginates by cursor only (`PaginationInput.offset` is a no-op — the\n * resolver never sets Prisma `skip` from it). So a page must send\n * `pagination.cursor = the previous page's pageInfo.cursor` to advance. This\n * hook keeps a stack of the cursors that opened each visited page so Prev/Next\n * are exact, and exposes a 0-indexed `page` for display.\n */\n\nimport { useCallback, useState } from 'react';\n\nexport interface CursorPager {\n /** 0-indexed page number for display. */\n page: number;\n /**\n * Cursor that opens the current page — `undefined` on the first page so it is\n * OMITTED from the GraphQL variables (the svc rejects an explicit `cursor: null`;\n * a first page must send no cursor at all, per cursor-pagination convention).\n */\n cursor: string | undefined;\n /** Advance to the next page using the current page's end cursor. No-op if null. */\n next: (endCursor: string | null | undefined) => void;\n /** Go back one page. No-op on the first page. */\n prev: () => void;\n /** Reset to the first page (call when filters change). */\n reset: () => void;\n /**\n * Adapter for `<Pagination onPageChange>` — compares the requested page to the\n * current one and calls next()/prev() accordingly.\n */\n onPageChange: (targetPage: number, endCursor: string | null | undefined) => void;\n}\n\nexport function useCursorPager(): CursorPager {\n const [stack, setStack] = useState<(string | null)[]>([null]);\n const page = stack.length - 1;\n const cursor = stack[page] ?? undefined;\n\n const next = useCallback((endCursor: string | null | undefined) => {\n if (endCursor) setStack((s) => [...s, endCursor]);\n }, []);\n\n const prev = useCallback(() => {\n setStack((s) => (s.length > 1 ? s.slice(0, -1) : s));\n }, []);\n\n const reset = useCallback(() => {\n setStack([null]);\n }, []);\n\n const onPageChange = useCallback((targetPage: number, endCursor: string | null | undefined) => {\n setStack((s) => {\n const current = s.length - 1;\n if (targetPage > current) {\n return endCursor ? [...s, endCursor] : s;\n }\n return s.length > 1 ? s.slice(0, -1) : s;\n });\n }, []);\n\n return { page, cursor, next, prev, reset, onPageChange };\n}\n"],"mappings":";;AAmCA,SAAgB,IAA8B;CAC5C,IAAM,CAAC,GAAO,KAAY,EAA4B,CAAC,KAAK,CAAC,EACvD,IAAO,EAAM,SAAS;AAyB5B,QAAO;EAAE;EAAM,QAxBA,EAAM,MAAS,KAAA;EAwBP,MAtBV,GAAa,MAAyC;AACjE,GAAI,KAAW,GAAU,MAAM,CAAC,GAAG,GAAG,EAAU,CAAC;KAChD,EAAE,CAAC;EAoBuB,MAlBhB,QAAkB;AAC7B,MAAU,MAAO,EAAE,SAAS,IAAI,EAAE,MAAM,GAAG,GAAG,GAAG,EAAG;KACnD,EAAE,CAAC;EAgB6B,OAdrB,QAAkB;AAC9B,KAAS,CAAC,KAAK,CAAC;KACf,EAAE,CAAC;EAYoC,cAVrB,GAAa,GAAoB,MAAyC;AAC7F,MAAU,MAEJ,IADY,EAAE,SAAS,IAElB,IAAY,CAAC,GAAG,GAAG,EAAU,GAAG,IAElC,EAAE,SAAS,IAAI,EAAE,MAAM,GAAG,GAAG,GAAG,EACvC;KACD,EAAE,CAAC;EAEkD"}
1
+ {"version":3,"file":"useCursorPager.js","names":[],"sources":["../../../src/comradecircle/hooks/useCursorPager.ts"],"sourcesContent":["/**\n * useCursorPager — forward/back cursor pagination for Com* list connections.\n * @module comradecircle/hooks\n *\n * The svc paginates by cursor only (`PaginationInput.offset` is a no-op — the\n * resolver never sets Prisma `skip` from it). So a page must send\n * `pagination.cursor = the previous page's pageInfo.cursor` to advance. This\n * hook keeps a stack of the cursors that opened each visited page so Prev/Next\n * are exact, and exposes a 0-indexed `page` for display.\n */\n\nimport { useCallback, useState } from 'react';\n\nexport interface CursorPager {\n /** 0-indexed page number for display. */\n page: number;\n /**\n * Cursor that opens the current page — `undefined` on the first page so it is\n * OMITTED from the GraphQL variables (the svc rejects an explicit `cursor: null`;\n * a first page must send no cursor at all, per cursor-pagination convention).\n */\n cursor: string | undefined;\n /** Advance to the next page using the current page's end cursor. No-op if null. */\n next: (endCursor: string | null | undefined) => void;\n /** Go back one page. No-op on the first page. */\n prev: () => void;\n /** Reset to the first page (call when filters change). */\n reset: () => void;\n /**\n * Adapter for `<Pagination onPageChange>` — compares the requested page to the\n * current one and calls next()/prev() accordingly.\n */\n onPageChange: (targetPage: number, endCursor: string | null | undefined) => void;\n}\n\nexport function useCursorPager(): CursorPager {\n const [stack, setStack] = useState<(string | null)[]>([null]);\n const page = stack.length - 1;\n const cursor = stack[page] ?? undefined;\n\n const next = useCallback((endCursor: string | null | undefined) => {\n if (endCursor) setStack((s) => [...s, endCursor]);\n }, []);\n\n const prev = useCallback(() => {\n setStack((s) => (s.length > 1 ? s.slice(0, -1) : s));\n }, []);\n\n const reset = useCallback(() => {\n setStack([null]);\n }, []);\n\n const onPageChange = useCallback((targetPage: number, endCursor: string | null | undefined) => {\n setStack((s) => {\n const current = s.length - 1;\n if (targetPage > current) {\n return endCursor ? [...s, endCursor] : s;\n }\n return s.length > 1 ? s.slice(0, -1) : s;\n });\n }, []);\n\n return { page, cursor, next, prev, reset, onPageChange };\n}\n"],"mappings":";;AAmCA,SAAgB,IAA8B;CAC5C,IAAM,CAAC,GAAO,KAAY,EAA4B,CAAC,IAAI,CAAC,GACtD,IAAO,EAAM,SAAS;CAyB5B,OAAO;EAAE;EAAM,QAxBA,EAAM,MAAS,KAAA;EAwBP,MAtBV,GAAa,MAAyC;GACjE,AAAI,KAAW,GAAU,MAAM,CAAC,GAAG,GAAG,CAAS,CAAC;EAClD,GAAG,CAAC,CAoBmB;EAAM,MAlBhB,QAAkB;GAC7B,GAAU,MAAO,EAAE,SAAS,IAAI,EAAE,MAAM,GAAG,EAAE,IAAI,CAAE;EACrD,GAAG,CAAC,CAgByB;EAAM,OAdrB,QAAkB;GAC9B,EAAS,CAAC,IAAI,CAAC;EACjB,GAAG,CAAC,CAY+B;EAAO,cAVrB,GAAa,GAAoB,MAAyC;GAC7F,GAAU,MAEJ,IADY,EAAE,SAAS,IAElB,IAAY,CAAC,GAAG,GAAG,CAAS,IAAI,IAElC,EAAE,SAAS,IAAI,EAAE,MAAM,GAAG,EAAE,IAAI,CACxC;EACH,GAAG,CAAC,CAEsC;CAAa;AACzD"}