@burdenoff/microfe-comradecircle 2026.716.1 → 2026.720.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/comradecircle/ComradeCircleRoot.js.map +1 -1
- package/dist/comradecircle/ComradeCircleRoutes.js.map +1 -1
- package/dist/comradecircle/components/Avatar.js.map +1 -1
- package/dist/comradecircle/components/AvatarUploadField.js.map +1 -1
- package/dist/comradecircle/components/CircleCard.js.map +1 -1
- package/dist/comradecircle/components/CommentThread.js.map +1 -1
- package/dist/comradecircle/components/CreateCircleDialog.js.map +1 -1
- package/dist/comradecircle/components/CreateEventDialog.js.map +1 -1
- package/dist/comradecircle/components/EmptyState.js.map +1 -1
- package/dist/comradecircle/components/EventCard.js.map +1 -1
- package/dist/comradecircle/components/FileImage.js.map +1 -1
- package/dist/comradecircle/components/FilterTabs.js.map +1 -1
- package/dist/comradecircle/components/ImageAttachments.js.map +1 -1
- package/dist/comradecircle/components/MemberIdentity.js.map +1 -1
- package/dist/comradecircle/components/MiniBar.js.map +1 -1
- package/dist/comradecircle/components/PageLayout.js.map +1 -1
- package/dist/comradecircle/components/Pagination.js.map +1 -1
- package/dist/comradecircle/components/PollCard.js.map +1 -1
- package/dist/comradecircle/components/PostCard.js.map +1 -1
- package/dist/comradecircle/components/PostComposer.js.map +1 -1
- package/dist/comradecircle/components/ReactionBar.js.map +1 -1
- package/dist/comradecircle/components/StateViews.js.map +1 -1
- package/dist/comradecircle/components/StatusBadge.js.map +1 -1
- package/dist/comradecircle/components/Tabs.js.map +1 -1
- package/dist/comradecircle/components/TagInput.js.map +1 -1
- package/dist/comradecircle/context/ComradeCircleContext.js.map +1 -1
- package/dist/comradecircle/hooks/useComradeCircleApi.d.ts.map +1 -1
- package/dist/comradecircle/hooks/useComradeCircleApi.js +119 -100
- package/dist/comradecircle/hooks/useComradeCircleApi.js.map +1 -1
- package/dist/comradecircle/hooks/useCursorPager.js.map +1 -1
- package/dist/comradecircle/hooks/useFiles.js.map +1 -1
- package/dist/comradecircle/lib/graphql-cache.js.map +1 -1
- package/dist/comradecircle/lib/toast.js.map +1 -1
- package/dist/comradecircle/pages/FeedPage.js.map +1 -1
- package/dist/comradecircle/pages/circles/CircleDetailPage.js.map +1 -1
- package/dist/comradecircle/pages/circles/CirclesPage.js.map +1 -1
- package/dist/comradecircle/pages/events/EventDetailPage.js.map +1 -1
- package/dist/comradecircle/pages/events/EventsPage.js.map +1 -1
- package/dist/comradecircle/pages/members/MemberProfilePage.js.map +1 -1
- package/dist/comradecircle/pages/members/MembersPage.js.map +1 -1
- package/dist/comradecircle/pages/profile/MyProfilePage.js.map +1 -1
- package/dist/comradecircle/utils/cn.js.map +1 -1
- package/dist/comradecircle/utils/formatters.js.map +1 -1
- package/dist/comradecircle/utils/navigation.js.map +1 -1
- package/dist/comradecircle/utils/pageContext.d.ts +29 -0
- package/dist/comradecircle/utils/pageContext.d.ts.map +1 -0
- package/dist/comradecircle/utils/pageContext.js +48 -0
- package/dist/comradecircle/utils/pageContext.js.map +1 -0
- package/dist/comradecircle/utils/slug.js.map +1 -1
- package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
- package/dist/node_modules/sonner/dist/index.js.map +1 -1
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1 -1
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
- package/dist/services/createSandboxAssistantTransport.d.ts.map +1 -1
- package/dist/services/createSandboxAssistantTransport.js +6 -4
- package/dist/services/createSandboxAssistantTransport.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MembersPage.js","names":[],"sources":["../../../../src/comradecircle/pages/members/MembersPage.tsx"],"sourcesContent":["/**\n * MembersPage — the discoverable member directory.\n * @module comradecircle/pages/members\n *\n * Reads `comListProfiles(discoverable: true)` — every member who has opted into\n * discovery via their {@link ComProfile}. Each card resolves display identity\n * from the social-identity layer (never a raw actor id), links to the member's\n * public profile, and offers a Follow/Following toggle wired to `comFollow` /\n * `comUnfollow` (targetType MEMBER). An optional client-side search narrows the\n * current page by name, headline, skills, or interests.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Search, UserPlus, UserMinus, Check, Loader2 } from 'lucide-react';\nimport { GlassCard, Button, Input, Skeleton } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { Avatar } from '../../components/Avatar';\nimport { EmptyState } from '../../components/EmptyState';\nimport { ErrorView } from '../../components/StateViews';\nimport { Pagination } from '../../components/Pagination';\nimport {\n useComProfiles,\n useComMyFollows,\n useComFollow,\n useComUnfollow,\n useCursorPager,\n} from '../../hooks';\nimport { useComradeCircle } from '../../context';\nimport { notify, describeError } from '../../lib/toast';\nimport type { ComProfile } from '../../types';\n\nconst PAGE_SIZE = 12;\n\nconst SKELETON_KEYS = ['s1', 's2', 's3', 's4', 's5', 's6', 's7', 's8'] as const;\n\n/** Card-shaped placeholder that mirrors the member card while the page loads. */\nfunction MemberCardSkeleton() {\n return (\n <GlassCard treatment=\"plain\" className=\"flex flex-col p-5\">\n <div className=\"flex items-start gap-3\">\n <Skeleton className=\"h-14 w-14 rounded-full\" />\n <div className=\"min-w-0 flex-1 space-y-2 py-1\">\n <Skeleton className=\"h-4 w-2/3\" />\n <Skeleton className=\"h-3 w-1/2\" />\n <Skeleton className=\"h-3 w-4/5\" />\n </div>\n </div>\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n <Skeleton className=\"h-5 w-16 rounded-full\" />\n <Skeleton className=\"h-5 w-20 rounded-full\" />\n </div>\n <div className=\"mt-4 flex justify-end border-t border-border-seam pt-3\">\n <Skeleton className=\"h-8 w-28 rounded-button\" />\n </div>\n </GlassCard>\n );\n}\n\nexport function MembersPage() {\n const { navigateTo, currentUser } = useComradeCircle();\n const pager = useCursorPager();\n const [search, setSearch] = useState('');\n const [pendingId, setPendingId] = useState<string | null>(null);\n\n const { data, isLoading, isError, refetch } = useComProfiles({\n discoverable: true,\n pagination: { limit: PAGE_SIZE, cursor: pager.cursor },\n });\n\n const { data: follows } = useComMyFollows('MEMBER');\n const follow = useComFollow();\n const unfollow = useComUnfollow();\n\n const followSet = useMemo(() => new Set((follows ?? []).map((f) => f.targetId)), [follows]);\n\n const profiles = useMemo(() => data?.items ?? [], [data]);\n\n const visible = useMemo(() => {\n const q = search.trim().toLowerCase();\n if (!q) return profiles;\n return profiles.filter((p) => {\n const haystack = [p.displayName, p.headline ?? '', ...p.skills, ...p.interests]\n .join(' ')\n .toLowerCase();\n return haystack.includes(q);\n });\n }, [profiles, search]);\n\n const toggleFollow = async (profile: ComProfile) => {\n const wasFollowing = followSet.has(profile.memberId);\n const input = { targetId: profile.memberId, targetType: 'MEMBER' as const };\n setPendingId(profile.memberId);\n try {\n if (wasFollowing) {\n await unfollow.mutateAsync(input);\n notify.success('Unfollowed', profile.displayName);\n } else {\n await follow.mutateAsync(input);\n notify.success('Following', profile.displayName);\n }\n } catch (err) {\n notify.error(wasFollowing ? 'Could not unfollow' : 'Could not follow', describeError(err));\n } finally {\n setPendingId(null);\n }\n };\n\n return (\n <PageLayout\n title=\"Members\"\n description=\"Discover and follow people across your circles — search by name, headline, or skills.\"\n >\n {isLoading ? (\n <div className=\"flex flex-col gap-5\">\n <Skeleton className=\"h-9 w-full max-w-sm rounded-button\" />\n <div className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4\">\n {SKELETON_KEYS.map((key) => (\n <MemberCardSkeleton key={key} />\n ))}\n </div>\n </div>\n ) : isError ? (\n <ErrorView message=\"We couldn't load the member directory.\" onRetry={() => refetch()} />\n ) : profiles.length === 0 && pager.page === 0 ? (\n <EmptyState\n illustration=\"empty-people\"\n title=\"No members to discover yet\"\n description=\"When members make their profiles discoverable, they'll show up here for you to follow.\"\n />\n ) : (\n <div className=\"flex flex-col gap-5\">\n <div className=\"relative w-full max-w-sm\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary\"\n aria-hidden=\"true\"\n />\n <Input\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder=\"Search by name, headline, or skills\"\n aria-label=\"Search members\"\n className=\"pl-9\"\n />\n </div>\n\n {visible.length === 0 ? (\n <EmptyState\n icon={<Search className=\"h-9 w-9\" />}\n title=\"No matches on this page\"\n description=\"No members on this page match your search. Try a different term or browse another page.\"\n />\n ) : (\n <>\n <p className=\"text-xs font-medium text-text-tertiary\">\n Showing {visible.length} member{visible.length === 1 ? '' : 's'}\n </p>\n\n <div className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4\">\n {visible.map((p) => {\n const isSelf = Boolean(currentUser?.id && p.memberId === currentUser.id);\n const isFollowing = followSet.has(p.memberId);\n const isPending = pendingId === p.memberId;\n const chips = Array.from(new Set([...p.skills, ...p.interests])).slice(0, 4);\n\n return (\n <GlassCard\n key={p.id}\n treatment=\"plain\"\n className=\"flex flex-col p-5 transition-all duration-200 motion-safe:hover:-translate-y-0.5 hover:border-[var(--color-accent-soft)]\"\n >\n <button\n type=\"button\"\n onClick={() => navigateTo(`members/${p.memberId}`)}\n aria-label={`View ${p.displayName}'s profile`}\n className=\"group/head flex w-full items-start gap-3 rounded-lg text-left outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Avatar\n name={p.displayName}\n presence={p.presence}\n size=\"lg\"\n fileId={p.avatar?.id}\n seed={p.memberId}\n />\n <div className=\"min-w-0 flex-1\">\n <span className=\"block max-w-full truncate text-base font-semibold text-text-primary group-hover/head:underline\">\n {p.displayName}\n </span>\n {p.headline && (\n <p className=\"truncate text-sm text-text-secondary\">{p.headline}</p>\n )}\n {p.bio && (\n <p className=\"mt-0.5 line-clamp-1 text-xs text-text-tertiary\">\n {p.bio}\n </p>\n )}\n </div>\n </button>\n\n {chips.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n {chips.map((chip) => (\n <span\n key={chip}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\"\n >\n {chip}\n </span>\n ))}\n </div>\n )}\n\n {!isSelf && (\n <div className=\"mt-4 flex items-center justify-end border-t border-border-seam pt-3\">\n {isFollowing ? (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={(e) => {\n e.stopPropagation();\n void toggleFollow(p);\n }}\n disabled={isPending}\n aria-label={`Following ${p.displayName}. Activate to unfollow.`}\n className=\"group/follow min-w-[7.5rem] justify-center hover:border-status-error-border hover:bg-status-error-bg-subtle hover:text-status-error-text\"\n >\n {isPending ? (\n <>\n <Loader2 className=\"motion-safe:animate-spin\" />\n <span>Following</span>\n </>\n ) : (\n <>\n <Check className=\"group-hover/follow:hidden\" />\n <UserMinus className=\"hidden group-hover/follow:inline\" />\n <span className=\"group-hover/follow:hidden\">Following</span>\n <span className=\"hidden group-hover/follow:inline\">Unfollow</span>\n </>\n )}\n </Button>\n ) : (\n <Button\n size=\"sm\"\n onClick={(e) => {\n e.stopPropagation();\n void toggleFollow(p);\n }}\n disabled={isPending}\n className=\"min-w-[7.5rem] justify-center\"\n >\n {isPending ? (\n <Loader2 className=\"motion-safe:animate-spin\" />\n ) : (\n <UserPlus />\n )}\n Follow\n </Button>\n )}\n </div>\n )}\n </GlassCard>\n );\n })}\n </div>\n </>\n )}\n\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={profiles.length}\n hasMore={data?.pageInfo.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target, data?.pageInfo.cursor)}\n />\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MembersPage.js","names":[],"sources":["../../../../src/comradecircle/pages/members/MembersPage.tsx"],"sourcesContent":["/**\n * MembersPage — the discoverable member directory.\n * @module comradecircle/pages/members\n *\n * Reads `comListProfiles(discoverable: true)` — every member who has opted into\n * discovery via their {@link ComProfile}. Each card resolves display identity\n * from the social-identity layer (never a raw actor id), links to the member's\n * public profile, and offers a Follow/Following toggle wired to `comFollow` /\n * `comUnfollow` (targetType MEMBER). An optional client-side search narrows the\n * current page by name, headline, skills, or interests.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Search, UserPlus, UserMinus, Check, Loader2 } from 'lucide-react';\nimport { GlassCard, Button, Input, Skeleton } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { Avatar } from '../../components/Avatar';\nimport { EmptyState } from '../../components/EmptyState';\nimport { ErrorView } from '../../components/StateViews';\nimport { Pagination } from '../../components/Pagination';\nimport {\n useComProfiles,\n useComMyFollows,\n useComFollow,\n useComUnfollow,\n useCursorPager,\n} from '../../hooks';\nimport { useComradeCircle } from '../../context';\nimport { notify, describeError } from '../../lib/toast';\nimport type { ComProfile } from '../../types';\n\nconst PAGE_SIZE = 12;\n\nconst SKELETON_KEYS = ['s1', 's2', 's3', 's4', 's5', 's6', 's7', 's8'] as const;\n\n/** Card-shaped placeholder that mirrors the member card while the page loads. */\nfunction MemberCardSkeleton() {\n return (\n <GlassCard treatment=\"plain\" className=\"flex flex-col p-5\">\n <div className=\"flex items-start gap-3\">\n <Skeleton className=\"h-14 w-14 rounded-full\" />\n <div className=\"min-w-0 flex-1 space-y-2 py-1\">\n <Skeleton className=\"h-4 w-2/3\" />\n <Skeleton className=\"h-3 w-1/2\" />\n <Skeleton className=\"h-3 w-4/5\" />\n </div>\n </div>\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n <Skeleton className=\"h-5 w-16 rounded-full\" />\n <Skeleton className=\"h-5 w-20 rounded-full\" />\n </div>\n <div className=\"mt-4 flex justify-end border-t border-border-seam pt-3\">\n <Skeleton className=\"h-8 w-28 rounded-button\" />\n </div>\n </GlassCard>\n );\n}\n\nexport function MembersPage() {\n const { navigateTo, currentUser } = useComradeCircle();\n const pager = useCursorPager();\n const [search, setSearch] = useState('');\n const [pendingId, setPendingId] = useState<string | null>(null);\n\n const { data, isLoading, isError, refetch } = useComProfiles({\n discoverable: true,\n pagination: { limit: PAGE_SIZE, cursor: pager.cursor },\n });\n\n const { data: follows } = useComMyFollows('MEMBER');\n const follow = useComFollow();\n const unfollow = useComUnfollow();\n\n const followSet = useMemo(() => new Set((follows ?? []).map((f) => f.targetId)), [follows]);\n\n const profiles = useMemo(() => data?.items ?? [], [data]);\n\n const visible = useMemo(() => {\n const q = search.trim().toLowerCase();\n if (!q) return profiles;\n return profiles.filter((p) => {\n const haystack = [p.displayName, p.headline ?? '', ...p.skills, ...p.interests]\n .join(' ')\n .toLowerCase();\n return haystack.includes(q);\n });\n }, [profiles, search]);\n\n const toggleFollow = async (profile: ComProfile) => {\n const wasFollowing = followSet.has(profile.memberId);\n const input = { targetId: profile.memberId, targetType: 'MEMBER' as const };\n setPendingId(profile.memberId);\n try {\n if (wasFollowing) {\n await unfollow.mutateAsync(input);\n notify.success('Unfollowed', profile.displayName);\n } else {\n await follow.mutateAsync(input);\n notify.success('Following', profile.displayName);\n }\n } catch (err) {\n notify.error(wasFollowing ? 'Could not unfollow' : 'Could not follow', describeError(err));\n } finally {\n setPendingId(null);\n }\n };\n\n return (\n <PageLayout\n title=\"Members\"\n description=\"Discover and follow people across your circles — search by name, headline, or skills.\"\n >\n {isLoading ? (\n <div className=\"flex flex-col gap-5\">\n <Skeleton className=\"h-9 w-full max-w-sm rounded-button\" />\n <div className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4\">\n {SKELETON_KEYS.map((key) => (\n <MemberCardSkeleton key={key} />\n ))}\n </div>\n </div>\n ) : isError ? (\n <ErrorView message=\"We couldn't load the member directory.\" onRetry={() => refetch()} />\n ) : profiles.length === 0 && pager.page === 0 ? (\n <EmptyState\n illustration=\"empty-people\"\n title=\"No members to discover yet\"\n description=\"When members make their profiles discoverable, they'll show up here for you to follow.\"\n />\n ) : (\n <div className=\"flex flex-col gap-5\">\n <div className=\"relative w-full max-w-sm\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary\"\n aria-hidden=\"true\"\n />\n <Input\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder=\"Search by name, headline, or skills\"\n aria-label=\"Search members\"\n className=\"pl-9\"\n />\n </div>\n\n {visible.length === 0 ? (\n <EmptyState\n icon={<Search className=\"h-9 w-9\" />}\n title=\"No matches on this page\"\n description=\"No members on this page match your search. Try a different term or browse another page.\"\n />\n ) : (\n <>\n <p className=\"text-xs font-medium text-text-tertiary\">\n Showing {visible.length} member{visible.length === 1 ? '' : 's'}\n </p>\n\n <div className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4\">\n {visible.map((p) => {\n const isSelf = Boolean(currentUser?.id && p.memberId === currentUser.id);\n const isFollowing = followSet.has(p.memberId);\n const isPending = pendingId === p.memberId;\n const chips = Array.from(new Set([...p.skills, ...p.interests])).slice(0, 4);\n\n return (\n <GlassCard\n key={p.id}\n treatment=\"plain\"\n className=\"flex flex-col p-5 transition-all duration-200 motion-safe:hover:-translate-y-0.5 hover:border-[var(--color-accent-soft)]\"\n >\n <button\n type=\"button\"\n onClick={() => navigateTo(`members/${p.memberId}`)}\n aria-label={`View ${p.displayName}'s profile`}\n className=\"group/head flex w-full items-start gap-3 rounded-lg text-left outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Avatar\n name={p.displayName}\n presence={p.presence}\n size=\"lg\"\n fileId={p.avatar?.id}\n seed={p.memberId}\n />\n <div className=\"min-w-0 flex-1\">\n <span className=\"block max-w-full truncate text-base font-semibold text-text-primary group-hover/head:underline\">\n {p.displayName}\n </span>\n {p.headline && (\n <p className=\"truncate text-sm text-text-secondary\">{p.headline}</p>\n )}\n {p.bio && (\n <p className=\"mt-0.5 line-clamp-1 text-xs text-text-tertiary\">\n {p.bio}\n </p>\n )}\n </div>\n </button>\n\n {chips.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n {chips.map((chip) => (\n <span\n key={chip}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\"\n >\n {chip}\n </span>\n ))}\n </div>\n )}\n\n {!isSelf && (\n <div className=\"mt-4 flex items-center justify-end border-t border-border-seam pt-3\">\n {isFollowing ? (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={(e) => {\n e.stopPropagation();\n void toggleFollow(p);\n }}\n disabled={isPending}\n aria-label={`Following ${p.displayName}. Activate to unfollow.`}\n className=\"group/follow min-w-[7.5rem] justify-center hover:border-status-error-border hover:bg-status-error-bg-subtle hover:text-status-error-text\"\n >\n {isPending ? (\n <>\n <Loader2 className=\"motion-safe:animate-spin\" />\n <span>Following</span>\n </>\n ) : (\n <>\n <Check className=\"group-hover/follow:hidden\" />\n <UserMinus className=\"hidden group-hover/follow:inline\" />\n <span className=\"group-hover/follow:hidden\">Following</span>\n <span className=\"hidden group-hover/follow:inline\">Unfollow</span>\n </>\n )}\n </Button>\n ) : (\n <Button\n size=\"sm\"\n onClick={(e) => {\n e.stopPropagation();\n void toggleFollow(p);\n }}\n disabled={isPending}\n className=\"min-w-[7.5rem] justify-center\"\n >\n {isPending ? (\n <Loader2 className=\"motion-safe:animate-spin\" />\n ) : (\n <UserPlus />\n )}\n Follow\n </Button>\n )}\n </div>\n )}\n </GlassCard>\n );\n })}\n </div>\n </>\n )}\n\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={profiles.length}\n hasMore={data?.pageInfo.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target, data?.pageInfo.cursor)}\n />\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MyProfilePage.js","names":[],"sources":["../../../../src/comradecircle/pages/profile/MyProfilePage.tsx"],"sourcesContent":["/**\n * MyProfilePage — create or edit the caller's own social profile.\n * @module comradecircle/pages/profile\n *\n * A member's {@link ComProfile} is their identity across every circle — the\n * name, headline, presence and avatar initials that `MemberIdentity` resolves\n * everywhere a `memberId` appears. This page reads the caller's own profile\n * (`comProfile` by `currentUser.id`) and upserts it (`comUpsertProfile`). It\n * works both when a profile already exists (edit) and when none does yet\n * (create). The editor is only mounted once the profile query has settled and\n * seeds its fields via lazy state initialisers (keyed on the profile id), so no\n * state-syncing effect is needed. A social-card live preview mirrors the form,\n * a completeness meter nudges the caller to fill it in, and Save stays disabled\n * until something actually changes.\n */\n\nimport { useState, type ReactNode } from 'react';\nimport { Save } from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Switch,\n GlassCard,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { Avatar } from '../../components/Avatar';\nimport { AvatarUploadField } from '../../components/AvatarUploadField';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { TagInput } from '../../components/TagInput';\nimport { MiniBar } from '../../components/MiniBar';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useComProfile, useComUpsertProfile } from '../../hooks';\nimport { useComradeCircle } from '../../context';\nimport { formatEnumLabel } from '../../utils';\nimport { notify, describeError } from '../../lib/toast';\nimport type { ComPresence, ComProfile } from '../../types';\n\nconst PRESENCE_OPTIONS: ComPresence[] = ['ONLINE', 'AWAY', 'BUSY', 'OFFLINE'];\n\nexport function MyProfilePage() {\n const { currentUser } = useComradeCircle();\n const memberId = currentUser?.id;\n const { data: profile, isLoading, isError, refetch } = useComProfile(memberId);\n\n // Seed create-mode from the shell identity so the name field is not blank.\n const identityName = currentUser?.name ?? currentUser?.email ?? '';\n\n if (isLoading) {\n return (\n <PageLayout title=\"My Profile\">\n <LoadingView label=\"Loading your profile…\" />\n </PageLayout>\n );\n }\n\n // A missing profile is NOT an error (the resolver returns null → create mode).\n // Only a genuine fetch failure lands here — surface it rather than silently\n // showing a blank form, which an upsert save could use to overwrite an\n // existing profile the caller never actually saw.\n if (isError) {\n return (\n <PageLayout title=\"My Profile\">\n <ErrorView\n title=\"We couldn't load your profile\"\n message=\"Something went wrong while fetching your profile. Retry before editing so you don't overwrite saved details.\"\n onRetry={() => refetch()}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"My Profile\"\n description=\"Your profile is your identity across every circle — it's how members recognise, find and follow you.\"\n >\n <ProfileEditor\n key={profile?.id ?? 'new'}\n initialProfile={profile ?? null}\n identityName={identityName}\n />\n </PageLayout>\n );\n}\n\ninterface ProfileEditorProps {\n initialProfile: ComProfile | null;\n identityName: string;\n}\n\n/** A subtle uppercase section header that groups the form. */\nfunction SectionLabel({ children }: { children: ReactNode }) {\n return (\n <p className=\"text-xs font-semibold uppercase tracking-[0.08em] text-text-tertiary\">\n {children}\n </p>\n );\n}\n\n/** Order-sensitive string-array equality (TagInput only appends / removes). */\nfunction sameTags(a: string[], b: string[]): boolean {\n return a.length === b.length && a.every((v, i) => v === b[i]);\n}\n\nfunction ProfileEditor({ initialProfile, identityName }: ProfileEditorProps) {\n const upsert = useComUpsertProfile();\n\n // The pristine baseline. Recomputed from props each render, but props are\n // stable for this component's life (ProfileEditor is keyed on the profile id),\n // so after a successful save the refetched profile flows back in here and the\n // form settles to \"no changes\" without any effect.\n const initial = {\n displayName: initialProfile?.displayName ?? identityName,\n headline: initialProfile?.headline ?? '',\n bio: initialProfile?.bio ?? '',\n interests: initialProfile?.interests ?? [],\n skills: initialProfile?.skills ?? [],\n presence: initialProfile?.presence ?? ('ONLINE' as ComPresence),\n discoverable: initialProfile?.discoverable ?? true,\n avatarFileId: initialProfile?.avatar?.id ?? null,\n };\n\n const [displayName, setDisplayName] = useState(initial.displayName);\n const [headline, setHeadline] = useState(initial.headline);\n const [bio, setBio] = useState(initial.bio);\n const [interests, setInterests] = useState<string[]>(initial.interests);\n const [skills, setSkills] = useState<string[]>(initial.skills);\n const [presence, setPresence] = useState<ComPresence>(initial.presence);\n const [discoverable, setDiscoverable] = useState(initial.discoverable);\n const [avatarFileId, setAvatarFileId] = useState<string | null>(initial.avatarFileId);\n\n const previewName = displayName.trim();\n\n // Profile completeness — derived entirely from the live (real) field values.\n const checklist: { done: boolean; nudge: string }[] = [\n {\n done: Boolean(avatarFileId),\n nudge: 'Add a profile photo so members recognise you at a glance.',\n },\n { done: previewName.length > 0, nudge: 'Add a display name so members know who you are.' },\n { done: headline.trim().length > 0, nudge: 'Add a headline to sum yourself up in one line.' },\n { done: bio.trim().length > 0, nudge: 'Write a short bio to help circles get to know you.' },\n { done: interests.length > 0, nudge: 'Add a few interests to help you find your people.' },\n { done: skills.length > 0, nudge: 'List some skills so circles know what you bring.' },\n ];\n const completedCount = checklist.filter((c) => c.done).length;\n const completeness = Math.round((completedCount / checklist.length) * 100);\n const nextNudge =\n checklist.find((c) => !c.done)?.nudge ?? 'Your profile is complete — it looks great.';\n\n // Dirty tracking — Save stays disabled until something actually changes.\n const isDirty =\n displayName !== initial.displayName ||\n headline !== initial.headline ||\n bio !== initial.bio ||\n presence !== initial.presence ||\n discoverable !== initial.discoverable ||\n avatarFileId !== initial.avatarFileId ||\n !sameTags(interests, initial.interests) ||\n !sameTags(skills, initial.skills);\n\n const handleSave = async () => {\n const name = displayName.trim();\n if (!name) {\n notify.error('Display name required', 'Add a name so members can recognise you.');\n return;\n }\n try {\n await upsert.mutateAsync({\n displayName: name,\n headline: headline.trim() || null,\n bio: bio.trim() || null,\n avatarFileId,\n interests,\n skills,\n presence,\n discoverable,\n });\n notify.success('Profile saved');\n } catch (err) {\n notify.error('Could not save profile', describeError(err));\n }\n };\n\n return (\n <div className=\"grid gap-6 lg:grid-cols-[1fr_320px]\">\n {/* Form */}\n <GlassCard treatment=\"plain\" className=\"order-2 p-5 sm:p-6 lg:order-1\">\n <div className=\"flex flex-col gap-6\">\n {/* Completeness meter — nudges the caller toward a richer profile. */}\n <div className=\"grid gap-2 rounded-xl border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex items-baseline justify-between gap-3\">\n <SectionLabel>Profile completeness</SectionLabel>\n <span className=\"text-sm font-semibold tabular-nums text-text-primary\">\n {completeness}%\n </span>\n </div>\n <MiniBar\n value={completeness}\n height={8}\n barClassName=\"bg-gradient-to-r from-accent to-[var(--color-accent-soft)]\"\n />\n <p className=\"text-xs text-text-secondary\">{nextNudge}</p>\n </div>\n\n {/* Identity */}\n <section className=\"grid gap-4\">\n <SectionLabel>Identity</SectionLabel>\n\n <div className=\"grid gap-1.5\">\n <Label>Avatar</Label>\n <AvatarUploadField\n fileId={avatarFileId}\n name={previewName || identityName}\n onUploaded={setAvatarFileId}\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"profile-name\">Display name</Label>\n <Input\n id=\"profile-name\"\n value={displayName}\n onChange={(e) => setDisplayName(e.target.value)}\n placeholder=\"How members see you\"\n required\n aria-required=\"true\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"profile-headline\">Headline</Label>\n <Input\n id=\"profile-headline\"\n value={headline}\n onChange={(e) => setHeadline(e.target.value)}\n placeholder=\"e.g. Community organiser · Photographer\"\n />\n </div>\n </section>\n\n {/* About you */}\n <section className=\"grid gap-4 border-t border-border-seam pt-6\">\n <SectionLabel>About you</SectionLabel>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"profile-bio\">Bio</Label>\n <Textarea\n id=\"profile-bio\"\n value={bio}\n onChange={(e) => setBio(e.target.value)}\n rows={4}\n placeholder=\"Tell circles a little about yourself.\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"profile-interests\">Interests</Label>\n <TagInput\n id=\"profile-interests\"\n value={interests}\n onChange={setInterests}\n placeholder=\"Add an interest…\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"profile-skills\">Skills</Label>\n <TagInput\n id=\"profile-skills\"\n value={skills}\n onChange={setSkills}\n placeholder=\"Add a skill…\"\n />\n </div>\n </section>\n\n {/* Visibility */}\n <section className=\"grid gap-4 border-t border-border-seam pt-6\">\n <SectionLabel>Visibility</SectionLabel>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"profile-presence\">Presence</Label>\n <Select value={presence} onValueChange={(v) => setPresence(v as ComPresence)}>\n <SelectTrigger id=\"profile-presence\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {PRESENCE_OPTIONS.map((p) => (\n <SelectItem key={p} value={p}>\n {formatEnumLabel(p)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"flex items-start justify-between gap-4 rounded-xl border border-border-seam bg-bg-surface p-3.5\">\n <div className=\"min-w-0\">\n <Label htmlFor=\"profile-discoverable\">List me in the member directory</Label>\n <p className=\"mt-1 text-xs text-text-tertiary\">\n When on, your profile appears in the discoverable member directory so others can\n find and follow you. Turn it off to stay unlisted.\n </p>\n </div>\n <Switch\n id=\"profile-discoverable\"\n checked={discoverable}\n onCheckedChange={setDiscoverable}\n aria-label=\"List me in the member directory\"\n className=\"mt-0.5 shrink-0\"\n />\n </div>\n </section>\n\n {/* Save */}\n <div className=\"flex flex-wrap items-center justify-end gap-3 border-t border-border-seam pt-5\">\n {!isDirty && !upsert.isPending && (\n <span className=\"text-xs text-text-tertiary\">No changes to save</span>\n )}\n <Button onClick={handleSave} disabled={upsert.isPending || !isDirty}>\n <Save className=\"mr-1.5 h-4 w-4\" />\n {upsert.isPending ? 'Saving…' : 'Save profile'}\n </Button>\n </div>\n </div>\n </GlassCard>\n\n {/* Live preview — the member's-eye view of this profile. */}\n <div className=\"order-1 lg:order-2\">\n <GlassCard treatment=\"plain\" className=\"overflow-hidden p-0 lg:sticky lg:top-6\">\n {/* Social cover strip — the xl avatar overlaps its bottom edge below. */}\n <div className=\"relative h-24 bg-gloss-card\">\n <div\n className=\"absolute inset-0 bg-gradient-to-br from-[var(--color-accent-soft)] via-[var(--color-accent-soft)]/30 to-transparent\"\n aria-hidden=\"true\"\n />\n <span className=\"absolute right-3 top-3 rounded-full bg-bg-surface/80 px-2 py-0.5 text-[0.62rem] font-medium uppercase tracking-[0.08em] text-text-tertiary backdrop-blur\">\n How members see you\n </span>\n </div>\n\n <div className=\"px-5 pb-5\">\n <div className=\"-mt-12 flex flex-col items-center text-center\">\n <Avatar\n name={previewName || identityName}\n size=\"xl\"\n presence={presence}\n fileId={avatarFileId}\n ring\n />\n <h3 className=\"mt-3 max-w-full truncate text-lg font-semibold text-text-primary\">\n {previewName || 'Your name'}\n </h3>\n {headline.trim() && (\n <p className=\"mt-0.5 max-w-full truncate text-sm text-text-secondary\">\n {headline.trim()}\n </p>\n )}\n <div className=\"mt-2.5\">\n <StatusBadge status={presence} />\n </div>\n </div>\n\n {bio.trim() && (\n <p className=\"mt-4 whitespace-pre-wrap break-words text-center text-sm text-text-secondary\">\n {bio.trim()}\n </p>\n )}\n\n {interests.length > 0 && (\n <div className=\"mt-5\">\n <p className=\"mb-1.5 text-xs font-medium text-text-tertiary\">Interests</p>\n <div className=\"flex flex-wrap gap-1.5\">\n {interests.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {skills.length > 0 && (\n <div className=\"mt-4\">\n <p className=\"mb-1.5 text-xs font-medium text-text-tertiary\">Skills</p>\n <div className=\"flex flex-wrap gap-1.5\">\n {skills.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-[var(--color-accent-soft)] px-2.5 py-0.5 text-xs font-medium text-text-primary\"\n >\n {tag}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {!discoverable && (\n <p className=\"mt-5 text-center text-xs text-text-tertiary\">\n Hidden from the member directory.\n </p>\n )}\n </div>\n </GlassCard>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA4CA,IAAM,IAAkC;CAAC;CAAU;CAAQ;CAAQ;CAAU;AAE7E,SAAgB,IAAgB;CAC9B,IAAM,EAAE,mBAAgB,GAAkB,EACpC,IAAW,GAAa,IACxB,EAAE,MAAM,GAAS,cAAW,YAAS,eAAY,EAAc,EAAS,EAGxE,IAAe,GAAa,QAAQ,GAAa,SAAS;AA0BhE,QAxBI,IAEA,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD,EAAa,OAAM,yBAA0B,CAAA;EAClC,CAAA,GAQb,IAEA,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD;GACE,OAAM;GACN,SAAQ;GACR,eAAe,GAAS;GACxB,CAAA;EACS,CAAA,GAKf,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;YAEZ,kBAAC,GAAD;GAEE,gBAAgB,KAAW;GACb;GACd,EAHK,GAAS,MAAM,MAGpB;EACS,CAAA;;AAUjB,SAAS,EAAa,EAAE,eAAqC;AAC3D,QACE,kBAAC,KAAD;EAAG,WAAU;EACV;EACC,CAAA;;AAKR,SAAS,EAAS,GAAa,GAAsB;AACnD,QAAO,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,GAAG,MAAM,MAAM,EAAE,GAAG;;AAG/D,SAAS,EAAc,EAAE,mBAAgB,mBAAoC;CAC3E,IAAM,IAAS,GAAqB,EAM9B,IAAU;EACd,aAAa,GAAgB,eAAe;EAC5C,UAAU,GAAgB,YAAY;EACtC,KAAK,GAAgB,OAAO;EAC5B,WAAW,GAAgB,aAAa,EAAE;EAC1C,QAAQ,GAAgB,UAAU,EAAE;EACpC,UAAU,GAAgB,YAAa;EACvC,cAAc,GAAgB,gBAAgB;EAC9C,cAAc,GAAgB,QAAQ,MAAM;EAC7C,EAEK,CAAC,GAAa,KAAkB,EAAS,EAAQ,YAAY,EAC7D,CAAC,GAAU,KAAe,EAAS,EAAQ,SAAS,EACpD,CAAC,GAAK,KAAU,EAAS,EAAQ,IAAI,EACrC,CAAC,GAAW,KAAgB,EAAmB,EAAQ,UAAU,EACjE,CAAC,GAAQ,KAAa,EAAmB,EAAQ,OAAO,EACxD,CAAC,GAAU,KAAe,EAAsB,EAAQ,SAAS,EACjE,CAAC,GAAc,KAAmB,EAAS,EAAQ,aAAa,EAChE,CAAC,GAAc,KAAmB,EAAwB,EAAQ,aAAa,EAE/E,IAAc,EAAY,MAAM,EAGhC,IAAgD;EACpD;GACE,MAAM,EAAQ;GACd,OAAO;GACR;EACD;GAAE,MAAM,EAAY,SAAS;GAAG,OAAO;GAAmD;EAC1F;GAAE,MAAM,EAAS,MAAM,CAAC,SAAS;GAAG,OAAO;GAAkD;EAC7F;GAAE,MAAM,EAAI,MAAM,CAAC,SAAS;GAAG,OAAO;GAAsD;EAC5F;GAAE,MAAM,EAAU,SAAS;GAAG,OAAO;GAAqD;EAC1F;GAAE,MAAM,EAAO,SAAS;GAAG,OAAO;GAAoD;EACvF,EACK,IAAiB,EAAU,QAAQ,MAAM,EAAE,KAAK,CAAC,QACjD,IAAe,KAAK,MAAO,IAAiB,EAAU,SAAU,IAAI,EACpE,IACJ,EAAU,MAAM,MAAM,CAAC,EAAE,KAAK,EAAE,SAAS,8CAGrC,IACJ,MAAgB,EAAQ,eACxB,MAAa,EAAQ,YACrB,MAAQ,EAAQ,OAChB,MAAa,EAAQ,YACrB,MAAiB,EAAQ,gBACzB,MAAiB,EAAQ,gBACzB,CAAC,EAAS,GAAW,EAAQ,UAAU,IACvC,CAAC,EAAS,GAAQ,EAAQ,OAAO;AAyBnC,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CAEE,kBAAC,GAAD;GAAW,WAAU;GAAQ,WAAU;aACrC,kBAAC,OAAD;IAAK,WAAU;cAAf;KAEE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAA,UAAc,wBAAmC,CAAA,EACjD,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CACG,GAAa,IACT;WACH;;OACN,kBAAC,GAAD;QACE,OAAO;QACP,QAAQ;QACR,cAAa;QACb,CAAA;OACF,kBAAC,KAAD;QAAG,WAAU;kBAA+B;QAAc,CAAA;OACtD;;KAGN,kBAAC,WAAD;MAAS,WAAU;gBAAnB;OACE,kBAAC,GAAD,EAAA,UAAc,YAAuB,CAAA;OAErC,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAA,UAAO,UAAc,CAAA,EACrB,kBAAC,GAAD;SACE,QAAQ;SACR,MAAM,KAAe;SACrB,YAAY;SACZ,CAAA,CACE;;OAEN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAe;SAAoB,CAAA,EAClD,kBAAC,GAAD;SACE,IAAG;SACH,OAAO;SACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;SAC/C,aAAY;SACZ,UAAA;SACA,iBAAc;SACd,CAAA,CACE;;OAEN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAmB;SAAgB,CAAA,EAClD,kBAAC,GAAD;SACE,IAAG;SACH,OAAO;SACP,WAAW,MAAM,EAAY,EAAE,OAAO,MAAM;SAC5C,aAAY;SACZ,CAAA,CACE;;OACE;;KAGV,kBAAC,WAAD;MAAS,WAAU;gBAAnB;OACE,kBAAC,GAAD,EAAA,UAAc,aAAwB,CAAA;OAEtC,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAc;SAAW,CAAA,EACxC,kBAAC,GAAD;SACE,IAAG;SACH,OAAO;SACP,WAAW,MAAM,EAAO,EAAE,OAAO,MAAM;SACvC,MAAM;SACN,aAAY;SACZ,CAAA,CACE;;OAEN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAoB;SAAiB,CAAA,EACpD,kBAAC,GAAD;SACE,IAAG;SACH,OAAO;SACP,UAAU;SACV,aAAY;SACZ,CAAA,CACE;;OAEN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAiB;SAAc,CAAA,EAC9C,kBAAC,GAAD;SACE,IAAG;SACH,OAAO;SACP,UAAU;SACV,aAAY;SACZ,CAAA,CACE;;OACE;;KAGV,kBAAC,WAAD;MAAS,WAAU;gBAAnB;OACE,kBAAC,GAAD,EAAA,UAAc,cAAyB,CAAA;OAEvC,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAmB;SAAgB,CAAA,EAClD,kBAAC,GAAD;SAAQ,OAAO;SAAU,gBAAgB,MAAM,EAAY,EAAiB;mBAA5E,CACE,kBAAC,GAAD;UAAe,IAAG;oBAChB,kBAAC,GAAD,EAAe,CAAA;UACD,CAAA,EAChB,kBAAC,GAAD,EAAA,UACG,EAAiB,KAAK,MACrB,kBAAC,GAAD;UAAoB,OAAO;oBACxB,EAAgB,EAAE;UACR,EAFI,EAEJ,CACb,EACY,CAAA,CACT;WACL;;OAEN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD;UAAO,SAAQ;oBAAuB;UAAuC,CAAA,EAC7E,kBAAC,KAAD;UAAG,WAAU;oBAAkC;UAG3C,CAAA,CACA;YACN,kBAAC,GAAD;SACE,IAAG;SACH,SAAS;SACT,iBAAiB;SACjB,cAAW;SACX,WAAU;SACV,CAAA,CACE;;OACE;;KAGV,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACG,CAAC,KAAW,CAAC,EAAO,aACnB,kBAAC,QAAD;OAAM,WAAU;iBAA6B;OAAyB,CAAA,EAExE,kBAAC,GAAD;OAAQ,SA/JC,YAAY;QAC7B,IAAM,IAAO,EAAY,MAAM;AAC/B,YAAI,CAAC,GAAM;AACT,WAAO,MAAM,yBAAyB,2CAA2C;AACjF;;AAEF,YAAI;AAWF,SAVA,MAAM,EAAO,YAAY;UACvB,aAAa;UACb,UAAU,EAAS,MAAM,IAAI;UAC7B,KAAK,EAAI,MAAM,IAAI;UACnB;UACA;UACA;UACA;UACA;UACD,CAAC,EACF,EAAO,QAAQ,gBAAgB;iBACxB,GAAK;AACZ,WAAO,MAAM,0BAA0B,EAAc,EAAI,CAAC;;;OA4IvB,UAAU,EAAO,aAAa,CAAC;iBAA5D,CACE,kBAAC,GAAD,EAAM,WAAU,kBAAmB,CAAA,EAClC,EAAO,YAAY,YAAY,eACzB;SACL;;KACF;;GACI,CAAA,EAGZ,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,GAAD;IAAW,WAAU;IAAQ,WAAU;cAAvC,CAEE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MACE,WAAU;MACV,eAAY;MACZ,CAAA,EACF,kBAAC,QAAD;MAAM,WAAU;gBAA2J;MAEpK,CAAA,CACH;QAEN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD;SACE,MAAM,KAAe;SACrB,MAAK;SACK;SACV,QAAQ;SACR,MAAA;SACA,CAAA;QACF,kBAAC,MAAD;SAAI,WAAU;mBACX,KAAe;SACb,CAAA;QACJ,EAAS,MAAM,IACd,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAS,MAAM;SACd,CAAA;QAEN,kBAAC,OAAD;SAAK,WAAU;mBACb,kBAAC,GAAD,EAAa,QAAQ,GAAY,CAAA;SAC7B,CAAA;QACF;;MAEL,EAAI,MAAM,IACT,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAI,MAAM;OACT,CAAA;MAGL,EAAU,SAAS,KAClB,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,KAAD;QAAG,WAAU;kBAAgD;QAAa,CAAA,EAC1E,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAU,KAAK,MACd,kBAAC,QAAD;SAEE,WAAU;mBAET;SACI,EAJA,EAIA,CACP;QACE,CAAA,CACF;;MAGP,EAAO,SAAS,KACf,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,KAAD;QAAG,WAAU;kBAAgD;QAAU,CAAA,EACvE,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAO,KAAK,MACX,kBAAC,QAAD;SAEE,WAAU;mBAET;SACI,EAJA,EAIA,CACP;QACE,CAAA,CACF;;MAGP,CAAC,KACA,kBAAC,KAAD;OAAG,WAAU;iBAA8C;OAEvD,CAAA;MAEF;OACI;;GACR,CAAA,CACF"}
|
|
1
|
+
{"version":3,"file":"MyProfilePage.js","names":[],"sources":["../../../../src/comradecircle/pages/profile/MyProfilePage.tsx"],"sourcesContent":["/**\n * MyProfilePage — create or edit the caller's own social profile.\n * @module comradecircle/pages/profile\n *\n * A member's {@link ComProfile} is their identity across every circle — the\n * name, headline, presence and avatar initials that `MemberIdentity` resolves\n * everywhere a `memberId` appears. This page reads the caller's own profile\n * (`comProfile` by `currentUser.id`) and upserts it (`comUpsertProfile`). It\n * works both when a profile already exists (edit) and when none does yet\n * (create). The editor is only mounted once the profile query has settled and\n * seeds its fields via lazy state initialisers (keyed on the profile id), so no\n * state-syncing effect is needed. A social-card live preview mirrors the form,\n * a completeness meter nudges the caller to fill it in, and Save stays disabled\n * until something actually changes.\n */\n\nimport { useState, type ReactNode } from 'react';\nimport { Save } from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Switch,\n GlassCard,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { Avatar } from '../../components/Avatar';\nimport { AvatarUploadField } from '../../components/AvatarUploadField';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { TagInput } from '../../components/TagInput';\nimport { MiniBar } from '../../components/MiniBar';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useComProfile, useComUpsertProfile } from '../../hooks';\nimport { useComradeCircle } from '../../context';\nimport { formatEnumLabel } from '../../utils';\nimport { notify, describeError } from '../../lib/toast';\nimport type { ComPresence, ComProfile } from '../../types';\n\nconst PRESENCE_OPTIONS: ComPresence[] = ['ONLINE', 'AWAY', 'BUSY', 'OFFLINE'];\n\nexport function MyProfilePage() {\n const { currentUser } = useComradeCircle();\n const memberId = currentUser?.id;\n const { data: profile, isLoading, isError, refetch } = useComProfile(memberId);\n\n // Seed create-mode from the shell identity so the name field is not blank.\n const identityName = currentUser?.name ?? currentUser?.email ?? '';\n\n if (isLoading) {\n return (\n <PageLayout title=\"My Profile\">\n <LoadingView label=\"Loading your profile…\" />\n </PageLayout>\n );\n }\n\n // A missing profile is NOT an error (the resolver returns null → create mode).\n // Only a genuine fetch failure lands here — surface it rather than silently\n // showing a blank form, which an upsert save could use to overwrite an\n // existing profile the caller never actually saw.\n if (isError) {\n return (\n <PageLayout title=\"My Profile\">\n <ErrorView\n title=\"We couldn't load your profile\"\n message=\"Something went wrong while fetching your profile. Retry before editing so you don't overwrite saved details.\"\n onRetry={() => refetch()}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"My Profile\"\n description=\"Your profile is your identity across every circle — it's how members recognise, find and follow you.\"\n >\n <ProfileEditor\n key={profile?.id ?? 'new'}\n initialProfile={profile ?? null}\n identityName={identityName}\n />\n </PageLayout>\n );\n}\n\ninterface ProfileEditorProps {\n initialProfile: ComProfile | null;\n identityName: string;\n}\n\n/** A subtle uppercase section header that groups the form. */\nfunction SectionLabel({ children }: { children: ReactNode }) {\n return (\n <p className=\"text-xs font-semibold uppercase tracking-[0.08em] text-text-tertiary\">\n {children}\n </p>\n );\n}\n\n/** Order-sensitive string-array equality (TagInput only appends / removes). */\nfunction sameTags(a: string[], b: string[]): boolean {\n return a.length === b.length && a.every((v, i) => v === b[i]);\n}\n\nfunction ProfileEditor({ initialProfile, identityName }: ProfileEditorProps) {\n const upsert = useComUpsertProfile();\n\n // The pristine baseline. Recomputed from props each render, but props are\n // stable for this component's life (ProfileEditor is keyed on the profile id),\n // so after a successful save the refetched profile flows back in here and the\n // form settles to \"no changes\" without any effect.\n const initial = {\n displayName: initialProfile?.displayName ?? identityName,\n headline: initialProfile?.headline ?? '',\n bio: initialProfile?.bio ?? '',\n interests: initialProfile?.interests ?? [],\n skills: initialProfile?.skills ?? [],\n presence: initialProfile?.presence ?? ('ONLINE' as ComPresence),\n discoverable: initialProfile?.discoverable ?? true,\n avatarFileId: initialProfile?.avatar?.id ?? null,\n };\n\n const [displayName, setDisplayName] = useState(initial.displayName);\n const [headline, setHeadline] = useState(initial.headline);\n const [bio, setBio] = useState(initial.bio);\n const [interests, setInterests] = useState<string[]>(initial.interests);\n const [skills, setSkills] = useState<string[]>(initial.skills);\n const [presence, setPresence] = useState<ComPresence>(initial.presence);\n const [discoverable, setDiscoverable] = useState(initial.discoverable);\n const [avatarFileId, setAvatarFileId] = useState<string | null>(initial.avatarFileId);\n\n const previewName = displayName.trim();\n\n // Profile completeness — derived entirely from the live (real) field values.\n const checklist: { done: boolean; nudge: string }[] = [\n {\n done: Boolean(avatarFileId),\n nudge: 'Add a profile photo so members recognise you at a glance.',\n },\n { done: previewName.length > 0, nudge: 'Add a display name so members know who you are.' },\n { done: headline.trim().length > 0, nudge: 'Add a headline to sum yourself up in one line.' },\n { done: bio.trim().length > 0, nudge: 'Write a short bio to help circles get to know you.' },\n { done: interests.length > 0, nudge: 'Add a few interests to help you find your people.' },\n { done: skills.length > 0, nudge: 'List some skills so circles know what you bring.' },\n ];\n const completedCount = checklist.filter((c) => c.done).length;\n const completeness = Math.round((completedCount / checklist.length) * 100);\n const nextNudge =\n checklist.find((c) => !c.done)?.nudge ?? 'Your profile is complete — it looks great.';\n\n // Dirty tracking — Save stays disabled until something actually changes.\n const isDirty =\n displayName !== initial.displayName ||\n headline !== initial.headline ||\n bio !== initial.bio ||\n presence !== initial.presence ||\n discoverable !== initial.discoverable ||\n avatarFileId !== initial.avatarFileId ||\n !sameTags(interests, initial.interests) ||\n !sameTags(skills, initial.skills);\n\n const handleSave = async () => {\n const name = displayName.trim();\n if (!name) {\n notify.error('Display name required', 'Add a name so members can recognise you.');\n return;\n }\n try {\n await upsert.mutateAsync({\n displayName: name,\n headline: headline.trim() || null,\n bio: bio.trim() || null,\n avatarFileId,\n interests,\n skills,\n presence,\n discoverable,\n });\n notify.success('Profile saved');\n } catch (err) {\n notify.error('Could not save profile', describeError(err));\n }\n };\n\n return (\n <div className=\"grid gap-6 lg:grid-cols-[1fr_320px]\">\n {/* Form */}\n <GlassCard treatment=\"plain\" className=\"order-2 p-5 sm:p-6 lg:order-1\">\n <div className=\"flex flex-col gap-6\">\n {/* Completeness meter — nudges the caller toward a richer profile. */}\n <div className=\"grid gap-2 rounded-xl border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex items-baseline justify-between gap-3\">\n <SectionLabel>Profile completeness</SectionLabel>\n <span className=\"text-sm font-semibold tabular-nums text-text-primary\">\n {completeness}%\n </span>\n </div>\n <MiniBar\n value={completeness}\n height={8}\n barClassName=\"bg-gradient-to-r from-accent to-[var(--color-accent-soft)]\"\n />\n <p className=\"text-xs text-text-secondary\">{nextNudge}</p>\n </div>\n\n {/* Identity */}\n <section className=\"grid gap-4\">\n <SectionLabel>Identity</SectionLabel>\n\n <div className=\"grid gap-1.5\">\n <Label>Avatar</Label>\n <AvatarUploadField\n fileId={avatarFileId}\n name={previewName || identityName}\n onUploaded={setAvatarFileId}\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"profile-name\">Display name</Label>\n <Input\n id=\"profile-name\"\n value={displayName}\n onChange={(e) => setDisplayName(e.target.value)}\n placeholder=\"How members see you\"\n required\n aria-required=\"true\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"profile-headline\">Headline</Label>\n <Input\n id=\"profile-headline\"\n value={headline}\n onChange={(e) => setHeadline(e.target.value)}\n placeholder=\"e.g. Community organiser · Photographer\"\n />\n </div>\n </section>\n\n {/* About you */}\n <section className=\"grid gap-4 border-t border-border-seam pt-6\">\n <SectionLabel>About you</SectionLabel>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"profile-bio\">Bio</Label>\n <Textarea\n id=\"profile-bio\"\n value={bio}\n onChange={(e) => setBio(e.target.value)}\n rows={4}\n placeholder=\"Tell circles a little about yourself.\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"profile-interests\">Interests</Label>\n <TagInput\n id=\"profile-interests\"\n value={interests}\n onChange={setInterests}\n placeholder=\"Add an interest…\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"profile-skills\">Skills</Label>\n <TagInput\n id=\"profile-skills\"\n value={skills}\n onChange={setSkills}\n placeholder=\"Add a skill…\"\n />\n </div>\n </section>\n\n {/* Visibility */}\n <section className=\"grid gap-4 border-t border-border-seam pt-6\">\n <SectionLabel>Visibility</SectionLabel>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"profile-presence\">Presence</Label>\n <Select value={presence} onValueChange={(v) => setPresence(v as ComPresence)}>\n <SelectTrigger id=\"profile-presence\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {PRESENCE_OPTIONS.map((p) => (\n <SelectItem key={p} value={p}>\n {formatEnumLabel(p)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"flex items-start justify-between gap-4 rounded-xl border border-border-seam bg-bg-surface p-3.5\">\n <div className=\"min-w-0\">\n <Label htmlFor=\"profile-discoverable\">List me in the member directory</Label>\n <p className=\"mt-1 text-xs text-text-tertiary\">\n When on, your profile appears in the discoverable member directory so others can\n find and follow you. Turn it off to stay unlisted.\n </p>\n </div>\n <Switch\n id=\"profile-discoverable\"\n checked={discoverable}\n onCheckedChange={setDiscoverable}\n aria-label=\"List me in the member directory\"\n className=\"mt-0.5 shrink-0\"\n />\n </div>\n </section>\n\n {/* Save */}\n <div className=\"flex flex-wrap items-center justify-end gap-3 border-t border-border-seam pt-5\">\n {!isDirty && !upsert.isPending && (\n <span className=\"text-xs text-text-tertiary\">No changes to save</span>\n )}\n <Button onClick={handleSave} disabled={upsert.isPending || !isDirty}>\n <Save className=\"mr-1.5 h-4 w-4\" />\n {upsert.isPending ? 'Saving…' : 'Save profile'}\n </Button>\n </div>\n </div>\n </GlassCard>\n\n {/* Live preview — the member's-eye view of this profile. */}\n <div className=\"order-1 lg:order-2\">\n <GlassCard treatment=\"plain\" className=\"overflow-hidden p-0 lg:sticky lg:top-6\">\n {/* Social cover strip — the xl avatar overlaps its bottom edge below. */}\n <div className=\"relative h-24 bg-gloss-card\">\n <div\n className=\"absolute inset-0 bg-gradient-to-br from-[var(--color-accent-soft)] via-[var(--color-accent-soft)]/30 to-transparent\"\n aria-hidden=\"true\"\n />\n <span className=\"absolute right-3 top-3 rounded-full bg-bg-surface/80 px-2 py-0.5 text-[0.62rem] font-medium uppercase tracking-[0.08em] text-text-tertiary backdrop-blur\">\n How members see you\n </span>\n </div>\n\n <div className=\"px-5 pb-5\">\n <div className=\"-mt-12 flex flex-col items-center text-center\">\n <Avatar\n name={previewName || identityName}\n size=\"xl\"\n presence={presence}\n fileId={avatarFileId}\n ring\n />\n <h3 className=\"mt-3 max-w-full truncate text-lg font-semibold text-text-primary\">\n {previewName || 'Your name'}\n </h3>\n {headline.trim() && (\n <p className=\"mt-0.5 max-w-full truncate text-sm text-text-secondary\">\n {headline.trim()}\n </p>\n )}\n <div className=\"mt-2.5\">\n <StatusBadge status={presence} />\n </div>\n </div>\n\n {bio.trim() && (\n <p className=\"mt-4 whitespace-pre-wrap break-words text-center text-sm text-text-secondary\">\n {bio.trim()}\n </p>\n )}\n\n {interests.length > 0 && (\n <div className=\"mt-5\">\n <p className=\"mb-1.5 text-xs font-medium text-text-tertiary\">Interests</p>\n <div className=\"flex flex-wrap gap-1.5\">\n {interests.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {skills.length > 0 && (\n <div className=\"mt-4\">\n <p className=\"mb-1.5 text-xs font-medium text-text-tertiary\">Skills</p>\n <div className=\"flex flex-wrap gap-1.5\">\n {skills.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-[var(--color-accent-soft)] px-2.5 py-0.5 text-xs font-medium text-text-primary\"\n >\n {tag}\n </span>\n ))}\n </div>\n </div>\n )}\n\n {!discoverable && (\n <p className=\"mt-5 text-center text-xs text-text-tertiary\">\n Hidden from the member directory.\n </p>\n )}\n </div>\n </GlassCard>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cn.js","names":[],"sources":["../../../src/comradecircle/utils/cn.ts"],"sourcesContent":["import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":";;;AAGA,SAAgB,EAAG,GAAG,GAAsB;
|
|
1
|
+
{"version":3,"file":"cn.js","names":[],"sources":["../../../src/comradecircle/utils/cn.ts"],"sourcesContent":["import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":";;;AAGA,SAAgB,EAAG,GAAG,GAAsB;CAC1C,OAAO,EAAQ,EAAK,CAAM,CAAC;AAC7B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatters.js","names":[],"sources":["../../../src/comradecircle/utils/formatters.ts"],"sourcesContent":["/**\n * Formatting utilities for the ComradeCircle microfrontend\n * @module comradecircle/utils/formatters\n */\n\n/** Format a date string or Date into a human-readable date. */\nexport function formatDate(\n date: string | Date | null | undefined,\n options?: Partial<Intl.DateTimeFormatOptions>\n): string {\n if (!date) return '—';\n const d = typeof date === 'string' ? new Date(date) : date;\n if (Number.isNaN(d.getTime())) return '—';\n return new Intl.DateTimeFormat(undefined, {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n ...options,\n }).format(d);\n}\n\n/** Format a date-time into a date + time string. */\nexport function formatDateTime(\n date: string | Date | null | undefined,\n options?: Partial<Intl.DateTimeFormatOptions>\n): string {\n if (!date) return '—';\n const d = typeof date === 'string' ? new Date(date) : date;\n if (Number.isNaN(d.getTime())) return '—';\n return new Intl.DateTimeFormat(undefined, {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n ...options,\n }).format(d);\n}\n\n/** Format a date into a relative time string (e.g. \"2 hours ago\", \"in 3 days\"). */\nexport function formatRelativeTime(date: string | Date | null | undefined): string {\n if (!date) return '—';\n const d = typeof date === 'string' ? new Date(date) : date;\n if (Number.isNaN(d.getTime())) return '—';\n\n const now = new Date();\n const diffMs = d.getTime() - now.getTime();\n const future = diffMs >= 0;\n const absSeconds = Math.floor(Math.abs(diffMs) / 1000);\n const minutes = Math.floor(absSeconds / 60);\n const hours = Math.floor(minutes / 60);\n const days = Math.floor(hours / 24);\n const weeks = Math.floor(days / 7);\n const months = Math.floor(days / 30);\n const years = Math.floor(days / 365);\n\n const phrase = (value: number, unit: string): string => {\n const label = `${value} ${unit}${value !== 1 ? 's' : ''}`;\n return future ? `in ${label}` : `${label} ago`;\n };\n\n if (absSeconds < 60) return future ? 'soon' : 'just now';\n if (minutes < 60) return phrase(minutes, 'min');\n if (hours < 24) return phrase(hours, 'hour');\n if (days < 7) return phrase(days, 'day');\n if (weeks < 4) return phrase(weeks, 'week');\n if (months < 12) return phrase(months, 'month');\n return phrase(years, 'year');\n}\n\n/** Format a number with locale grouping. */\nexport function formatNumber(\n value: number | null | undefined,\n options?: Partial<Intl.NumberFormatOptions>\n): string {\n if (value == null || Number.isNaN(value)) return '—';\n return new Intl.NumberFormat(undefined, options).format(value);\n}\n\n/** Format a quantity with an optional unit suffix (e.g. \"1,200 kg\"). */\nexport function formatQuantity(value: number | null | undefined, unit?: string | null): string {\n if (value == null || Number.isNaN(value)) return '—';\n const num = formatNumber(value, { maximumFractionDigits: 2 });\n return unit ? `${num} ${unit}` : num;\n}\n\n/** Format a percentage (0–100) into a rounded \"%\" string. */\nexport function formatPercent(value: number | null | undefined, fractionDigits = 0): string {\n if (value == null || Number.isNaN(value)) return '—';\n return `${value.toFixed(fractionDigits)}%`;\n}\n\n/**\n * Format a mass of CO2e (kg) into a compact, human-scaled string:\n * < 1,000 kg → \"kg CO₂e\"; ≥ 1,000 kg → \"t CO₂e\" (tonnes).\n */\nexport function formatCo2e(kg: number | null | undefined): string {\n if (kg == null || Number.isNaN(kg)) return '—';\n if (Math.abs(kg) >= 1000) {\n return `${formatNumber(kg / 1000, { maximumFractionDigits: 2 })} t CO₂e`;\n }\n return `${formatNumber(kg, { maximumFractionDigits: 1 })} kg CO₂e`;\n}\n\n/** Format coordinates into a compact \"lat, lng\" string. */\nexport function formatCoords(\n lat: number | null | undefined,\n lng: number | null | undefined\n): string {\n if (lat == null || lng == null) return '—';\n return `${lat.toFixed(4)}, ${lng.toFixed(4)}`;\n}\n\n/** Turn an ENUM_LIKE_TOKEN into a human \"Enum Like Token\" label. */\nexport function formatEnumLabel(value: string | null | undefined): string {\n if (!value) return '—';\n return value\n .toLowerCase()\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (char) => char.toUpperCase());\n}\n"],"mappings":";AAMA,SAAgB,EACd,GACA,GACQ;
|
|
1
|
+
{"version":3,"file":"formatters.js","names":[],"sources":["../../../src/comradecircle/utils/formatters.ts"],"sourcesContent":["/**\n * Formatting utilities for the ComradeCircle microfrontend\n * @module comradecircle/utils/formatters\n */\n\n/** Format a date string or Date into a human-readable date. */\nexport function formatDate(\n date: string | Date | null | undefined,\n options?: Partial<Intl.DateTimeFormatOptions>\n): string {\n if (!date) return '—';\n const d = typeof date === 'string' ? new Date(date) : date;\n if (Number.isNaN(d.getTime())) return '—';\n return new Intl.DateTimeFormat(undefined, {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n ...options,\n }).format(d);\n}\n\n/** Format a date-time into a date + time string. */\nexport function formatDateTime(\n date: string | Date | null | undefined,\n options?: Partial<Intl.DateTimeFormatOptions>\n): string {\n if (!date) return '—';\n const d = typeof date === 'string' ? new Date(date) : date;\n if (Number.isNaN(d.getTime())) return '—';\n return new Intl.DateTimeFormat(undefined, {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n ...options,\n }).format(d);\n}\n\n/** Format a date into a relative time string (e.g. \"2 hours ago\", \"in 3 days\"). */\nexport function formatRelativeTime(date: string | Date | null | undefined): string {\n if (!date) return '—';\n const d = typeof date === 'string' ? new Date(date) : date;\n if (Number.isNaN(d.getTime())) return '—';\n\n const now = new Date();\n const diffMs = d.getTime() - now.getTime();\n const future = diffMs >= 0;\n const absSeconds = Math.floor(Math.abs(diffMs) / 1000);\n const minutes = Math.floor(absSeconds / 60);\n const hours = Math.floor(minutes / 60);\n const days = Math.floor(hours / 24);\n const weeks = Math.floor(days / 7);\n const months = Math.floor(days / 30);\n const years = Math.floor(days / 365);\n\n const phrase = (value: number, unit: string): string => {\n const label = `${value} ${unit}${value !== 1 ? 's' : ''}`;\n return future ? `in ${label}` : `${label} ago`;\n };\n\n if (absSeconds < 60) return future ? 'soon' : 'just now';\n if (minutes < 60) return phrase(minutes, 'min');\n if (hours < 24) return phrase(hours, 'hour');\n if (days < 7) return phrase(days, 'day');\n if (weeks < 4) return phrase(weeks, 'week');\n if (months < 12) return phrase(months, 'month');\n return phrase(years, 'year');\n}\n\n/** Format a number with locale grouping. */\nexport function formatNumber(\n value: number | null | undefined,\n options?: Partial<Intl.NumberFormatOptions>\n): string {\n if (value == null || Number.isNaN(value)) return '—';\n return new Intl.NumberFormat(undefined, options).format(value);\n}\n\n/** Format a quantity with an optional unit suffix (e.g. \"1,200 kg\"). */\nexport function formatQuantity(value: number | null | undefined, unit?: string | null): string {\n if (value == null || Number.isNaN(value)) return '—';\n const num = formatNumber(value, { maximumFractionDigits: 2 });\n return unit ? `${num} ${unit}` : num;\n}\n\n/** Format a percentage (0–100) into a rounded \"%\" string. */\nexport function formatPercent(value: number | null | undefined, fractionDigits = 0): string {\n if (value == null || Number.isNaN(value)) return '—';\n return `${value.toFixed(fractionDigits)}%`;\n}\n\n/**\n * Format a mass of CO2e (kg) into a compact, human-scaled string:\n * < 1,000 kg → \"kg CO₂e\"; ≥ 1,000 kg → \"t CO₂e\" (tonnes).\n */\nexport function formatCo2e(kg: number | null | undefined): string {\n if (kg == null || Number.isNaN(kg)) return '—';\n if (Math.abs(kg) >= 1000) {\n return `${formatNumber(kg / 1000, { maximumFractionDigits: 2 })} t CO₂e`;\n }\n return `${formatNumber(kg, { maximumFractionDigits: 1 })} kg CO₂e`;\n}\n\n/** Format coordinates into a compact \"lat, lng\" string. */\nexport function formatCoords(\n lat: number | null | undefined,\n lng: number | null | undefined\n): string {\n if (lat == null || lng == null) return '—';\n return `${lat.toFixed(4)}, ${lng.toFixed(4)}`;\n}\n\n/** Turn an ENUM_LIKE_TOKEN into a human \"Enum Like Token\" label. */\nexport function formatEnumLabel(value: string | null | undefined): string {\n if (!value) return '—';\n return value\n .toLowerCase()\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (char) => char.toUpperCase());\n}\n"],"mappings":";AAMA,SAAgB,EACd,GACA,GACQ;CACR,IAAI,CAAC,GAAM,OAAO;CAClB,IAAM,IAAI,OAAO,KAAS,WAAW,IAAI,KAAK,CAAI,IAAI;CAEtD,OADI,OAAO,MAAM,EAAE,QAAQ,CAAC,IAAU,MAC/B,IAAI,KAAK,eAAe,KAAA,GAAW;EACxC,KAAK;EACL,OAAO;EACP,MAAM;EACN,GAAG;CACL,CAAC,EAAE,OAAO,CAAC;AACb;AAGA,SAAgB,EACd,GACA,GACQ;CACR,IAAI,CAAC,GAAM,OAAO;CAClB,IAAM,IAAI,OAAO,KAAS,WAAW,IAAI,KAAK,CAAI,IAAI;CAEtD,OADI,OAAO,MAAM,EAAE,QAAQ,CAAC,IAAU,MAC/B,IAAI,KAAK,eAAe,KAAA,GAAW;EACxC,KAAK;EACL,OAAO;EACP,MAAM;EACN,MAAM;EACN,QAAQ;EACR,GAAG;CACL,CAAC,EAAE,OAAO,CAAC;AACb;AAGA,SAAgB,EAAmB,GAAgD;CACjF,IAAI,CAAC,GAAM,OAAO;CAClB,IAAM,IAAI,OAAO,KAAS,WAAW,IAAI,KAAK,CAAI,IAAI;CACtD,IAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,GAAG,OAAO;CAEtC,IAAM,oBAAM,IAAI,KAAK,GACf,IAAS,EAAE,QAAQ,IAAI,EAAI,QAAQ,GACnC,IAAS,KAAU,GACnB,IAAa,KAAK,MAAM,KAAK,IAAI,CAAM,IAAI,GAAI,GAC/C,IAAU,KAAK,MAAM,IAAa,EAAE,GACpC,IAAQ,KAAK,MAAM,IAAU,EAAE,GAC/B,IAAO,KAAK,MAAM,IAAQ,EAAE,GAC5B,IAAQ,KAAK,MAAM,IAAO,CAAC,GAC3B,IAAS,KAAK,MAAM,IAAO,EAAE,GAC7B,IAAQ,KAAK,MAAM,IAAO,GAAG,GAE7B,KAAU,GAAe,MAAyB;EACtD,IAAM,IAAQ,GAAG,EAAM,GAAG,IAAO,MAAU,IAAU,KAAN;EAC/C,OAAO,IAAS,MAAM,MAAU,GAAG,EAAM;CAC3C;CAQA,OANI,IAAa,KAAW,IAAS,SAAS,aAC1C,IAAU,KAAW,EAAO,GAAS,KAAK,IAC1C,IAAQ,KAAW,EAAO,GAAO,MAAM,IACvC,IAAO,IAAU,EAAO,GAAM,KAAK,IACnC,IAAQ,IAAU,EAAO,GAAO,MAAM,IACtC,IAAS,KAAW,EAAO,GAAQ,OAAO,IACvC,EAAO,GAAO,MAAM;AAC7B;AAGA,SAAgB,EACd,GACA,GACQ;CAER,OADI,KAAS,QAAQ,OAAO,MAAM,CAAK,IAAU,MAC1C,IAAI,KAAK,aAAa,KAAA,GAAW,CAAO,EAAE,OAAO,CAAK;AAC/D;AAGA,SAAgB,EAAe,GAAkC,GAA8B;CAC7F,IAAI,KAAS,QAAQ,OAAO,MAAM,CAAK,GAAG,OAAO;CACjD,IAAM,IAAM,EAAa,GAAO,EAAE,uBAAuB,EAAE,CAAC;CAC5D,OAAO,IAAO,GAAG,EAAI,GAAG,MAAS;AACnC;AAGA,SAAgB,EAAc,GAAkC,IAAiB,GAAW;CAE1F,OADI,KAAS,QAAQ,OAAO,MAAM,CAAK,IAAU,MAC1C,GAAG,EAAM,QAAQ,CAAc,EAAE;AAC1C;AAwBA,SAAgB,EAAgB,GAA0C;CAExE,OADK,IACE,EACJ,YAAY,EACZ,QAAQ,MAAM,GAAG,EACjB,QAAQ,UAAU,MAAS,EAAK,YAAY,CAAC,IAJ7B;AAKrB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation.js","names":[],"sources":["../../../src/comradecircle/utils/navigation.ts"],"sourcesContent":["/**\n * Navigation utilities for the ComradeCircle microfrontend\n * @module comradecircle/utils\n */\n\n/**\n * Join basePath with a relative path, correctly handling slashes.\n *\n * @example\n * joinPath('/comradecircle', 'sites') // => '/comradecircle/sites'\n * joinPath('/comradecircle/', 'sites') // => '/comradecircle/sites'\n * joinPath('/comradecircle', '') // => '/comradecircle'\n */\nexport function joinPath(basePath: string, relativePath: string): string {\n const cleanBase = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;\n if (!relativePath || relativePath === '') {\n return cleanBase || '/';\n }\n const cleanPath = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;\n return cleanBase + cleanPath;\n}\n"],"mappings":";AAaA,SAAgB,EAAS,GAAkB,GAA8B;CACvE,IAAM,IAAY,EAAS,SAAS,IAAI,
|
|
1
|
+
{"version":3,"file":"navigation.js","names":[],"sources":["../../../src/comradecircle/utils/navigation.ts"],"sourcesContent":["/**\n * Navigation utilities for the ComradeCircle microfrontend\n * @module comradecircle/utils\n */\n\n/**\n * Join basePath with a relative path, correctly handling slashes.\n *\n * @example\n * joinPath('/comradecircle', 'sites') // => '/comradecircle/sites'\n * joinPath('/comradecircle/', 'sites') // => '/comradecircle/sites'\n * joinPath('/comradecircle', '') // => '/comradecircle'\n */\nexport function joinPath(basePath: string, relativePath: string): string {\n const cleanBase = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;\n if (!relativePath || relativePath === '') {\n return cleanBase || '/';\n }\n const cleanPath = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;\n return cleanBase + cleanPath;\n}\n"],"mappings":";AAaA,SAAgB,EAAS,GAAkB,GAA8B;CACvE,IAAM,IAAY,EAAS,SAAS,GAAG,IAAI,EAAS,MAAM,GAAG,EAAE,IAAI;CAKnE,OAJI,CAAC,KAAgB,MAAiB,KAC7B,KAAa,MAGf,KADW,EAAa,WAAW,GAAG,IAAI,IAAe,IAAI;AAEtE"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page context gathering — captures the user's current browser context to be
|
|
3
|
+
* sent alongside every assistant prompt for better AI awareness (BOFF-3707).
|
|
4
|
+
*
|
|
5
|
+
* Modeled on microfe-vibecontrols/src/utils/pageContext.ts.
|
|
6
|
+
*/
|
|
7
|
+
interface ConversationContextShape {
|
|
8
|
+
entityType?: string;
|
|
9
|
+
entityId?: string;
|
|
10
|
+
activeTab?: string;
|
|
11
|
+
metadata?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface PageContext {
|
|
14
|
+
currentUrl: string;
|
|
15
|
+
currentPage: string;
|
|
16
|
+
entityType?: string;
|
|
17
|
+
entityId?: string;
|
|
18
|
+
platform: string;
|
|
19
|
+
version: string;
|
|
20
|
+
viewportSize: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function gatherPageContext(): PageContext;
|
|
23
|
+
/**
|
|
24
|
+
* Route -> entity table. `/comradecircle/*` is the app-shell mount point;
|
|
25
|
+
* paths below are relative to it (see ComradeCircleRoutes.tsx).
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveComradeCircleConversationContext(pathname: string, search: string): ConversationContextShape;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=pageContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pageContext.d.ts","sourceRoot":"","sources":["../../../src/comradecircle/utils/pageContext.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,UAAU,wBAAwB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,WAAW;IAG1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IAGpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,iBAAiB,IAAI,WAAW,CAkB/C;AAMD;;;GAGG;AACH,wBAAgB,uCAAuC,CACrD,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,wBAAwB,CAgD1B"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/comradecircle/utils/pageContext.ts
|
|
2
|
+
function e() {
|
|
3
|
+
let e = window.location.href, t = window.location.pathname.split("/").filter(Boolean).slice(0, 3).join("/") || "home", r = n(window.location.pathname, window.location.search);
|
|
4
|
+
return {
|
|
5
|
+
currentUrl: e,
|
|
6
|
+
currentPage: t,
|
|
7
|
+
entityType: r.entityType,
|
|
8
|
+
entityId: r.entityId,
|
|
9
|
+
platform: "comradecircle",
|
|
10
|
+
version: "2026.720.1",
|
|
11
|
+
viewportSize: `${window.innerWidth}x${window.innerHeight}`
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function t(e, t) {
|
|
15
|
+
return t.match(e);
|
|
16
|
+
}
|
|
17
|
+
function n(e, n) {
|
|
18
|
+
let r = new URLSearchParams(n);
|
|
19
|
+
for (let [n, r] of [
|
|
20
|
+
[/^\/comradecircle\/circles\/([^/]+)$/, "COMRADECIRCLE_CIRCLE"],
|
|
21
|
+
[/^\/comradecircle\/events\/([^/]+)$/, "COMRADECIRCLE_EVENT"],
|
|
22
|
+
[/^\/comradecircle\/members\/([^/]+)$/, "COMRADECIRCLE_MEMBER"]
|
|
23
|
+
]) {
|
|
24
|
+
let i = t(n, e);
|
|
25
|
+
if (i) return {
|
|
26
|
+
entityType: r,
|
|
27
|
+
entityId: i[1],
|
|
28
|
+
metadata: { routePattern: n.source }
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (/^\/comradecircle\/circles$/.test(e)) return { entityType: "COMRADECIRCLE_CIRCLE_LIST" };
|
|
32
|
+
if (/^\/comradecircle\/events$/.test(e)) return { entityType: "COMRADECIRCLE_EVENT_LIST" };
|
|
33
|
+
if (/^\/comradecircle\/members$/.test(e)) return { entityType: "COMRADECIRCLE_MEMBER_LIST" };
|
|
34
|
+
if (/^\/comradecircle\/profile$/.test(e)) return { entityType: "COMRADECIRCLE_MY_PROFILE" };
|
|
35
|
+
if (/^\/comradecircle\/?$/.test(e)) return { entityType: "COMRADECIRCLE_FEED" };
|
|
36
|
+
let i = r.get("tab") ?? (() => {
|
|
37
|
+
let t = e.split("/").filter(Boolean);
|
|
38
|
+
return t.length > 2 ? t[t.length - 1] : void 0;
|
|
39
|
+
})();
|
|
40
|
+
return {
|
|
41
|
+
activeTab: i ?? void 0,
|
|
42
|
+
metadata: i ? { activeTab: i } : void 0
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { e as gatherPageContext };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=pageContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pageContext.js","names":[],"sources":["../../../src/comradecircle/utils/pageContext.ts"],"sourcesContent":["/**\n * Page context gathering — captures the user's current browser context to be\n * sent alongside every assistant prompt for better AI awareness (BOFF-3707).\n *\n * Modeled on microfe-vibecontrols/src/utils/pageContext.ts.\n */\n\ninterface ConversationContextShape {\n entityType?: string;\n entityId?: string;\n activeTab?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface PageContext {\n // `currentUrl` (NOT `activeUrl`) — must match the agent's allowed-context-key\n // contract, otherwise the agent filters it out.\n currentUrl: string;\n currentPage: string;\n // The entity the user is currently looking at, resolved from the route. Lets\n // the assistant act on \"this / current / here\" without asking for an id.\n entityType?: string;\n entityId?: string;\n platform: string;\n version: string;\n viewportSize: string;\n}\n\nexport function gatherPageContext(): PageContext {\n const url = window.location.href;\n const pathSegments = window.location.pathname.split('/').filter(Boolean);\n const currentPage = pathSegments.slice(0, 3).join('/') || 'home';\n const entity = resolveComradeCircleConversationContext(\n window.location.pathname,\n window.location.search\n );\n\n return {\n currentUrl: url,\n currentPage,\n entityType: entity.entityType,\n entityId: entity.entityId,\n platform: 'comradecircle',\n version: import.meta.env.VITE_APP_VERSION ?? '2026.720.1',\n viewportSize: `${window.innerWidth}x${window.innerHeight}`,\n };\n}\n\nfunction matchPath(pattern: RegExp, pathname: string): RegExpMatchArray | null {\n return pathname.match(pattern);\n}\n\n/**\n * Route -> entity table. `/comradecircle/*` is the app-shell mount point;\n * paths below are relative to it (see ComradeCircleRoutes.tsx).\n */\nexport function resolveComradeCircleConversationContext(\n pathname: string,\n search: string\n): ConversationContextShape {\n const params = new URLSearchParams(search);\n const directMatches: Array<[RegExp, string]> = [\n [/^\\/comradecircle\\/circles\\/([^/]+)$/, 'COMRADECIRCLE_CIRCLE'],\n [/^\\/comradecircle\\/events\\/([^/]+)$/, 'COMRADECIRCLE_EVENT'],\n [/^\\/comradecircle\\/members\\/([^/]+)$/, 'COMRADECIRCLE_MEMBER'],\n ];\n\n for (const [pattern, entityType] of directMatches) {\n const match = matchPath(pattern, pathname);\n if (match) {\n return {\n entityType,\n entityId: match[1],\n metadata: {\n routePattern: pattern.source,\n },\n };\n }\n }\n\n if (/^\\/comradecircle\\/circles$/.test(pathname)) {\n return { entityType: 'COMRADECIRCLE_CIRCLE_LIST' };\n }\n if (/^\\/comradecircle\\/events$/.test(pathname)) {\n return { entityType: 'COMRADECIRCLE_EVENT_LIST' };\n }\n if (/^\\/comradecircle\\/members$/.test(pathname)) {\n return { entityType: 'COMRADECIRCLE_MEMBER_LIST' };\n }\n if (/^\\/comradecircle\\/profile$/.test(pathname)) {\n return { entityType: 'COMRADECIRCLE_MY_PROFILE' };\n }\n if (/^\\/comradecircle\\/?$/.test(pathname)) {\n return { entityType: 'COMRADECIRCLE_FEED' };\n }\n\n const activeTab =\n params.get('tab') ??\n (() => {\n const segments = pathname.split('/').filter(Boolean);\n return segments.length > 2 ? segments[segments.length - 1] : undefined;\n })();\n\n return {\n activeTab: activeTab ?? undefined,\n metadata: activeTab ? { activeTab } : undefined,\n };\n}\n"],"mappings":";AA4BA,SAAgB,IAAiC;CAC/C,IAAM,IAAM,OAAO,SAAS,MAEtB,IADe,OAAO,SAAS,SAAS,MAAM,GAAG,EAAE,OAAO,OAC5C,EAAa,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,QACpD,IAAS,EACb,OAAO,SAAS,UAChB,OAAO,SAAS,MAClB;CAEA,OAAO;EACL,YAAY;EACZ;EACA,YAAY,EAAO;EACnB,UAAU,EAAO;EACjB,UAAU;EACV,SAA6C;EAC7C,cAAc,GAAG,OAAO,WAAW,GAAG,OAAO;CAC/C;AACF;AAEA,SAAS,EAAU,GAAiB,GAA2C;CAC7E,OAAO,EAAS,MAAM,CAAO;AAC/B;AAMA,SAAgB,EACd,GACA,GAC0B;CAC1B,IAAM,IAAS,IAAI,gBAAgB,CAAM;CAOzC,KAAK,IAAM,CAAC,GAAS,MAAe;EALlC,CAAC,uCAAuC,sBAAsB;EAC9D,CAAC,sCAAsC,qBAAqB;EAC5D,CAAC,uCAAuC,sBAAsB;CAG5B,GAAe;EACjD,IAAM,IAAQ,EAAU,GAAS,CAAQ;EACzC,IAAI,GACF,OAAO;GACL;GACA,UAAU,EAAM;GAChB,UAAU,EACR,cAAc,EAAQ,OACxB;EACF;CAEJ;CAEA,IAAI,6BAA6B,KAAK,CAAQ,GAC5C,OAAO,EAAE,YAAY,4BAA4B;CAEnD,IAAI,4BAA4B,KAAK,CAAQ,GAC3C,OAAO,EAAE,YAAY,2BAA2B;CAElD,IAAI,6BAA6B,KAAK,CAAQ,GAC5C,OAAO,EAAE,YAAY,4BAA4B;CAEnD,IAAI,6BAA6B,KAAK,CAAQ,GAC5C,OAAO,EAAE,YAAY,2BAA2B;CAElD,IAAI,uBAAuB,KAAK,CAAQ,GACtC,OAAO,EAAE,YAAY,qBAAqB;CAG5C,IAAM,IACJ,EAAO,IAAI,KAAK,YACT;EACL,IAAM,IAAW,EAAS,MAAM,GAAG,EAAE,OAAO,OAAO;EACnD,OAAO,EAAS,SAAS,IAAI,EAAS,EAAS,SAAS,KAAK,KAAA;CAC/D,GAAG;CAEL,OAAO;EACL,WAAW,KAAa,KAAA;EACxB,UAAU,IAAY,EAAE,aAAU,IAAI,KAAA;CACxC;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slug.js","names":[],"sources":["../../../src/comradecircle/utils/slug.ts"],"sourcesContent":["/**\n * slugify — turn a display name into a URL/handle-safe slug.\n * @module comradecircle/utils\n */\nexport function slugify(value: string): string {\n return value\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 48);\n}\n"],"mappings":";AAIA,SAAgB,EAAQ,GAAuB;
|
|
1
|
+
{"version":3,"file":"slug.js","names":[],"sources":["../../../src/comradecircle/utils/slug.ts"],"sourcesContent":["/**\n * slugify — turn a display name into a URL/handle-safe slug.\n * @module comradecircle/utils\n */\nexport function slugify(value: string): string {\n return value\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 48);\n}\n"],"mappings":";AAIA,SAAgB,EAAQ,GAAuB;CAC7C,OAAO,EACJ,YAAY,EACZ,KAAK,EACL,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE,EACtB,MAAM,GAAG,EAAE;AAChB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clsx.js","names":[],"sources":["../../../../node_modules/clsx/dist/clsx.mjs"],"sourcesContent":["function r(e){var t,f,n=\"\";if(\"string\"==typeof e||\"number\"==typeof e)n+=e;else if(\"object\"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(f=r(e[t]))&&(n&&(n+=\" \"),n+=f)}else for(f in e)e[f]&&(n&&(n+=\" \"),n+=f);return n}export function clsx(){for(var e,t,f=0,n=\"\",o=arguments.length;f<o;f++)(e=arguments[f])&&(t=r(e))&&(n&&(n+=\" \"),n+=t);return n}export default clsx;"],"x_google_ignoreList":[0],"mappings":";AAAA,SAAS,EAAE,GAAE;CAAC,IAAI,GAAE,GAAE,IAAE;
|
|
1
|
+
{"version":3,"file":"clsx.js","names":[],"sources":["../../../../node_modules/clsx/dist/clsx.mjs"],"sourcesContent":["function r(e){var t,f,n=\"\";if(\"string\"==typeof e||\"number\"==typeof e)n+=e;else if(\"object\"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(f=r(e[t]))&&(n&&(n+=\" \"),n+=f)}else for(f in e)e[f]&&(n&&(n+=\" \"),n+=f);return n}export function clsx(){for(var e,t,f=0,n=\"\",o=arguments.length;f<o;f++)(e=arguments[f])&&(t=r(e))&&(n&&(n+=\" \"),n+=t);return n}export default clsx;"],"x_google_ignoreList":[0],"mappings":";AAAA,SAAS,EAAE,GAAE;CAAC,IAAI,GAAE,GAAE,IAAE;CAAG,IAAa,OAAO,KAAjB,YAA8B,OAAO,KAAjB,UAAmB,KAAG;MAAO,IAAa,OAAO,KAAjB,UAAmB,IAAG,MAAM,QAAQ,CAAC,GAAE;EAAC,IAAI,IAAE,EAAE;EAAO,KAAI,IAAE,GAAE,IAAE,GAAE,KAAI,EAAE,OAAK,IAAE,EAAE,EAAE,EAAE,OAAK,MAAI,KAAG,MAAK,KAAG;CAAE,OAAM,KAAI,KAAK,GAAE,EAAE,OAAK,MAAI,KAAG,MAAK,KAAG;CAAG,OAAO;AAAC;AAAC,SAAgB,IAAM;CAAC,KAAI,IAAI,GAAE,GAAE,IAAE,GAAE,IAAE,IAAG,IAAE,UAAU,QAAO,IAAE,GAAE,KAAI,CAAC,IAAE,UAAU,QAAM,IAAE,EAAE,CAAC,OAAK,MAAI,KAAG,MAAK,KAAG;CAAG,OAAO;AAAC"}
|