@flanksource/clicky-ui 0.3.33 → 0.3.34
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/comments/CommentCard.cjs +9 -8
- package/dist/comments/CommentCard.cjs.map +1 -1
- package/dist/comments/CommentCard.d.ts.map +1 -1
- package/dist/comments/CommentCard.js +2 -1
- package/dist/comments/CommentCard.js.map +1 -1
- package/dist/comments/CommentCardParts.cjs +2 -1
- package/dist/comments/CommentCardParts.cjs.map +1 -1
- package/dist/comments/CommentCardParts.d.ts.map +1 -1
- package/dist/comments/CommentCardParts.js +2 -1
- package/dist/comments/CommentCardParts.js.map +1 -1
- package/dist/comments/CommentSidePanel.cjs +3 -2
- package/dist/comments/CommentSidePanel.cjs.map +1 -1
- package/dist/comments/CommentSidePanel.d.ts.map +1 -1
- package/dist/comments/CommentSidePanel.js +2 -1
- package/dist/comments/CommentSidePanel.js.map +1 -1
- package/dist/comments/comment-utils.cjs +0 -27
- package/dist/comments/comment-utils.cjs.map +1 -1
- package/dist/comments/comment-utils.d.ts +1 -7
- package/dist/comments/comment-utils.d.ts.map +1 -1
- package/dist/comments/comment-utils.js +0 -27
- package/dist/comments/comment-utils.js.map +1 -1
- package/dist/comments.cjs +4 -3
- package/dist/comments.cjs.map +1 -1
- package/dist/comments.d.ts +1 -0
- package/dist/comments.d.ts.map +1 -1
- package/dist/comments.js +2 -1
- package/dist/comments.js.map +1 -1
- package/dist/components/RangeSlider.cjs +15 -1
- package/dist/components/RangeSlider.cjs.map +1 -1
- package/dist/components/RangeSlider.d.ts.map +1 -1
- package/dist/components/RangeSlider.js +15 -1
- package/dist/components/RangeSlider.js.map +1 -1
- package/dist/data/badge-variants.d.ts +2 -2
- package/dist/data/version-info.cjs +3 -3
- package/dist/data/version-info.js +3 -3
- package/dist/index.cjs +4 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/comment-stage.cjs +31 -0
- package/dist/lib/comment-stage.cjs.map +1 -0
- package/dist/lib/comment-stage.d.ts +8 -0
- package/dist/lib/comment-stage.d.ts.map +1 -0
- package/dist/lib/comment-stage.js +31 -0
- package/dist/lib/comment-stage.js.map +1 -0
- package/dist/utils.cjs +4 -0
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +4 -0
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommentSidePanel.js","sources":["../../src/comments/CommentSidePanel.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState, type MouseEvent } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { UiComment } from \"../icons\";\nimport { CommentThread } from \"./CommentThread\";\nimport { CommentThreadList } from \"./CommentThreadList\";\nimport {\n useCommentContextOptional,\n type CommentContextValue,\n} from \"./comment-context\";\nimport {\n buildThreadListHandlers,\n getRoots,\n resolveCommentStage,\n selectAnchorThreads,\n selectCommentThreadsByStage,\n sortReplies,\n buildReplyMap,\n} from \"./comment-utils\";\nimport {\n DOCUMENT_ANCHOR,\n type Comment,\n type CommentAnchor,\n type CommentStatusStage,\n} from \"./comment-types\";\n\nexport type CommentSidePanelProps = {\n /** Explicit label per anchor key. */\n anchorLabels?: Record<CommentAnchor, string>;\n /** Fallback label formatter for anchors without an explicit label. */\n formatAnchorLabel?: (anchor: CommentAnchor) => string;\n compact?: boolean;\n /** Position the focused thread beside its registered content anchor. */\n focusedAlignment?: \"flow\" | \"anchor\";\n className?: string;\n /** Serialize one whole thread for Copy and its maximized Markdown tab. */\n threadToMarkdown?: (thread: readonly Comment[]) => string;\n};\n\nfunction defaultAnchorLabel(anchor: CommentAnchor): string {\n if (anchor === DOCUMENT_ANCHOR) return \"General\";\n return anchor\n .replace(/\\[\\d+\\]/g, \"\")\n .replaceAll(\".\", \" › \")\n .replaceAll(\"_\", \" \")\n .trim();\n}\n\nfunction useAnchorLabel(props: CommentSidePanelProps) {\n return (anchor: CommentAnchor): string =>\n props.anchorLabels?.[anchor] ??\n props.formatAnchorLabel?.(anchor) ??\n defaultAnchorLabel(anchor);\n}\n\nfunction LocationMeta({ label }: { label: string }) {\n return (\n <span\n data-testid=\"comment-location-meta\"\n className=\"inline-flex items-center rounded-full bg-muted px-2 py-1 text-[10px] font-medium text-muted-foreground\"\n >\n {label}\n </span>\n );\n}\n\nfunction RailToggle({\n active,\n onClick,\n children,\n testId,\n}: {\n active?: boolean;\n onClick: () => void;\n children: React.ReactNode;\n testId?: string;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n data-testid={testId}\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium shadow-sm transition-colors\",\n active\n ? \"border-primary bg-primary text-primary-foreground\"\n : \"border-border bg-background text-muted-foreground hover:text-foreground\",\n )}\n >\n <Icon icon={UiComment} className=\"text-xs\" />\n {children}\n </button>\n );\n}\n\nfunction orderedAnchors(ctx: CommentContextValue): string[] {\n return Object.keys(ctx.commentMeta).sort((a, b) => {\n if (a === DOCUMENT_ANCHOR) return -1;\n if (b === DOCUMENT_ANCHOR) return 1;\n const at = ctx.getAnchorTop(a);\n const bt = ctx.getAnchorTop(b);\n if (at == null && bt == null) return a.localeCompare(b);\n if (at == null) return 1;\n if (bt == null) return -1;\n if (at !== bt) return at - bt;\n return a.localeCompare(b);\n });\n}\n\nfunction orderComments(\n comments: Comment[],\n anchorOrder: Map<string, number>,\n): Comment[] {\n const replyMap = buildReplyMap(comments);\n const roots = getRoots(comments).sort((a, b) => {\n const ai =\n anchorOrder.get(a.anchor ?? DOCUMENT_ANCHOR) ?? Number.MAX_SAFE_INTEGER;\n const bi =\n anchorOrder.get(b.anchor ?? DOCUMENT_ANCHOR) ?? Number.MAX_SAFE_INTEGER;\n if (ai !== bi) return ai - bi;\n if (a.createdAt !== b.createdAt)\n return a.createdAt.localeCompare(b.createdAt);\n return String(a.id).localeCompare(String(b.id));\n });\n return roots.flatMap((root) => [\n root,\n ...sortReplies(replyMap.get(root.id) ?? []),\n ]);\n}\n\nfunction AllComments({\n ctx,\n comments,\n label,\n emptyLabel,\n threadToMarkdown,\n}: {\n ctx: CommentContextValue;\n comments: Comment[];\n label: (a: CommentAnchor) => string;\n emptyLabel: string;\n threadToMarkdown?: (thread: readonly Comment[]) => string;\n}) {\n const ordered = useMemo(() => {\n const anchorOrder = new Map(orderedAnchors(ctx).map((a, i) => [a, i]));\n return orderComments(comments, anchorOrder);\n }, [ctx, comments]);\n const handlers = buildThreadListHandlers(ordered, ctx.config, ctx.callbacks);\n\n function activateThread(\n event: MouseEvent<HTMLDivElement>,\n anchor: CommentAnchor,\n ) {\n const target = event.target as Element;\n if (\n target.closest(\n 'button, a, input, textarea, select, [contenteditable=\"true\"]',\n )\n )\n return;\n const roleButton = target.closest('[role=\"button\"]');\n if (roleButton && roleButton !== target.closest(\"[data-comment-kind]\"))\n return;\n if (anchor !== DOCUMENT_ANCHOR) {\n const found = ctx.scrollToAnchor(anchor, {\n behavior: \"smooth\",\n block: \"start\",\n offset: 12,\n });\n if (!found) return;\n }\n ctx.focusAnchor(anchor);\n }\n\n if (ordered.length === 0) {\n return (\n <p className=\"rounded-lg bg-muted/40 px-3 py-2 text-xs text-muted-foreground\">\n {emptyLabel}\n </p>\n );\n }\n\n return (\n <div\n data-testid=\"comment-all-rail\"\n className=\"space-y-3 overflow-y-auto pr-1\"\n >\n <CommentThreadList\n comments={ordered}\n config={ctx.config}\n compact\n renderRootMeta={(c) => {\n const anchor = c.anchor ?? DOCUMENT_ANCHOR;\n const available =\n anchor === DOCUMENT_ANCHOR || ctx.getAnchorTop(anchor) != null;\n return (\n <LocationMeta\n label={`${label(anchor)}${available ? \"\" : \" · Unavailable\"}`}\n />\n );\n }}\n getThreadProps={(c) => ({\n \"data-testid\": \"comment-feed-item\",\n onMouseEnter: () =>\n ctx.setHighlightAnchor(c.anchor ?? DOCUMENT_ANCHOR),\n onMouseLeave: () => ctx.setHighlightAnchor(null),\n onClick: (event) =>\n activateThread(event, c.anchor ?? DOCUMENT_ANCHOR),\n })}\n {...handlers}\n {...(threadToMarkdown ? { threadToMarkdown } : {})}\n />\n </div>\n );\n}\n\nfunction FocusedComments({\n ctx,\n comments: visible,\n anchor,\n label,\n compact,\n threadToMarkdown,\n}: {\n ctx: CommentContextValue;\n comments: Comment[];\n anchor: CommentAnchor;\n label: string;\n compact?: boolean;\n threadToMarkdown?: (thread: readonly Comment[]) => string;\n}) {\n const comments = selectAnchorThreads(visible, anchor);\n const hasComments = comments.length > 0;\n return (\n <div className=\"space-y-3\" data-comment-anchor={anchor}>\n <LocationMeta\n label={\n anchor === DOCUMENT_ANCHOR\n ? \"Whole-page comment\"\n : `Attached to ${label}`\n }\n />\n <CommentThread\n comments={comments}\n config={ctx.config}\n anchor={anchor}\n compact={compact ?? false}\n autoFocusComposer={!hasComments}\n defaultExpanded={hasComments}\n composerPlaceholder={\n hasComments ? \"Add another top-level comment…\" : \"Add a comment…\"\n }\n {...ctx.callbacks}\n {...(threadToMarkdown ? { threadToMarkdown } : {})}\n />\n </div>\n );\n}\n\n/**\n * A controlled comment rail driven by {@link CommentProvider}. Shows a focused\n * thread for the active anchor, the full document feed in anchor order, or a\n * toggle when collapsed. Renders nothing outside a provider or when empty.\n */\nexport function CommentSidePanel(props: CommentSidePanelProps) {\n const ctx = useCommentContextOptional();\n const label = useAnchorLabel(props);\n const railRef = useRef<HTMLElement>(null);\n const focusedRef = useRef<HTMLDivElement>(null);\n const [focusedOffset, setFocusedOffset] = useState<number | null>(null);\n const [stage, setStage] = useState<CommentStatusStage>(\"active\");\n const focusedAnchor = ctx?.railMode === \"focused\" ? ctx.focusedAnchor : null;\n\n useEffect(() => {\n if (!ctx || props.focusedAlignment !== \"anchor\" || !focusedAnchor) {\n setFocusedOffset(null);\n return;\n }\n const content = ctx.contentRef.current;\n const rail = railRef.current;\n const focused = focusedRef.current;\n if (!content || !rail || !focused) return;\n\n const update = () => {\n const anchorTop = ctx.getAnchorTop(focusedAnchor);\n if (anchorTop == null) {\n setFocusedOffset(null);\n return;\n }\n const desiredTop = anchorTop + content.getBoundingClientRect().top;\n const currentOffset = Number.parseFloat(focused.style.top) || 0;\n const nextOffset =\n currentOffset + desiredTop - focused.getBoundingClientRect().top;\n setFocusedOffset(Math.max(0, nextOffset));\n };\n update();\n content.addEventListener(\"scroll\", update, { passive: true });\n window.addEventListener(\"resize\", update);\n const observer =\n typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(update);\n observer?.observe(content);\n observer?.observe(rail);\n return () => {\n content.removeEventListener(\"scroll\", update);\n window.removeEventListener(\"resize\", update);\n observer?.disconnect();\n };\n }, [ctx, focusedAnchor, props.focusedAlignment]);\n\n if (!ctx) return null;\n\n // Threads, not cards: a reply is part of its root, never a separate entry.\n const counts = getRoots(ctx.comments).reduce(\n (result, root) => {\n const rootStage = resolveCommentStage(ctx.config, root.status);\n if (rootStage) result[rootStage] += 1;\n return result;\n },\n { active: 0, resolved: 0, closed: 0 },\n );\n const visible = selectCommentThreadsByStage(ctx.comments, ctx.config, stage);\n if (\n ctx.railMode === \"closed\" &&\n counts.active + counts.resolved + counts.closed === 0\n )\n return null;\n\n function selectStage(next: CommentStatusStage) {\n setStage(next);\n if (ctx?.railMode !== \"all\") ctx?.openCommentList();\n }\n\n return (\n <aside\n ref={railRef}\n data-testid=\"comment-side-panel\"\n className={cn(\"relative w-[320px] space-y-3\", props.className)}\n >\n <div\n data-testid=\"comment-rail-header\"\n className=\"sticky top-0 z-20 flex items-center gap-2 bg-background py-2\"\n >\n {(counts.active > 0 || ctx.railMode === \"all\") && (\n <RailToggle\n active={ctx.railMode === \"all\" && stage === \"active\"}\n onClick={() => selectStage(\"active\")}\n testId=\"comment-open-all\"\n >\n {ctx.railMode === \"closed\"\n ? `Open comments (${counts.active})`\n : ctx.railMode === \"focused\"\n ? `All comments (${counts.active})`\n : `Open (${counts.active})`}\n </RailToggle>\n )}\n {counts.resolved > 0 && (\n <button\n type=\"button\"\n aria-pressed={ctx.railMode === \"all\" && stage === \"resolved\"}\n onClick={() => selectStage(\"resolved\")}\n className={cn(\n \"rounded-full border px-2 py-1 text-xs font-medium transition-colors\",\n ctx.railMode === \"all\" && stage === \"resolved\"\n ? \"border-primary bg-primary text-primary-foreground\"\n : \"border-border bg-background text-muted-foreground hover:text-foreground\",\n )}\n >\n Resolved ({counts.resolved})\n </button>\n )}\n {counts.closed > 0 && (\n <button\n type=\"button\"\n aria-pressed={ctx.railMode === \"all\" && stage === \"closed\"}\n onClick={() => selectStage(\"closed\")}\n className={cn(\n \"rounded-full border px-2 py-1 text-xs font-medium transition-colors\",\n ctx.railMode === \"all\" && stage === \"closed\"\n ? \"border-primary bg-primary text-primary-foreground\"\n : \"border-border bg-background text-muted-foreground hover:text-foreground\",\n )}\n >\n Closed ({counts.closed})\n </button>\n )}\n {ctx.railMode !== \"closed\" && (\n <button\n type=\"button\"\n onClick={ctx.closeRail}\n className=\"ml-auto rounded-md px-2 py-1 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground\"\n >\n {ctx.railMode === \"all\" ? \"Close\" : \"Hide\"}\n </button>\n )}\n </div>\n {ctx.railMode === \"all\" ? (\n <AllComments\n ctx={ctx}\n comments={visible}\n label={label}\n emptyLabel={\n stage === \"active\"\n ? \"No open comments.\"\n : stage === \"resolved\"\n ? \"No resolved comments.\"\n : \"No closed comments.\"\n }\n {...(props.threadToMarkdown\n ? { threadToMarkdown: props.threadToMarkdown }\n : {})}\n />\n ) : ctx.railMode === \"focused\" && ctx.focusedAnchor ? (\n <div\n ref={focusedRef}\n data-testid=\"comment-focused-rail\"\n className=\"relative space-y-3\"\n style={focusedOffset == null ? undefined : { top: focusedOffset }}\n >\n <FocusedComments\n ctx={ctx}\n comments={ctx.comments}\n anchor={ctx.focusedAnchor}\n label={label(ctx.focusedAnchor)}\n {...(props.compact !== undefined ? { compact: props.compact } : {})}\n {...(props.threadToMarkdown\n ? { threadToMarkdown: props.threadToMarkdown }\n : {})}\n />\n </div>\n ) : null}\n </aside>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CommentSidePanel.js","sources":["../../src/comments/CommentSidePanel.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState, type MouseEvent } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { UiComment } from \"../icons\";\nimport { CommentThread } from \"./CommentThread\";\nimport { CommentThreadList } from \"./CommentThreadList\";\nimport {\n useCommentContextOptional,\n type CommentContextValue,\n} from \"./comment-context\";\nimport {\n buildThreadListHandlers,\n getRoots,\n selectAnchorThreads,\n sortReplies,\n buildReplyMap,\n} from \"./comment-utils\";\nimport {\n resolveCommentStage,\n selectCommentThreadsByStage,\n} from \"../lib/comment-stage\";\nimport {\n DOCUMENT_ANCHOR,\n type Comment,\n type CommentAnchor,\n type CommentStatusStage,\n} from \"./comment-types\";\n\nexport type CommentSidePanelProps = {\n /** Explicit label per anchor key. */\n anchorLabels?: Record<CommentAnchor, string>;\n /** Fallback label formatter for anchors without an explicit label. */\n formatAnchorLabel?: (anchor: CommentAnchor) => string;\n compact?: boolean;\n /** Position the focused thread beside its registered content anchor. */\n focusedAlignment?: \"flow\" | \"anchor\";\n className?: string;\n /** Serialize one whole thread for Copy and its maximized Markdown tab. */\n threadToMarkdown?: (thread: readonly Comment[]) => string;\n};\n\nfunction defaultAnchorLabel(anchor: CommentAnchor): string {\n if (anchor === DOCUMENT_ANCHOR) return \"General\";\n return anchor\n .replace(/\\[\\d+\\]/g, \"\")\n .replaceAll(\".\", \" › \")\n .replaceAll(\"_\", \" \")\n .trim();\n}\n\nfunction useAnchorLabel(props: CommentSidePanelProps) {\n return (anchor: CommentAnchor): string =>\n props.anchorLabels?.[anchor] ??\n props.formatAnchorLabel?.(anchor) ??\n defaultAnchorLabel(anchor);\n}\n\nfunction LocationMeta({ label }: { label: string }) {\n return (\n <span\n data-testid=\"comment-location-meta\"\n className=\"inline-flex items-center rounded-full bg-muted px-2 py-1 text-[10px] font-medium text-muted-foreground\"\n >\n {label}\n </span>\n );\n}\n\nfunction RailToggle({\n active,\n onClick,\n children,\n testId,\n}: {\n active?: boolean;\n onClick: () => void;\n children: React.ReactNode;\n testId?: string;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n data-testid={testId}\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium shadow-sm transition-colors\",\n active\n ? \"border-primary bg-primary text-primary-foreground\"\n : \"border-border bg-background text-muted-foreground hover:text-foreground\",\n )}\n >\n <Icon icon={UiComment} className=\"text-xs\" />\n {children}\n </button>\n );\n}\n\nfunction orderedAnchors(ctx: CommentContextValue): string[] {\n return Object.keys(ctx.commentMeta).sort((a, b) => {\n if (a === DOCUMENT_ANCHOR) return -1;\n if (b === DOCUMENT_ANCHOR) return 1;\n const at = ctx.getAnchorTop(a);\n const bt = ctx.getAnchorTop(b);\n if (at == null && bt == null) return a.localeCompare(b);\n if (at == null) return 1;\n if (bt == null) return -1;\n if (at !== bt) return at - bt;\n return a.localeCompare(b);\n });\n}\n\nfunction orderComments(\n comments: Comment[],\n anchorOrder: Map<string, number>,\n): Comment[] {\n const replyMap = buildReplyMap(comments);\n const roots = getRoots(comments).sort((a, b) => {\n const ai =\n anchorOrder.get(a.anchor ?? DOCUMENT_ANCHOR) ?? Number.MAX_SAFE_INTEGER;\n const bi =\n anchorOrder.get(b.anchor ?? DOCUMENT_ANCHOR) ?? Number.MAX_SAFE_INTEGER;\n if (ai !== bi) return ai - bi;\n if (a.createdAt !== b.createdAt)\n return a.createdAt.localeCompare(b.createdAt);\n return String(a.id).localeCompare(String(b.id));\n });\n return roots.flatMap((root) => [\n root,\n ...sortReplies(replyMap.get(root.id) ?? []),\n ]);\n}\n\nfunction AllComments({\n ctx,\n comments,\n label,\n emptyLabel,\n threadToMarkdown,\n}: {\n ctx: CommentContextValue;\n comments: Comment[];\n label: (a: CommentAnchor) => string;\n emptyLabel: string;\n threadToMarkdown?: (thread: readonly Comment[]) => string;\n}) {\n const ordered = useMemo(() => {\n const anchorOrder = new Map(orderedAnchors(ctx).map((a, i) => [a, i]));\n return orderComments(comments, anchorOrder);\n }, [ctx, comments]);\n const handlers = buildThreadListHandlers(ordered, ctx.config, ctx.callbacks);\n\n function activateThread(\n event: MouseEvent<HTMLDivElement>,\n anchor: CommentAnchor,\n ) {\n const target = event.target as Element;\n if (\n target.closest(\n 'button, a, input, textarea, select, [contenteditable=\"true\"]',\n )\n )\n return;\n const roleButton = target.closest('[role=\"button\"]');\n if (roleButton && roleButton !== target.closest(\"[data-comment-kind]\"))\n return;\n if (anchor !== DOCUMENT_ANCHOR) {\n const found = ctx.scrollToAnchor(anchor, {\n behavior: \"smooth\",\n block: \"start\",\n offset: 12,\n });\n if (!found) return;\n }\n ctx.focusAnchor(anchor);\n }\n\n if (ordered.length === 0) {\n return (\n <p className=\"rounded-lg bg-muted/40 px-3 py-2 text-xs text-muted-foreground\">\n {emptyLabel}\n </p>\n );\n }\n\n return (\n <div\n data-testid=\"comment-all-rail\"\n className=\"space-y-3 overflow-y-auto pr-1\"\n >\n <CommentThreadList\n comments={ordered}\n config={ctx.config}\n compact\n renderRootMeta={(c) => {\n const anchor = c.anchor ?? DOCUMENT_ANCHOR;\n const available =\n anchor === DOCUMENT_ANCHOR || ctx.getAnchorTop(anchor) != null;\n return (\n <LocationMeta\n label={`${label(anchor)}${available ? \"\" : \" · Unavailable\"}`}\n />\n );\n }}\n getThreadProps={(c) => ({\n \"data-testid\": \"comment-feed-item\",\n onMouseEnter: () =>\n ctx.setHighlightAnchor(c.anchor ?? DOCUMENT_ANCHOR),\n onMouseLeave: () => ctx.setHighlightAnchor(null),\n onClick: (event) =>\n activateThread(event, c.anchor ?? DOCUMENT_ANCHOR),\n })}\n {...handlers}\n {...(threadToMarkdown ? { threadToMarkdown } : {})}\n />\n </div>\n );\n}\n\nfunction FocusedComments({\n ctx,\n comments: visible,\n anchor,\n label,\n compact,\n threadToMarkdown,\n}: {\n ctx: CommentContextValue;\n comments: Comment[];\n anchor: CommentAnchor;\n label: string;\n compact?: boolean;\n threadToMarkdown?: (thread: readonly Comment[]) => string;\n}) {\n const comments = selectAnchorThreads(visible, anchor);\n const hasComments = comments.length > 0;\n return (\n <div className=\"space-y-3\" data-comment-anchor={anchor}>\n <LocationMeta\n label={\n anchor === DOCUMENT_ANCHOR\n ? \"Whole-page comment\"\n : `Attached to ${label}`\n }\n />\n <CommentThread\n comments={comments}\n config={ctx.config}\n anchor={anchor}\n compact={compact ?? false}\n autoFocusComposer={!hasComments}\n defaultExpanded={hasComments}\n composerPlaceholder={\n hasComments ? \"Add another top-level comment…\" : \"Add a comment…\"\n }\n {...ctx.callbacks}\n {...(threadToMarkdown ? { threadToMarkdown } : {})}\n />\n </div>\n );\n}\n\n/**\n * A controlled comment rail driven by {@link CommentProvider}. Shows a focused\n * thread for the active anchor, the full document feed in anchor order, or a\n * toggle when collapsed. Renders nothing outside a provider or when empty.\n */\nexport function CommentSidePanel(props: CommentSidePanelProps) {\n const ctx = useCommentContextOptional();\n const label = useAnchorLabel(props);\n const railRef = useRef<HTMLElement>(null);\n const focusedRef = useRef<HTMLDivElement>(null);\n const [focusedOffset, setFocusedOffset] = useState<number | null>(null);\n const [stage, setStage] = useState<CommentStatusStage>(\"active\");\n const focusedAnchor = ctx?.railMode === \"focused\" ? ctx.focusedAnchor : null;\n\n useEffect(() => {\n if (!ctx || props.focusedAlignment !== \"anchor\" || !focusedAnchor) {\n setFocusedOffset(null);\n return;\n }\n const content = ctx.contentRef.current;\n const rail = railRef.current;\n const focused = focusedRef.current;\n if (!content || !rail || !focused) return;\n\n const update = () => {\n const anchorTop = ctx.getAnchorTop(focusedAnchor);\n if (anchorTop == null) {\n setFocusedOffset(null);\n return;\n }\n const desiredTop = anchorTop + content.getBoundingClientRect().top;\n const currentOffset = Number.parseFloat(focused.style.top) || 0;\n const nextOffset =\n currentOffset + desiredTop - focused.getBoundingClientRect().top;\n setFocusedOffset(Math.max(0, nextOffset));\n };\n update();\n content.addEventListener(\"scroll\", update, { passive: true });\n window.addEventListener(\"resize\", update);\n const observer =\n typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(update);\n observer?.observe(content);\n observer?.observe(rail);\n return () => {\n content.removeEventListener(\"scroll\", update);\n window.removeEventListener(\"resize\", update);\n observer?.disconnect();\n };\n }, [ctx, focusedAnchor, props.focusedAlignment]);\n\n if (!ctx) return null;\n\n // Threads, not cards: a reply is part of its root, never a separate entry.\n const counts = getRoots(ctx.comments).reduce(\n (result, root) => {\n const rootStage = resolveCommentStage(ctx.config, root.status);\n if (rootStage) result[rootStage] += 1;\n return result;\n },\n { active: 0, resolved: 0, closed: 0 },\n );\n const visible = selectCommentThreadsByStage(ctx.comments, ctx.config, stage);\n if (\n ctx.railMode === \"closed\" &&\n counts.active + counts.resolved + counts.closed === 0\n )\n return null;\n\n function selectStage(next: CommentStatusStage) {\n setStage(next);\n if (ctx?.railMode !== \"all\") ctx?.openCommentList();\n }\n\n return (\n <aside\n ref={railRef}\n data-testid=\"comment-side-panel\"\n className={cn(\"relative w-[320px] space-y-3\", props.className)}\n >\n <div\n data-testid=\"comment-rail-header\"\n className=\"sticky top-0 z-20 flex items-center gap-2 bg-background py-2\"\n >\n {(counts.active > 0 || ctx.railMode === \"all\") && (\n <RailToggle\n active={ctx.railMode === \"all\" && stage === \"active\"}\n onClick={() => selectStage(\"active\")}\n testId=\"comment-open-all\"\n >\n {ctx.railMode === \"closed\"\n ? `Open comments (${counts.active})`\n : ctx.railMode === \"focused\"\n ? `All comments (${counts.active})`\n : `Open (${counts.active})`}\n </RailToggle>\n )}\n {counts.resolved > 0 && (\n <button\n type=\"button\"\n aria-pressed={ctx.railMode === \"all\" && stage === \"resolved\"}\n onClick={() => selectStage(\"resolved\")}\n className={cn(\n \"rounded-full border px-2 py-1 text-xs font-medium transition-colors\",\n ctx.railMode === \"all\" && stage === \"resolved\"\n ? \"border-primary bg-primary text-primary-foreground\"\n : \"border-border bg-background text-muted-foreground hover:text-foreground\",\n )}\n >\n Resolved ({counts.resolved})\n </button>\n )}\n {counts.closed > 0 && (\n <button\n type=\"button\"\n aria-pressed={ctx.railMode === \"all\" && stage === \"closed\"}\n onClick={() => selectStage(\"closed\")}\n className={cn(\n \"rounded-full border px-2 py-1 text-xs font-medium transition-colors\",\n ctx.railMode === \"all\" && stage === \"closed\"\n ? \"border-primary bg-primary text-primary-foreground\"\n : \"border-border bg-background text-muted-foreground hover:text-foreground\",\n )}\n >\n Closed ({counts.closed})\n </button>\n )}\n {ctx.railMode !== \"closed\" && (\n <button\n type=\"button\"\n onClick={ctx.closeRail}\n className=\"ml-auto rounded-md px-2 py-1 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground\"\n >\n {ctx.railMode === \"all\" ? \"Close\" : \"Hide\"}\n </button>\n )}\n </div>\n {ctx.railMode === \"all\" ? (\n <AllComments\n ctx={ctx}\n comments={visible}\n label={label}\n emptyLabel={\n stage === \"active\"\n ? \"No open comments.\"\n : stage === \"resolved\"\n ? \"No resolved comments.\"\n : \"No closed comments.\"\n }\n {...(props.threadToMarkdown\n ? { threadToMarkdown: props.threadToMarkdown }\n : {})}\n />\n ) : ctx.railMode === \"focused\" && ctx.focusedAnchor ? (\n <div\n ref={focusedRef}\n data-testid=\"comment-focused-rail\"\n className=\"relative space-y-3\"\n style={focusedOffset == null ? undefined : { top: focusedOffset }}\n >\n <FocusedComments\n ctx={ctx}\n comments={ctx.comments}\n anchor={ctx.focusedAnchor}\n label={label(ctx.focusedAnchor)}\n {...(props.compact !== undefined ? { compact: props.compact } : {})}\n {...(props.threadToMarkdown\n ? { threadToMarkdown: props.threadToMarkdown }\n : {})}\n />\n </div>\n ) : null}\n </aside>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -38,22 +38,6 @@ function isUnresolved(config, status) {
|
|
|
38
38
|
var _a;
|
|
39
39
|
return ((_a = resolveStatusConfig(config, status)) == null ? void 0 : _a.unresolved) ?? false;
|
|
40
40
|
}
|
|
41
|
-
function resolveCommentStage(config, status) {
|
|
42
|
-
if (status == null) return "active";
|
|
43
|
-
const resolved = resolveStatusConfig(config, status);
|
|
44
|
-
if (!resolved) return void 0;
|
|
45
|
-
if (resolved.stage) return resolved.stage;
|
|
46
|
-
if (resolved.unresolved) return "active";
|
|
47
|
-
if (resolved.value.toLowerCase() === "closed" || resolved.label.toLowerCase() === "closed") {
|
|
48
|
-
return "closed";
|
|
49
|
-
}
|
|
50
|
-
return "resolved";
|
|
51
|
-
}
|
|
52
|
-
function statusForCommentStage(config, stage) {
|
|
53
|
-
return config.statuses.find(
|
|
54
|
-
(status) => resolveCommentStage(config, status.value) === stage
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
41
|
function getRoots(comments) {
|
|
58
42
|
return comments.filter((c) => !c.parentId);
|
|
59
43
|
}
|
|
@@ -93,14 +77,6 @@ function selectUnresolvedThreads(comments, config) {
|
|
|
93
77
|
);
|
|
94
78
|
return comments.filter((c) => keep.has(String(c.parentId ?? c.id)));
|
|
95
79
|
}
|
|
96
|
-
function selectCommentThreadsByStage(comments, config, stage) {
|
|
97
|
-
const keep = new Set(
|
|
98
|
-
getRoots(comments).filter((root) => resolveCommentStage(config, root.status) === stage).map((root) => String(root.id))
|
|
99
|
-
);
|
|
100
|
-
return comments.filter(
|
|
101
|
-
(comment) => keep.has(String(comment.parentId ?? comment.id))
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
80
|
function sortReplies(replies) {
|
|
105
81
|
return [...replies].sort((a, b) => {
|
|
106
82
|
if (a.createdAt !== b.createdAt)
|
|
@@ -259,14 +235,11 @@ exports.hasActiveFilters = hasActiveFilters;
|
|
|
259
235
|
exports.isUnresolved = isUnresolved;
|
|
260
236
|
exports.matchMentionsInBody = matchMentionsInBody;
|
|
261
237
|
exports.nextChecklistStatus = nextChecklistStatus;
|
|
262
|
-
exports.resolveCommentStage = resolveCommentStage;
|
|
263
238
|
exports.resolveFacetOption = resolveFacetOption;
|
|
264
239
|
exports.resolveStatusConfig = resolveStatusConfig;
|
|
265
240
|
exports.selectAnchorThreads = selectAnchorThreads;
|
|
266
|
-
exports.selectCommentThreadsByStage = selectCommentThreadsByStage;
|
|
267
241
|
exports.selectUnresolvedThreads = selectUnresolvedThreads;
|
|
268
242
|
exports.sortReplies = sortReplies;
|
|
269
|
-
exports.statusForCommentStage = statusForCommentStage;
|
|
270
243
|
exports.toneToBadgeTone = toneToBadgeTone;
|
|
271
244
|
exports.truncatePlain = truncatePlain;
|
|
272
245
|
//# sourceMappingURL=comment-utils.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comment-utils.cjs","sources":["../../src/comments/comment-utils.ts"],"sourcesContent":["import type { BadgeTone } from \"../data/Badge\";\nimport {\n DEFAULT_CHECKLIST_CYCLE,\n type Comment,\n type CommentAnchor,\n type CommentAnchorMeta,\n type CommentAuthor,\n type CommentCallbacks,\n type CommentConfig,\n type CommentFacet,\n type CommentFacetOption,\n type CommentFilters,\n type CommentMention,\n type CommentMentionable,\n type CommentMentionKind,\n type CommentStatusConfig,\n type CommentStatusStage,\n type CommentTone,\n DOCUMENT_ANCHOR,\n} from \"./comment-types\";\n\n/** Human-readable name for an author, falling back to \"Unknown\". */\nexport function authorDisplayName(author: CommentAuthor | null): string {\n return author?.name?.trim() || \"Unknown\";\n}\n\n/** Maps a CommentTone onto the Badge component's tone vocabulary. */\nexport function toneToBadgeTone(tone: CommentTone | undefined): BadgeTone {\n switch (tone) {\n case \"success\":\n return \"success\";\n case \"danger\":\n return \"danger\";\n case \"warning\":\n return \"warning\";\n case \"info\":\n return \"info\";\n case \"neutral\":\n case \"default\":\n default:\n return \"neutral\";\n }\n}\n\n/** Looks up the status config for a stored status value. */\nexport function resolveStatusConfig(\n config: CommentConfig,\n value: string | undefined,\n): CommentStatusConfig | undefined {\n if (value == null) return undefined;\n return config.statuses.find((s) => s.value === value);\n}\n\n/** Looks up a facet option for a stored facet value. */\nexport function resolveFacetOption(\n facet: CommentFacet,\n value: string | undefined,\n): CommentFacetOption | undefined {\n if (value == null) return undefined;\n return facet.options.find((o) => o.value === value);\n}\n\n/** The default status value for new comments: first unresolved, else first. */\nexport function defaultStatusValue(config: CommentConfig): string | undefined {\n const unresolved = config.statuses.find((s) => s.unresolved);\n return (unresolved ?? config.statuses[0])?.value;\n}\n\n/** Whether a stored status counts as unresolved/open. */\nexport function isUnresolved(\n config: CommentConfig,\n status: string | undefined,\n): boolean {\n return resolveStatusConfig(config, status)?.unresolved ?? false;\n}\n\n/** Resolves a stored status to its lifecycle stage. */\nexport function resolveCommentStage(\n config: CommentConfig,\n status: string | undefined,\n): CommentStatusStage | undefined {\n if (status == null) return \"active\";\n const resolved = resolveStatusConfig(config, status);\n if (!resolved) return undefined;\n if (resolved.stage) return resolved.stage;\n if (resolved.unresolved) return \"active\";\n if (\n resolved.value.toLowerCase() === \"closed\" ||\n resolved.label.toLowerCase() === \"closed\"\n ) {\n return \"closed\";\n }\n return \"resolved\";\n}\n\n/** Returns the first configured stored status for a lifecycle stage. */\nexport function statusForCommentStage(\n config: CommentConfig,\n stage: CommentStatusStage,\n): CommentStatusConfig | undefined {\n return config.statuses.find(\n (status) => resolveCommentStage(config, status.value) === stage,\n );\n}\n\n/** Returns the root (non-reply) comments in input order. */\nexport function getRoots(comments: Comment[]): Comment[] {\n return comments.filter((c) => !c.parentId);\n}\n\n/** Indexes replies by their parent id. */\nexport function buildReplyMap(comments: Comment[]): Map<string, Comment[]> {\n const map = new Map<string, Comment[]>();\n for (const reply of comments) {\n if (!reply.parentId) continue;\n const items = map.get(reply.parentId) ?? [];\n items.push(reply);\n map.set(reply.parentId, items);\n }\n return map;\n}\n\n/**\n * Comments forming the threads rooted at `anchor`: matching roots plus their\n * descendants. Replies carry no anchor of their own — they belong to a thread,\n * not to a location — so selecting by anchor equality alone would drop them.\n */\nexport function selectAnchorThreads(\n comments: Comment[],\n anchor: CommentAnchor,\n): Comment[] {\n const roots = comments.filter(\n (c) => !c.parentId && (c.anchor ?? DOCUMENT_ANCHOR) === anchor,\n );\n const ids = new Set(roots.map((c) => String(c.id)));\n const descendants: Comment[] = [];\n // Fixed point, so a reply to a reply is kept when replies are not flattened.\n for (let grew = true; grew; ) {\n grew = false;\n for (const c of comments) {\n if (!c.parentId || ids.has(String(c.id))) continue;\n if (!ids.has(String(c.parentId))) continue;\n ids.add(String(c.id));\n descendants.push(c);\n grew = true;\n }\n }\n return [...roots, ...descendants];\n}\n\n/**\n * Threads whose root is not yet resolved. A root carrying no status has not\n * been acted on, so it counts as unresolved; replies follow their root.\n */\nexport function selectUnresolvedThreads(\n comments: Comment[],\n config: CommentConfig,\n): Comment[] {\n const keep = new Set(\n getRoots(comments)\n .filter(\n (root) => root.status == null || isUnresolved(config, root.status),\n )\n .map((root) => String(root.id)),\n );\n return comments.filter((c) => keep.has(String(c.parentId ?? c.id)));\n}\n\n/** Selects roots in one lifecycle stage together with every descendant reply. */\nexport function selectCommentThreadsByStage(\n comments: Comment[],\n config: CommentConfig,\n stage: CommentStatusStage,\n): Comment[] {\n const keep = new Set(\n getRoots(comments)\n .filter((root) => resolveCommentStage(config, root.status) === stage)\n .map((root) => String(root.id)),\n );\n return comments.filter((comment) =>\n keep.has(String(comment.parentId ?? comment.id)),\n );\n}\n\n/** Stable reply ordering: by creation time, then id. */\nexport function sortReplies(replies: Comment[]): Comment[] {\n return [...replies].sort((a, b) => {\n if (a.createdAt !== b.createdAt)\n return a.createdAt.localeCompare(b.createdAt);\n return String(a.id).localeCompare(String(b.id));\n });\n}\n\n/** Groups root comments by the value of a facet key (missing → \"\"). */\nexport function groupByFacet(\n comments: Comment[],\n facetKey: string,\n): Map<string, Comment[]> {\n const map = new Map<string, Comment[]>();\n for (const root of getRoots(comments)) {\n const key = root.facets?.[facetKey] ?? \"\";\n const items = map.get(key) ?? [];\n items.push(root);\n map.set(key, items);\n }\n return map;\n}\n\n/** Per-anchor comment counts derived from the list (document-level → DOCUMENT_ANCHOR). */\nexport function deriveAnchorCounts(\n comments: Comment[],\n): Record<CommentAnchor, number> {\n const counts: Record<CommentAnchor, number> = {};\n for (const c of comments) {\n const key = c.anchor ?? DOCUMENT_ANCHOR;\n counts[key] = (counts[key] ?? 0) + 1;\n }\n return counts;\n}\n\n/** Per-anchor aggregate metadata (count, distinct authors, latest status). */\nexport function deriveAnchorMeta(\n comments: Comment[],\n): Record<CommentAnchor, CommentAnchorMeta> {\n const grouped: Record<CommentAnchor, Comment[]> = {};\n for (const c of comments) {\n const key = c.anchor ?? DOCUMENT_ANCHOR;\n (grouped[key] ??= []).push(c);\n }\n const meta: Record<CommentAnchor, CommentAnchorMeta> = {};\n for (const [anchor, items] of Object.entries(grouped)) {\n const authors = [\n ...new Set(\n items.map((c) => c.author?.name).filter((n): n is string => !!n),\n ),\n ];\n const latest = items.reduce((a, b) =>\n (a.updatedAt ?? a.createdAt) > (b.updatedAt ?? b.createdAt) ? a : b,\n );\n meta[anchor] = {\n count: items.length,\n authors,\n ...(latest.status != null ? { latestStatus: latest.status } : {}),\n };\n }\n return meta;\n}\n\n/** Sums counts for an anchor and its descendants under a separator-delimited prefix. */\nexport function commentCountForPrefix(\n counts: Record<string, number>,\n prefix: string,\n separator = \".\",\n): number {\n let total = 0;\n for (const [key, count] of Object.entries(counts)) {\n if (key === prefix || key.startsWith(prefix + separator)) total += count;\n }\n return total;\n}\n\n/** Advances a checklist status to the next in the cycle (wrapping). */\nexport function nextChecklistStatus(\n current: string,\n cycle = DEFAULT_CHECKLIST_CYCLE,\n): string {\n const idx = cycle.indexOf(current);\n if (idx === -1) return cycle[0] ?? current;\n return cycle[(idx + 1) % cycle.length] ?? current;\n}\n\n/** Strips inline markdown punctuation and truncates to a plain preview. */\nexport function truncatePlain(body: string, max = 80): string {\n const plain = body.replace(/[#*_`~>[\\]()!]/g, \"\").trim();\n return plain.length > max ? `${plain.slice(0, max)}…` : plain;\n}\n\nfunction authorKindOf(comment: Comment): CommentMentionKind {\n return comment.author?.kind ?? \"user\";\n}\n\n/**\n * Filters a comment list by status / facets / author kind while keeping threads\n * intact: a reply survives iff its root survives. An empty status set or empty\n * per-facet set means \"no constraint\" for that axis.\n */\nexport function applyCommentFilters(\n comments: Comment[],\n filters: CommentFilters,\n config: CommentConfig,\n): Comment[] {\n const facetKeys = (config.facets ?? []).map((f) => f.key);\n const passingRootIds = new Set<string>();\n\n for (const root of getRoots(comments)) {\n if (\n filters.statuses.size > 0 &&\n (root.status == null || !filters.statuses.has(root.status))\n ) {\n continue;\n }\n if (\n filters.authorKind &&\n filters.authorKind !== \"all\" &&\n authorKindOf(root) !== filters.authorKind\n ) {\n continue;\n }\n const facetsPass = facetKeys.every((key) => {\n const selected = filters.facets[key];\n if (!selected || selected.size === 0) return true;\n const value = root.facets?.[key];\n return value != null && selected.has(value);\n });\n if (!facetsPass) continue;\n passingRootIds.add(root.id);\n }\n\n return comments.filter((c) =>\n c.parentId ? passingRootIds.has(c.parentId) : passingRootIds.has(c.id),\n );\n}\n\n/** True when any filter axis constrains the result. */\nexport function hasActiveFilters(filters: CommentFilters): boolean {\n if (filters.statuses.size > 0) return true;\n if (filters.authorKind && filters.authorKind !== \"all\") return true;\n return Object.values(filters.facets).some((set) => set.size > 0);\n}\n\n/** An empty filter state. */\nexport function emptyCommentFilters(): CommentFilters {\n return { statuses: new Set(), facets: {}, authorKind: \"all\" };\n}\n\n/** Finds mentionables whose `@name` token appears in a body (case-insensitive). */\nexport function matchMentionsInBody(\n body: string,\n mentionables: CommentMentionable[],\n): CommentMention[] {\n const lower = body.toLowerCase();\n return mentionables\n .filter((m) => lower.includes(`@${m.name.toLowerCase()}`))\n .map((m) => ({ id: m.id, name: m.name, kind: m.kind }));\n}\n\n/** Per-id handlers consumed by CommentThreadList. */\nexport type ThreadListHandlers = {\n onUpdateStatus?: (id: string, status: string) => void | Promise<void>;\n onClose?: (id: string) => void | Promise<void>;\n onChecklistToggle?: (id: string, index: number) => void;\n onDelete?: (id: string) => void;\n onReply?: (parent: Comment, body: string) => void | Promise<void>;\n};\n\n/**\n * Adapts high-level {@link CommentCallbacks} into the id-keyed handlers a\n * {@link CommentThreadList} consumes: resolves the next checklist status from the\n * cycle and extracts mentions from a reply body. Shared by `CommentThread` and\n * `CommentSidePanel` so the adaptation lives in one place.\n */\nexport function buildThreadListHandlers(\n comments: Comment[],\n config: CommentConfig,\n cb: CommentCallbacks,\n): ThreadListHandlers {\n const handlers: ThreadListHandlers = {};\n if (cb.onUpdateStatus) handlers.onUpdateStatus = cb.onUpdateStatus;\n if (cb.onClose) handlers.onClose = cb.onClose;\n if (cb.onDelete) handlers.onDelete = cb.onDelete;\n if (cb.onChecklistToggle) {\n const toggle = cb.onChecklistToggle;\n handlers.onChecklistToggle = (id, index) => {\n const item = comments.find((c) => c.id === id)?.checklist?.[index];\n if (!item) return;\n void toggle(\n id,\n index,\n nextChecklistStatus(item.status, config.checklistStatusCycle),\n );\n };\n }\n if (cb.onReply) {\n const reply = cb.onReply;\n const onMention = cb.onMention;\n handlers.onReply = async (parent, body) => {\n const mentions = matchMentionsInBody(body, config.mentionables ?? []);\n await reply({\n parentId: parent.id,\n body,\n anchor: parent.anchor ?? null,\n ...(mentions.length > 0 ? { mentions } : {}),\n });\n for (const mention of mentions) onMention?.(mention, { body });\n };\n }\n return handlers;\n}\n"],"names":["DOCUMENT_ANCHOR","DEFAULT_CHECKLIST_CYCLE"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"comment-utils.cjs","sources":["../../src/comments/comment-utils.ts"],"sourcesContent":["import type { BadgeTone } from \"../data/Badge\";\nimport {\n DEFAULT_CHECKLIST_CYCLE,\n type Comment,\n type CommentAnchor,\n type CommentAnchorMeta,\n type CommentAuthor,\n type CommentCallbacks,\n type CommentConfig,\n type CommentFacet,\n type CommentFacetOption,\n type CommentFilters,\n type CommentMention,\n type CommentMentionable,\n type CommentMentionKind,\n type CommentStatusConfig,\n type CommentTone,\n DOCUMENT_ANCHOR,\n} from \"./comment-types\";\n\n/** Human-readable name for an author, falling back to \"Unknown\". */\nexport function authorDisplayName(author: CommentAuthor | null): string {\n return author?.name?.trim() || \"Unknown\";\n}\n\n/** Maps a CommentTone onto the Badge component's tone vocabulary. */\nexport function toneToBadgeTone(tone: CommentTone | undefined): BadgeTone {\n switch (tone) {\n case \"success\":\n return \"success\";\n case \"danger\":\n return \"danger\";\n case \"warning\":\n return \"warning\";\n case \"info\":\n return \"info\";\n case \"neutral\":\n case \"default\":\n default:\n return \"neutral\";\n }\n}\n\n/** Looks up the status config for a stored status value. */\nexport function resolveStatusConfig(\n config: CommentConfig,\n value: string | undefined,\n): CommentStatusConfig | undefined {\n if (value == null) return undefined;\n return config.statuses.find((s) => s.value === value);\n}\n\n/** Looks up a facet option for a stored facet value. */\nexport function resolveFacetOption(\n facet: CommentFacet,\n value: string | undefined,\n): CommentFacetOption | undefined {\n if (value == null) return undefined;\n return facet.options.find((o) => o.value === value);\n}\n\n/** The default status value for new comments: first unresolved, else first. */\nexport function defaultStatusValue(config: CommentConfig): string | undefined {\n const unresolved = config.statuses.find((s) => s.unresolved);\n return (unresolved ?? config.statuses[0])?.value;\n}\n\n/** Whether a stored status counts as unresolved/open. */\nexport function isUnresolved(\n config: CommentConfig,\n status: string | undefined,\n): boolean {\n return resolveStatusConfig(config, status)?.unresolved ?? false;\n}\n\n/** Returns the root (non-reply) comments in input order. */\nexport function getRoots(comments: Comment[]): Comment[] {\n return comments.filter((c) => !c.parentId);\n}\n\n/** Indexes replies by their parent id. */\nexport function buildReplyMap(comments: Comment[]): Map<string, Comment[]> {\n const map = new Map<string, Comment[]>();\n for (const reply of comments) {\n if (!reply.parentId) continue;\n const items = map.get(reply.parentId) ?? [];\n items.push(reply);\n map.set(reply.parentId, items);\n }\n return map;\n}\n\n/**\n * Comments forming the threads rooted at `anchor`: matching roots plus their\n * descendants. Replies carry no anchor of their own — they belong to a thread,\n * not to a location — so selecting by anchor equality alone would drop them.\n */\nexport function selectAnchorThreads(\n comments: Comment[],\n anchor: CommentAnchor,\n): Comment[] {\n const roots = comments.filter(\n (c) => !c.parentId && (c.anchor ?? DOCUMENT_ANCHOR) === anchor,\n );\n const ids = new Set(roots.map((c) => String(c.id)));\n const descendants: Comment[] = [];\n // Fixed point, so a reply to a reply is kept when replies are not flattened.\n for (let grew = true; grew; ) {\n grew = false;\n for (const c of comments) {\n if (!c.parentId || ids.has(String(c.id))) continue;\n if (!ids.has(String(c.parentId))) continue;\n ids.add(String(c.id));\n descendants.push(c);\n grew = true;\n }\n }\n return [...roots, ...descendants];\n}\n\n/**\n * Threads whose root is not yet resolved. A root carrying no status has not\n * been acted on, so it counts as unresolved; replies follow their root.\n */\nexport function selectUnresolvedThreads(\n comments: Comment[],\n config: CommentConfig,\n): Comment[] {\n const keep = new Set(\n getRoots(comments)\n .filter(\n (root) => root.status == null || isUnresolved(config, root.status),\n )\n .map((root) => String(root.id)),\n );\n return comments.filter((c) => keep.has(String(c.parentId ?? c.id)));\n}\n\n/** Stable reply ordering: by creation time, then id. */\nexport function sortReplies(replies: Comment[]): Comment[] {\n return [...replies].sort((a, b) => {\n if (a.createdAt !== b.createdAt)\n return a.createdAt.localeCompare(b.createdAt);\n return String(a.id).localeCompare(String(b.id));\n });\n}\n\n/** Groups root comments by the value of a facet key (missing → \"\"). */\nexport function groupByFacet(\n comments: Comment[],\n facetKey: string,\n): Map<string, Comment[]> {\n const map = new Map<string, Comment[]>();\n for (const root of getRoots(comments)) {\n const key = root.facets?.[facetKey] ?? \"\";\n const items = map.get(key) ?? [];\n items.push(root);\n map.set(key, items);\n }\n return map;\n}\n\n/** Per-anchor comment counts derived from the list (document-level → DOCUMENT_ANCHOR). */\nexport function deriveAnchorCounts(\n comments: Comment[],\n): Record<CommentAnchor, number> {\n const counts: Record<CommentAnchor, number> = {};\n for (const c of comments) {\n const key = c.anchor ?? DOCUMENT_ANCHOR;\n counts[key] = (counts[key] ?? 0) + 1;\n }\n return counts;\n}\n\n/** Per-anchor aggregate metadata (count, distinct authors, latest status). */\nexport function deriveAnchorMeta(\n comments: Comment[],\n): Record<CommentAnchor, CommentAnchorMeta> {\n const grouped: Record<CommentAnchor, Comment[]> = {};\n for (const c of comments) {\n const key = c.anchor ?? DOCUMENT_ANCHOR;\n (grouped[key] ??= []).push(c);\n }\n const meta: Record<CommentAnchor, CommentAnchorMeta> = {};\n for (const [anchor, items] of Object.entries(grouped)) {\n const authors = [\n ...new Set(\n items.map((c) => c.author?.name).filter((n): n is string => !!n),\n ),\n ];\n const latest = items.reduce((a, b) =>\n (a.updatedAt ?? a.createdAt) > (b.updatedAt ?? b.createdAt) ? a : b,\n );\n meta[anchor] = {\n count: items.length,\n authors,\n ...(latest.status != null ? { latestStatus: latest.status } : {}),\n };\n }\n return meta;\n}\n\n/** Sums counts for an anchor and its descendants under a separator-delimited prefix. */\nexport function commentCountForPrefix(\n counts: Record<string, number>,\n prefix: string,\n separator = \".\",\n): number {\n let total = 0;\n for (const [key, count] of Object.entries(counts)) {\n if (key === prefix || key.startsWith(prefix + separator)) total += count;\n }\n return total;\n}\n\n/** Advances a checklist status to the next in the cycle (wrapping). */\nexport function nextChecklistStatus(\n current: string,\n cycle = DEFAULT_CHECKLIST_CYCLE,\n): string {\n const idx = cycle.indexOf(current);\n if (idx === -1) return cycle[0] ?? current;\n return cycle[(idx + 1) % cycle.length] ?? current;\n}\n\n/** Strips inline markdown punctuation and truncates to a plain preview. */\nexport function truncatePlain(body: string, max = 80): string {\n const plain = body.replace(/[#*_`~>[\\]()!]/g, \"\").trim();\n return plain.length > max ? `${plain.slice(0, max)}…` : plain;\n}\n\nfunction authorKindOf(comment: Comment): CommentMentionKind {\n return comment.author?.kind ?? \"user\";\n}\n\n/**\n * Filters a comment list by status / facets / author kind while keeping threads\n * intact: a reply survives iff its root survives. An empty status set or empty\n * per-facet set means \"no constraint\" for that axis.\n */\nexport function applyCommentFilters(\n comments: Comment[],\n filters: CommentFilters,\n config: CommentConfig,\n): Comment[] {\n const facetKeys = (config.facets ?? []).map((f) => f.key);\n const passingRootIds = new Set<string>();\n\n for (const root of getRoots(comments)) {\n if (\n filters.statuses.size > 0 &&\n (root.status == null || !filters.statuses.has(root.status))\n ) {\n continue;\n }\n if (\n filters.authorKind &&\n filters.authorKind !== \"all\" &&\n authorKindOf(root) !== filters.authorKind\n ) {\n continue;\n }\n const facetsPass = facetKeys.every((key) => {\n const selected = filters.facets[key];\n if (!selected || selected.size === 0) return true;\n const value = root.facets?.[key];\n return value != null && selected.has(value);\n });\n if (!facetsPass) continue;\n passingRootIds.add(root.id);\n }\n\n return comments.filter((c) =>\n c.parentId ? passingRootIds.has(c.parentId) : passingRootIds.has(c.id),\n );\n}\n\n/** True when any filter axis constrains the result. */\nexport function hasActiveFilters(filters: CommentFilters): boolean {\n if (filters.statuses.size > 0) return true;\n if (filters.authorKind && filters.authorKind !== \"all\") return true;\n return Object.values(filters.facets).some((set) => set.size > 0);\n}\n\n/** An empty filter state. */\nexport function emptyCommentFilters(): CommentFilters {\n return { statuses: new Set(), facets: {}, authorKind: \"all\" };\n}\n\n/** Finds mentionables whose `@name` token appears in a body (case-insensitive). */\nexport function matchMentionsInBody(\n body: string,\n mentionables: CommentMentionable[],\n): CommentMention[] {\n const lower = body.toLowerCase();\n return mentionables\n .filter((m) => lower.includes(`@${m.name.toLowerCase()}`))\n .map((m) => ({ id: m.id, name: m.name, kind: m.kind }));\n}\n\n/** Per-id handlers consumed by CommentThreadList. */\nexport type ThreadListHandlers = {\n onUpdateStatus?: (id: string, status: string) => void | Promise<void>;\n onClose?: (id: string) => void | Promise<void>;\n onChecklistToggle?: (id: string, index: number) => void;\n onDelete?: (id: string) => void;\n onReply?: (parent: Comment, body: string) => void | Promise<void>;\n};\n\n/**\n * Adapts high-level {@link CommentCallbacks} into the id-keyed handlers a\n * {@link CommentThreadList} consumes: resolves the next checklist status from the\n * cycle and extracts mentions from a reply body. Shared by `CommentThread` and\n * `CommentSidePanel` so the adaptation lives in one place.\n */\nexport function buildThreadListHandlers(\n comments: Comment[],\n config: CommentConfig,\n cb: CommentCallbacks,\n): ThreadListHandlers {\n const handlers: ThreadListHandlers = {};\n if (cb.onUpdateStatus) handlers.onUpdateStatus = cb.onUpdateStatus;\n if (cb.onClose) handlers.onClose = cb.onClose;\n if (cb.onDelete) handlers.onDelete = cb.onDelete;\n if (cb.onChecklistToggle) {\n const toggle = cb.onChecklistToggle;\n handlers.onChecklistToggle = (id, index) => {\n const item = comments.find((c) => c.id === id)?.checklist?.[index];\n if (!item) return;\n void toggle(\n id,\n index,\n nextChecklistStatus(item.status, config.checklistStatusCycle),\n );\n };\n }\n if (cb.onReply) {\n const reply = cb.onReply;\n const onMention = cb.onMention;\n handlers.onReply = async (parent, body) => {\n const mentions = matchMentionsInBody(body, config.mentionables ?? []);\n await reply({\n parentId: parent.id,\n body,\n anchor: parent.anchor ?? null,\n ...(mentions.length > 0 ? { mentions } : {}),\n });\n for (const mention of mentions) onMention?.(mention, { body });\n };\n }\n return handlers;\n}\n"],"names":["DOCUMENT_ANCHOR","DEFAULT_CHECKLIST_CYCLE"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BadgeTone } from '../data/Badge';
|
|
2
|
-
import { Comment, CommentAnchor, CommentAnchorMeta, CommentAuthor, CommentCallbacks, CommentConfig, CommentFacet, CommentFacetOption, CommentFilters, CommentMention, CommentMentionable, CommentStatusConfig,
|
|
2
|
+
import { Comment, CommentAnchor, CommentAnchorMeta, CommentAuthor, CommentCallbacks, CommentConfig, CommentFacet, CommentFacetOption, CommentFilters, CommentMention, CommentMentionable, CommentStatusConfig, CommentTone } from './comment-types';
|
|
3
3
|
/** Human-readable name for an author, falling back to "Unknown". */
|
|
4
4
|
export declare function authorDisplayName(author: CommentAuthor | null): string;
|
|
5
5
|
/** Maps a CommentTone onto the Badge component's tone vocabulary. */
|
|
@@ -12,10 +12,6 @@ export declare function resolveFacetOption(facet: CommentFacet, value: string |
|
|
|
12
12
|
export declare function defaultStatusValue(config: CommentConfig): string | undefined;
|
|
13
13
|
/** Whether a stored status counts as unresolved/open. */
|
|
14
14
|
export declare function isUnresolved(config: CommentConfig, status: string | undefined): boolean;
|
|
15
|
-
/** Resolves a stored status to its lifecycle stage. */
|
|
16
|
-
export declare function resolveCommentStage(config: CommentConfig, status: string | undefined): CommentStatusStage | undefined;
|
|
17
|
-
/** Returns the first configured stored status for a lifecycle stage. */
|
|
18
|
-
export declare function statusForCommentStage(config: CommentConfig, stage: CommentStatusStage): CommentStatusConfig | undefined;
|
|
19
15
|
/** Returns the root (non-reply) comments in input order. */
|
|
20
16
|
export declare function getRoots(comments: Comment[]): Comment[];
|
|
21
17
|
/** Indexes replies by their parent id. */
|
|
@@ -31,8 +27,6 @@ export declare function selectAnchorThreads(comments: Comment[], anchor: Comment
|
|
|
31
27
|
* been acted on, so it counts as unresolved; replies follow their root.
|
|
32
28
|
*/
|
|
33
29
|
export declare function selectUnresolvedThreads(comments: Comment[], config: CommentConfig): Comment[];
|
|
34
|
-
/** Selects roots in one lifecycle stage together with every descendant reply. */
|
|
35
|
-
export declare function selectCommentThreadsByStage(comments: Comment[], config: CommentConfig, stage: CommentStatusStage): Comment[];
|
|
36
30
|
/** Stable reply ordering: by creation time, then id. */
|
|
37
31
|
export declare function sortReplies(replies: Comment[]): Comment[];
|
|
38
32
|
/** Groups root comments by the value of a facet key (missing → ""). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comment-utils.d.ts","sourceRoot":"","sources":["../../src/comments/comment-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAEL,KAAK,OAAO,EACZ,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EAEvB,KAAK,mBAAmB,EACxB,KAAK,
|
|
1
|
+
{"version":3,"file":"comment-utils.d.ts","sourceRoot":"","sources":["../../src/comments/comment-utils.ts"],"names":[],"mappings":"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"}
|
|
@@ -36,22 +36,6 @@ function isUnresolved(config, status) {
|
|
|
36
36
|
var _a;
|
|
37
37
|
return ((_a = resolveStatusConfig(config, status)) == null ? void 0 : _a.unresolved) ?? false;
|
|
38
38
|
}
|
|
39
|
-
function resolveCommentStage(config, status) {
|
|
40
|
-
if (status == null) return "active";
|
|
41
|
-
const resolved = resolveStatusConfig(config, status);
|
|
42
|
-
if (!resolved) return void 0;
|
|
43
|
-
if (resolved.stage) return resolved.stage;
|
|
44
|
-
if (resolved.unresolved) return "active";
|
|
45
|
-
if (resolved.value.toLowerCase() === "closed" || resolved.label.toLowerCase() === "closed") {
|
|
46
|
-
return "closed";
|
|
47
|
-
}
|
|
48
|
-
return "resolved";
|
|
49
|
-
}
|
|
50
|
-
function statusForCommentStage(config, stage) {
|
|
51
|
-
return config.statuses.find(
|
|
52
|
-
(status) => resolveCommentStage(config, status.value) === stage
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
39
|
function getRoots(comments) {
|
|
56
40
|
return comments.filter((c) => !c.parentId);
|
|
57
41
|
}
|
|
@@ -91,14 +75,6 @@ function selectUnresolvedThreads(comments, config) {
|
|
|
91
75
|
);
|
|
92
76
|
return comments.filter((c) => keep.has(String(c.parentId ?? c.id)));
|
|
93
77
|
}
|
|
94
|
-
function selectCommentThreadsByStage(comments, config, stage) {
|
|
95
|
-
const keep = new Set(
|
|
96
|
-
getRoots(comments).filter((root) => resolveCommentStage(config, root.status) === stage).map((root) => String(root.id))
|
|
97
|
-
);
|
|
98
|
-
return comments.filter(
|
|
99
|
-
(comment) => keep.has(String(comment.parentId ?? comment.id))
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
78
|
function sortReplies(replies) {
|
|
103
79
|
return [...replies].sort((a, b) => {
|
|
104
80
|
if (a.createdAt !== b.createdAt)
|
|
@@ -258,14 +234,11 @@ export {
|
|
|
258
234
|
isUnresolved,
|
|
259
235
|
matchMentionsInBody,
|
|
260
236
|
nextChecklistStatus,
|
|
261
|
-
resolveCommentStage,
|
|
262
237
|
resolveFacetOption,
|
|
263
238
|
resolveStatusConfig,
|
|
264
239
|
selectAnchorThreads,
|
|
265
|
-
selectCommentThreadsByStage,
|
|
266
240
|
selectUnresolvedThreads,
|
|
267
241
|
sortReplies,
|
|
268
|
-
statusForCommentStage,
|
|
269
242
|
toneToBadgeTone,
|
|
270
243
|
truncatePlain
|
|
271
244
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comment-utils.js","sources":["../../src/comments/comment-utils.ts"],"sourcesContent":["import type { BadgeTone } from \"../data/Badge\";\nimport {\n DEFAULT_CHECKLIST_CYCLE,\n type Comment,\n type CommentAnchor,\n type CommentAnchorMeta,\n type CommentAuthor,\n type CommentCallbacks,\n type CommentConfig,\n type CommentFacet,\n type CommentFacetOption,\n type CommentFilters,\n type CommentMention,\n type CommentMentionable,\n type CommentMentionKind,\n type CommentStatusConfig,\n type CommentStatusStage,\n type CommentTone,\n DOCUMENT_ANCHOR,\n} from \"./comment-types\";\n\n/** Human-readable name for an author, falling back to \"Unknown\". */\nexport function authorDisplayName(author: CommentAuthor | null): string {\n return author?.name?.trim() || \"Unknown\";\n}\n\n/** Maps a CommentTone onto the Badge component's tone vocabulary. */\nexport function toneToBadgeTone(tone: CommentTone | undefined): BadgeTone {\n switch (tone) {\n case \"success\":\n return \"success\";\n case \"danger\":\n return \"danger\";\n case \"warning\":\n return \"warning\";\n case \"info\":\n return \"info\";\n case \"neutral\":\n case \"default\":\n default:\n return \"neutral\";\n }\n}\n\n/** Looks up the status config for a stored status value. */\nexport function resolveStatusConfig(\n config: CommentConfig,\n value: string | undefined,\n): CommentStatusConfig | undefined {\n if (value == null) return undefined;\n return config.statuses.find((s) => s.value === value);\n}\n\n/** Looks up a facet option for a stored facet value. */\nexport function resolveFacetOption(\n facet: CommentFacet,\n value: string | undefined,\n): CommentFacetOption | undefined {\n if (value == null) return undefined;\n return facet.options.find((o) => o.value === value);\n}\n\n/** The default status value for new comments: first unresolved, else first. */\nexport function defaultStatusValue(config: CommentConfig): string | undefined {\n const unresolved = config.statuses.find((s) => s.unresolved);\n return (unresolved ?? config.statuses[0])?.value;\n}\n\n/** Whether a stored status counts as unresolved/open. */\nexport function isUnresolved(\n config: CommentConfig,\n status: string | undefined,\n): boolean {\n return resolveStatusConfig(config, status)?.unresolved ?? false;\n}\n\n/** Resolves a stored status to its lifecycle stage. */\nexport function resolveCommentStage(\n config: CommentConfig,\n status: string | undefined,\n): CommentStatusStage | undefined {\n if (status == null) return \"active\";\n const resolved = resolveStatusConfig(config, status);\n if (!resolved) return undefined;\n if (resolved.stage) return resolved.stage;\n if (resolved.unresolved) return \"active\";\n if (\n resolved.value.toLowerCase() === \"closed\" ||\n resolved.label.toLowerCase() === \"closed\"\n ) {\n return \"closed\";\n }\n return \"resolved\";\n}\n\n/** Returns the first configured stored status for a lifecycle stage. */\nexport function statusForCommentStage(\n config: CommentConfig,\n stage: CommentStatusStage,\n): CommentStatusConfig | undefined {\n return config.statuses.find(\n (status) => resolveCommentStage(config, status.value) === stage,\n );\n}\n\n/** Returns the root (non-reply) comments in input order. */\nexport function getRoots(comments: Comment[]): Comment[] {\n return comments.filter((c) => !c.parentId);\n}\n\n/** Indexes replies by their parent id. */\nexport function buildReplyMap(comments: Comment[]): Map<string, Comment[]> {\n const map = new Map<string, Comment[]>();\n for (const reply of comments) {\n if (!reply.parentId) continue;\n const items = map.get(reply.parentId) ?? [];\n items.push(reply);\n map.set(reply.parentId, items);\n }\n return map;\n}\n\n/**\n * Comments forming the threads rooted at `anchor`: matching roots plus their\n * descendants. Replies carry no anchor of their own — they belong to a thread,\n * not to a location — so selecting by anchor equality alone would drop them.\n */\nexport function selectAnchorThreads(\n comments: Comment[],\n anchor: CommentAnchor,\n): Comment[] {\n const roots = comments.filter(\n (c) => !c.parentId && (c.anchor ?? DOCUMENT_ANCHOR) === anchor,\n );\n const ids = new Set(roots.map((c) => String(c.id)));\n const descendants: Comment[] = [];\n // Fixed point, so a reply to a reply is kept when replies are not flattened.\n for (let grew = true; grew; ) {\n grew = false;\n for (const c of comments) {\n if (!c.parentId || ids.has(String(c.id))) continue;\n if (!ids.has(String(c.parentId))) continue;\n ids.add(String(c.id));\n descendants.push(c);\n grew = true;\n }\n }\n return [...roots, ...descendants];\n}\n\n/**\n * Threads whose root is not yet resolved. A root carrying no status has not\n * been acted on, so it counts as unresolved; replies follow their root.\n */\nexport function selectUnresolvedThreads(\n comments: Comment[],\n config: CommentConfig,\n): Comment[] {\n const keep = new Set(\n getRoots(comments)\n .filter(\n (root) => root.status == null || isUnresolved(config, root.status),\n )\n .map((root) => String(root.id)),\n );\n return comments.filter((c) => keep.has(String(c.parentId ?? c.id)));\n}\n\n/** Selects roots in one lifecycle stage together with every descendant reply. */\nexport function selectCommentThreadsByStage(\n comments: Comment[],\n config: CommentConfig,\n stage: CommentStatusStage,\n): Comment[] {\n const keep = new Set(\n getRoots(comments)\n .filter((root) => resolveCommentStage(config, root.status) === stage)\n .map((root) => String(root.id)),\n );\n return comments.filter((comment) =>\n keep.has(String(comment.parentId ?? comment.id)),\n );\n}\n\n/** Stable reply ordering: by creation time, then id. */\nexport function sortReplies(replies: Comment[]): Comment[] {\n return [...replies].sort((a, b) => {\n if (a.createdAt !== b.createdAt)\n return a.createdAt.localeCompare(b.createdAt);\n return String(a.id).localeCompare(String(b.id));\n });\n}\n\n/** Groups root comments by the value of a facet key (missing → \"\"). */\nexport function groupByFacet(\n comments: Comment[],\n facetKey: string,\n): Map<string, Comment[]> {\n const map = new Map<string, Comment[]>();\n for (const root of getRoots(comments)) {\n const key = root.facets?.[facetKey] ?? \"\";\n const items = map.get(key) ?? [];\n items.push(root);\n map.set(key, items);\n }\n return map;\n}\n\n/** Per-anchor comment counts derived from the list (document-level → DOCUMENT_ANCHOR). */\nexport function deriveAnchorCounts(\n comments: Comment[],\n): Record<CommentAnchor, number> {\n const counts: Record<CommentAnchor, number> = {};\n for (const c of comments) {\n const key = c.anchor ?? DOCUMENT_ANCHOR;\n counts[key] = (counts[key] ?? 0) + 1;\n }\n return counts;\n}\n\n/** Per-anchor aggregate metadata (count, distinct authors, latest status). */\nexport function deriveAnchorMeta(\n comments: Comment[],\n): Record<CommentAnchor, CommentAnchorMeta> {\n const grouped: Record<CommentAnchor, Comment[]> = {};\n for (const c of comments) {\n const key = c.anchor ?? DOCUMENT_ANCHOR;\n (grouped[key] ??= []).push(c);\n }\n const meta: Record<CommentAnchor, CommentAnchorMeta> = {};\n for (const [anchor, items] of Object.entries(grouped)) {\n const authors = [\n ...new Set(\n items.map((c) => c.author?.name).filter((n): n is string => !!n),\n ),\n ];\n const latest = items.reduce((a, b) =>\n (a.updatedAt ?? a.createdAt) > (b.updatedAt ?? b.createdAt) ? a : b,\n );\n meta[anchor] = {\n count: items.length,\n authors,\n ...(latest.status != null ? { latestStatus: latest.status } : {}),\n };\n }\n return meta;\n}\n\n/** Sums counts for an anchor and its descendants under a separator-delimited prefix. */\nexport function commentCountForPrefix(\n counts: Record<string, number>,\n prefix: string,\n separator = \".\",\n): number {\n let total = 0;\n for (const [key, count] of Object.entries(counts)) {\n if (key === prefix || key.startsWith(prefix + separator)) total += count;\n }\n return total;\n}\n\n/** Advances a checklist status to the next in the cycle (wrapping). */\nexport function nextChecklistStatus(\n current: string,\n cycle = DEFAULT_CHECKLIST_CYCLE,\n): string {\n const idx = cycle.indexOf(current);\n if (idx === -1) return cycle[0] ?? current;\n return cycle[(idx + 1) % cycle.length] ?? current;\n}\n\n/** Strips inline markdown punctuation and truncates to a plain preview. */\nexport function truncatePlain(body: string, max = 80): string {\n const plain = body.replace(/[#*_`~>[\\]()!]/g, \"\").trim();\n return plain.length > max ? `${plain.slice(0, max)}…` : plain;\n}\n\nfunction authorKindOf(comment: Comment): CommentMentionKind {\n return comment.author?.kind ?? \"user\";\n}\n\n/**\n * Filters a comment list by status / facets / author kind while keeping threads\n * intact: a reply survives iff its root survives. An empty status set or empty\n * per-facet set means \"no constraint\" for that axis.\n */\nexport function applyCommentFilters(\n comments: Comment[],\n filters: CommentFilters,\n config: CommentConfig,\n): Comment[] {\n const facetKeys = (config.facets ?? []).map((f) => f.key);\n const passingRootIds = new Set<string>();\n\n for (const root of getRoots(comments)) {\n if (\n filters.statuses.size > 0 &&\n (root.status == null || !filters.statuses.has(root.status))\n ) {\n continue;\n }\n if (\n filters.authorKind &&\n filters.authorKind !== \"all\" &&\n authorKindOf(root) !== filters.authorKind\n ) {\n continue;\n }\n const facetsPass = facetKeys.every((key) => {\n const selected = filters.facets[key];\n if (!selected || selected.size === 0) return true;\n const value = root.facets?.[key];\n return value != null && selected.has(value);\n });\n if (!facetsPass) continue;\n passingRootIds.add(root.id);\n }\n\n return comments.filter((c) =>\n c.parentId ? passingRootIds.has(c.parentId) : passingRootIds.has(c.id),\n );\n}\n\n/** True when any filter axis constrains the result. */\nexport function hasActiveFilters(filters: CommentFilters): boolean {\n if (filters.statuses.size > 0) return true;\n if (filters.authorKind && filters.authorKind !== \"all\") return true;\n return Object.values(filters.facets).some((set) => set.size > 0);\n}\n\n/** An empty filter state. */\nexport function emptyCommentFilters(): CommentFilters {\n return { statuses: new Set(), facets: {}, authorKind: \"all\" };\n}\n\n/** Finds mentionables whose `@name` token appears in a body (case-insensitive). */\nexport function matchMentionsInBody(\n body: string,\n mentionables: CommentMentionable[],\n): CommentMention[] {\n const lower = body.toLowerCase();\n return mentionables\n .filter((m) => lower.includes(`@${m.name.toLowerCase()}`))\n .map((m) => ({ id: m.id, name: m.name, kind: m.kind }));\n}\n\n/** Per-id handlers consumed by CommentThreadList. */\nexport type ThreadListHandlers = {\n onUpdateStatus?: (id: string, status: string) => void | Promise<void>;\n onClose?: (id: string) => void | Promise<void>;\n onChecklistToggle?: (id: string, index: number) => void;\n onDelete?: (id: string) => void;\n onReply?: (parent: Comment, body: string) => void | Promise<void>;\n};\n\n/**\n * Adapts high-level {@link CommentCallbacks} into the id-keyed handlers a\n * {@link CommentThreadList} consumes: resolves the next checklist status from the\n * cycle and extracts mentions from a reply body. Shared by `CommentThread` and\n * `CommentSidePanel` so the adaptation lives in one place.\n */\nexport function buildThreadListHandlers(\n comments: Comment[],\n config: CommentConfig,\n cb: CommentCallbacks,\n): ThreadListHandlers {\n const handlers: ThreadListHandlers = {};\n if (cb.onUpdateStatus) handlers.onUpdateStatus = cb.onUpdateStatus;\n if (cb.onClose) handlers.onClose = cb.onClose;\n if (cb.onDelete) handlers.onDelete = cb.onDelete;\n if (cb.onChecklistToggle) {\n const toggle = cb.onChecklistToggle;\n handlers.onChecklistToggle = (id, index) => {\n const item = comments.find((c) => c.id === id)?.checklist?.[index];\n if (!item) return;\n void toggle(\n id,\n index,\n nextChecklistStatus(item.status, config.checklistStatusCycle),\n );\n };\n }\n if (cb.onReply) {\n const reply = cb.onReply;\n const onMention = cb.onMention;\n handlers.onReply = async (parent, body) => {\n const mentions = matchMentionsInBody(body, config.mentionables ?? []);\n await reply({\n parentId: parent.id,\n body,\n anchor: parent.anchor ?? null,\n ...(mentions.length > 0 ? { mentions } : {}),\n });\n for (const mention of mentions) onMention?.(mention, { body });\n };\n }\n return handlers;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"comment-utils.js","sources":["../../src/comments/comment-utils.ts"],"sourcesContent":["import type { BadgeTone } from \"../data/Badge\";\nimport {\n DEFAULT_CHECKLIST_CYCLE,\n type Comment,\n type CommentAnchor,\n type CommentAnchorMeta,\n type CommentAuthor,\n type CommentCallbacks,\n type CommentConfig,\n type CommentFacet,\n type CommentFacetOption,\n type CommentFilters,\n type CommentMention,\n type CommentMentionable,\n type CommentMentionKind,\n type CommentStatusConfig,\n type CommentTone,\n DOCUMENT_ANCHOR,\n} from \"./comment-types\";\n\n/** Human-readable name for an author, falling back to \"Unknown\". */\nexport function authorDisplayName(author: CommentAuthor | null): string {\n return author?.name?.trim() || \"Unknown\";\n}\n\n/** Maps a CommentTone onto the Badge component's tone vocabulary. */\nexport function toneToBadgeTone(tone: CommentTone | undefined): BadgeTone {\n switch (tone) {\n case \"success\":\n return \"success\";\n case \"danger\":\n return \"danger\";\n case \"warning\":\n return \"warning\";\n case \"info\":\n return \"info\";\n case \"neutral\":\n case \"default\":\n default:\n return \"neutral\";\n }\n}\n\n/** Looks up the status config for a stored status value. */\nexport function resolveStatusConfig(\n config: CommentConfig,\n value: string | undefined,\n): CommentStatusConfig | undefined {\n if (value == null) return undefined;\n return config.statuses.find((s) => s.value === value);\n}\n\n/** Looks up a facet option for a stored facet value. */\nexport function resolveFacetOption(\n facet: CommentFacet,\n value: string | undefined,\n): CommentFacetOption | undefined {\n if (value == null) return undefined;\n return facet.options.find((o) => o.value === value);\n}\n\n/** The default status value for new comments: first unresolved, else first. */\nexport function defaultStatusValue(config: CommentConfig): string | undefined {\n const unresolved = config.statuses.find((s) => s.unresolved);\n return (unresolved ?? config.statuses[0])?.value;\n}\n\n/** Whether a stored status counts as unresolved/open. */\nexport function isUnresolved(\n config: CommentConfig,\n status: string | undefined,\n): boolean {\n return resolveStatusConfig(config, status)?.unresolved ?? false;\n}\n\n/** Returns the root (non-reply) comments in input order. */\nexport function getRoots(comments: Comment[]): Comment[] {\n return comments.filter((c) => !c.parentId);\n}\n\n/** Indexes replies by their parent id. */\nexport function buildReplyMap(comments: Comment[]): Map<string, Comment[]> {\n const map = new Map<string, Comment[]>();\n for (const reply of comments) {\n if (!reply.parentId) continue;\n const items = map.get(reply.parentId) ?? [];\n items.push(reply);\n map.set(reply.parentId, items);\n }\n return map;\n}\n\n/**\n * Comments forming the threads rooted at `anchor`: matching roots plus their\n * descendants. Replies carry no anchor of their own — they belong to a thread,\n * not to a location — so selecting by anchor equality alone would drop them.\n */\nexport function selectAnchorThreads(\n comments: Comment[],\n anchor: CommentAnchor,\n): Comment[] {\n const roots = comments.filter(\n (c) => !c.parentId && (c.anchor ?? DOCUMENT_ANCHOR) === anchor,\n );\n const ids = new Set(roots.map((c) => String(c.id)));\n const descendants: Comment[] = [];\n // Fixed point, so a reply to a reply is kept when replies are not flattened.\n for (let grew = true; grew; ) {\n grew = false;\n for (const c of comments) {\n if (!c.parentId || ids.has(String(c.id))) continue;\n if (!ids.has(String(c.parentId))) continue;\n ids.add(String(c.id));\n descendants.push(c);\n grew = true;\n }\n }\n return [...roots, ...descendants];\n}\n\n/**\n * Threads whose root is not yet resolved. A root carrying no status has not\n * been acted on, so it counts as unresolved; replies follow their root.\n */\nexport function selectUnresolvedThreads(\n comments: Comment[],\n config: CommentConfig,\n): Comment[] {\n const keep = new Set(\n getRoots(comments)\n .filter(\n (root) => root.status == null || isUnresolved(config, root.status),\n )\n .map((root) => String(root.id)),\n );\n return comments.filter((c) => keep.has(String(c.parentId ?? c.id)));\n}\n\n/** Stable reply ordering: by creation time, then id. */\nexport function sortReplies(replies: Comment[]): Comment[] {\n return [...replies].sort((a, b) => {\n if (a.createdAt !== b.createdAt)\n return a.createdAt.localeCompare(b.createdAt);\n return String(a.id).localeCompare(String(b.id));\n });\n}\n\n/** Groups root comments by the value of a facet key (missing → \"\"). */\nexport function groupByFacet(\n comments: Comment[],\n facetKey: string,\n): Map<string, Comment[]> {\n const map = new Map<string, Comment[]>();\n for (const root of getRoots(comments)) {\n const key = root.facets?.[facetKey] ?? \"\";\n const items = map.get(key) ?? [];\n items.push(root);\n map.set(key, items);\n }\n return map;\n}\n\n/** Per-anchor comment counts derived from the list (document-level → DOCUMENT_ANCHOR). */\nexport function deriveAnchorCounts(\n comments: Comment[],\n): Record<CommentAnchor, number> {\n const counts: Record<CommentAnchor, number> = {};\n for (const c of comments) {\n const key = c.anchor ?? DOCUMENT_ANCHOR;\n counts[key] = (counts[key] ?? 0) + 1;\n }\n return counts;\n}\n\n/** Per-anchor aggregate metadata (count, distinct authors, latest status). */\nexport function deriveAnchorMeta(\n comments: Comment[],\n): Record<CommentAnchor, CommentAnchorMeta> {\n const grouped: Record<CommentAnchor, Comment[]> = {};\n for (const c of comments) {\n const key = c.anchor ?? DOCUMENT_ANCHOR;\n (grouped[key] ??= []).push(c);\n }\n const meta: Record<CommentAnchor, CommentAnchorMeta> = {};\n for (const [anchor, items] of Object.entries(grouped)) {\n const authors = [\n ...new Set(\n items.map((c) => c.author?.name).filter((n): n is string => !!n),\n ),\n ];\n const latest = items.reduce((a, b) =>\n (a.updatedAt ?? a.createdAt) > (b.updatedAt ?? b.createdAt) ? a : b,\n );\n meta[anchor] = {\n count: items.length,\n authors,\n ...(latest.status != null ? { latestStatus: latest.status } : {}),\n };\n }\n return meta;\n}\n\n/** Sums counts for an anchor and its descendants under a separator-delimited prefix. */\nexport function commentCountForPrefix(\n counts: Record<string, number>,\n prefix: string,\n separator = \".\",\n): number {\n let total = 0;\n for (const [key, count] of Object.entries(counts)) {\n if (key === prefix || key.startsWith(prefix + separator)) total += count;\n }\n return total;\n}\n\n/** Advances a checklist status to the next in the cycle (wrapping). */\nexport function nextChecklistStatus(\n current: string,\n cycle = DEFAULT_CHECKLIST_CYCLE,\n): string {\n const idx = cycle.indexOf(current);\n if (idx === -1) return cycle[0] ?? current;\n return cycle[(idx + 1) % cycle.length] ?? current;\n}\n\n/** Strips inline markdown punctuation and truncates to a plain preview. */\nexport function truncatePlain(body: string, max = 80): string {\n const plain = body.replace(/[#*_`~>[\\]()!]/g, \"\").trim();\n return plain.length > max ? `${plain.slice(0, max)}…` : plain;\n}\n\nfunction authorKindOf(comment: Comment): CommentMentionKind {\n return comment.author?.kind ?? \"user\";\n}\n\n/**\n * Filters a comment list by status / facets / author kind while keeping threads\n * intact: a reply survives iff its root survives. An empty status set or empty\n * per-facet set means \"no constraint\" for that axis.\n */\nexport function applyCommentFilters(\n comments: Comment[],\n filters: CommentFilters,\n config: CommentConfig,\n): Comment[] {\n const facetKeys = (config.facets ?? []).map((f) => f.key);\n const passingRootIds = new Set<string>();\n\n for (const root of getRoots(comments)) {\n if (\n filters.statuses.size > 0 &&\n (root.status == null || !filters.statuses.has(root.status))\n ) {\n continue;\n }\n if (\n filters.authorKind &&\n filters.authorKind !== \"all\" &&\n authorKindOf(root) !== filters.authorKind\n ) {\n continue;\n }\n const facetsPass = facetKeys.every((key) => {\n const selected = filters.facets[key];\n if (!selected || selected.size === 0) return true;\n const value = root.facets?.[key];\n return value != null && selected.has(value);\n });\n if (!facetsPass) continue;\n passingRootIds.add(root.id);\n }\n\n return comments.filter((c) =>\n c.parentId ? passingRootIds.has(c.parentId) : passingRootIds.has(c.id),\n );\n}\n\n/** True when any filter axis constrains the result. */\nexport function hasActiveFilters(filters: CommentFilters): boolean {\n if (filters.statuses.size > 0) return true;\n if (filters.authorKind && filters.authorKind !== \"all\") return true;\n return Object.values(filters.facets).some((set) => set.size > 0);\n}\n\n/** An empty filter state. */\nexport function emptyCommentFilters(): CommentFilters {\n return { statuses: new Set(), facets: {}, authorKind: \"all\" };\n}\n\n/** Finds mentionables whose `@name` token appears in a body (case-insensitive). */\nexport function matchMentionsInBody(\n body: string,\n mentionables: CommentMentionable[],\n): CommentMention[] {\n const lower = body.toLowerCase();\n return mentionables\n .filter((m) => lower.includes(`@${m.name.toLowerCase()}`))\n .map((m) => ({ id: m.id, name: m.name, kind: m.kind }));\n}\n\n/** Per-id handlers consumed by CommentThreadList. */\nexport type ThreadListHandlers = {\n onUpdateStatus?: (id: string, status: string) => void | Promise<void>;\n onClose?: (id: string) => void | Promise<void>;\n onChecklistToggle?: (id: string, index: number) => void;\n onDelete?: (id: string) => void;\n onReply?: (parent: Comment, body: string) => void | Promise<void>;\n};\n\n/**\n * Adapts high-level {@link CommentCallbacks} into the id-keyed handlers a\n * {@link CommentThreadList} consumes: resolves the next checklist status from the\n * cycle and extracts mentions from a reply body. Shared by `CommentThread` and\n * `CommentSidePanel` so the adaptation lives in one place.\n */\nexport function buildThreadListHandlers(\n comments: Comment[],\n config: CommentConfig,\n cb: CommentCallbacks,\n): ThreadListHandlers {\n const handlers: ThreadListHandlers = {};\n if (cb.onUpdateStatus) handlers.onUpdateStatus = cb.onUpdateStatus;\n if (cb.onClose) handlers.onClose = cb.onClose;\n if (cb.onDelete) handlers.onDelete = cb.onDelete;\n if (cb.onChecklistToggle) {\n const toggle = cb.onChecklistToggle;\n handlers.onChecklistToggle = (id, index) => {\n const item = comments.find((c) => c.id === id)?.checklist?.[index];\n if (!item) return;\n void toggle(\n id,\n index,\n nextChecklistStatus(item.status, config.checklistStatusCycle),\n );\n };\n }\n if (cb.onReply) {\n const reply = cb.onReply;\n const onMention = cb.onMention;\n handlers.onReply = async (parent, body) => {\n const mentions = matchMentionsInBody(body, config.mentionables ?? []);\n await reply({\n parentId: parent.id,\n body,\n anchor: parent.anchor ?? null,\n ...(mentions.length > 0 ? { mentions } : {}),\n });\n for (const mention of mentions) onMention?.(mention, { body });\n };\n }\n return handlers;\n}\n"],"names":[],"mappings":"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"}
|
package/dist/comments.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const commentTypes = require("./comments/comment-types.cjs");
|
|
4
4
|
const commentUtils = require("./comments/comment-utils.cjs");
|
|
5
|
+
const commentStage = require("./lib/comment-stage.cjs");
|
|
5
6
|
const CommentMarkdown = require("./comments/CommentMarkdown.cjs");
|
|
6
7
|
const CommentAuthor = require("./comments/CommentAuthor.cjs");
|
|
7
8
|
const MentionTextarea = require("./comments/MentionTextarea.cjs");
|
|
@@ -33,16 +34,16 @@ exports.hasActiveFilters = commentUtils.hasActiveFilters;
|
|
|
33
34
|
exports.isUnresolved = commentUtils.isUnresolved;
|
|
34
35
|
exports.matchMentionsInBody = commentUtils.matchMentionsInBody;
|
|
35
36
|
exports.nextChecklistStatus = commentUtils.nextChecklistStatus;
|
|
36
|
-
exports.resolveCommentStage = commentUtils.resolveCommentStage;
|
|
37
37
|
exports.resolveFacetOption = commentUtils.resolveFacetOption;
|
|
38
38
|
exports.resolveStatusConfig = commentUtils.resolveStatusConfig;
|
|
39
39
|
exports.selectAnchorThreads = commentUtils.selectAnchorThreads;
|
|
40
|
-
exports.selectCommentThreadsByStage = commentUtils.selectCommentThreadsByStage;
|
|
41
40
|
exports.selectUnresolvedThreads = commentUtils.selectUnresolvedThreads;
|
|
42
41
|
exports.sortReplies = commentUtils.sortReplies;
|
|
43
|
-
exports.statusForCommentStage = commentUtils.statusForCommentStage;
|
|
44
42
|
exports.toneToBadgeTone = commentUtils.toneToBadgeTone;
|
|
45
43
|
exports.truncatePlain = commentUtils.truncatePlain;
|
|
44
|
+
exports.resolveCommentStage = commentStage.resolveCommentStage;
|
|
45
|
+
exports.selectCommentThreadsByStage = commentStage.selectCommentThreadsByStage;
|
|
46
|
+
exports.statusForCommentStage = commentStage.statusForCommentStage;
|
|
46
47
|
exports.CommentMarkdown = CommentMarkdown.CommentMarkdown;
|
|
47
48
|
exports.CommentAuthorAvatar = CommentAuthor.CommentAuthorAvatar;
|
|
48
49
|
exports.MentionTextarea = MentionTextarea.MentionTextarea;
|
package/dist/comments.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comments.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"comments.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/comments.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './comments/comment-types';
|
|
2
2
|
export * from './comments/comment-utils';
|
|
3
|
+
export { resolveCommentStage, selectCommentThreadsByStage, statusForCommentStage, } from './lib/comment-stage';
|
|
3
4
|
export { CommentMarkdown, type CommentMarkdownProps, } from './comments/CommentMarkdown';
|
|
4
5
|
export { CommentAuthorAvatar, type CommentAuthorAvatarProps, } from './comments/CommentAuthor';
|
|
5
6
|
export { MentionTextarea, type MentionTextareaProps, } from './comments/MentionTextarea';
|
package/dist/comments.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comments.d.ts","sourceRoot":"","sources":["../src/comments.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"comments.d.ts","sourceRoot":"","sources":["../src/comments.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,OAAO,EACL,mBAAmB,EACnB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,GAC1B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,mBAAmB,EACnB,KAAK,wBAAwB,GAC9B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,GAC1B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,GAC1B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,iBAAiB,EACjB,KAAK,sBAAsB,GAC5B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,aAAa,EACb,KAAK,kBAAkB,GACxB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACxF,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,wBAAwB,EACxB,+BAA+B,EAC/B,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,oBAAoB,GAC1B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,gBAAgB,EAChB,KAAK,qBAAqB,GAC3B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,GAC1B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,gBAAgB,EAChB,KAAK,qBAAqB,GAC3B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,GAC1B,MAAM,4BAA4B,CAAC"}
|
package/dist/comments.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_CHECKLIST_CYCLE, DEFAULT_COMMENT_STATUSES, DOCUMENT_ANCHOR } from "./comments/comment-types.js";
|
|
2
|
-
import { applyCommentFilters, authorDisplayName, buildReplyMap, buildThreadListHandlers, commentCountForPrefix, defaultStatusValue, deriveAnchorCounts, deriveAnchorMeta, emptyCommentFilters, getRoots, groupByFacet, hasActiveFilters, isUnresolved, matchMentionsInBody, nextChecklistStatus,
|
|
2
|
+
import { applyCommentFilters, authorDisplayName, buildReplyMap, buildThreadListHandlers, commentCountForPrefix, defaultStatusValue, deriveAnchorCounts, deriveAnchorMeta, emptyCommentFilters, getRoots, groupByFacet, hasActiveFilters, isUnresolved, matchMentionsInBody, nextChecklistStatus, resolveFacetOption, resolveStatusConfig, selectAnchorThreads, selectUnresolvedThreads, sortReplies, toneToBadgeTone, truncatePlain } from "./comments/comment-utils.js";
|
|
3
|
+
import { resolveCommentStage, selectCommentThreadsByStage, statusForCommentStage } from "./lib/comment-stage.js";
|
|
3
4
|
import { CommentMarkdown } from "./comments/CommentMarkdown.js";
|
|
4
5
|
import { CommentAuthorAvatar } from "./comments/CommentAuthor.js";
|
|
5
6
|
import { MentionTextarea } from "./comments/MentionTextarea.js";
|
package/dist/comments.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comments.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"comments.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;"}
|
|
@@ -85,6 +85,7 @@ function RangeSlider({
|
|
|
85
85
|
min,
|
|
86
86
|
max: upper,
|
|
87
87
|
step,
|
|
88
|
+
stepBase: min,
|
|
88
89
|
value: lower,
|
|
89
90
|
onChange: (next) => onChange([next, upper])
|
|
90
91
|
}
|
|
@@ -97,6 +98,7 @@ function RangeSlider({
|
|
|
97
98
|
min: lower,
|
|
98
99
|
max,
|
|
99
100
|
step,
|
|
101
|
+
stepBase: min,
|
|
100
102
|
value: upper,
|
|
101
103
|
onChange: (next) => onChange([lower, next])
|
|
102
104
|
}
|
|
@@ -110,6 +112,7 @@ function EditableBound({
|
|
|
110
112
|
min,
|
|
111
113
|
max,
|
|
112
114
|
step,
|
|
115
|
+
stepBase,
|
|
113
116
|
value,
|
|
114
117
|
onChange
|
|
115
118
|
}) {
|
|
@@ -129,7 +132,7 @@ function EditableBound({
|
|
|
129
132
|
if (event.currentTarget.value === "") return;
|
|
130
133
|
const next = Number(event.currentTarget.value);
|
|
131
134
|
if (!Number.isFinite(next)) throw new Error(`${ariaLabel} must be a finite number`);
|
|
132
|
-
onChange(
|
|
135
|
+
onChange(snapToStep(next, { base: stepBase, step, min, max }));
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
138
|
)
|
|
@@ -144,6 +147,17 @@ function normalizeRangeValue(value, min, max) {
|
|
|
144
147
|
function clampNumber(value, min, max) {
|
|
145
148
|
return Math.min(max, Math.max(min, value));
|
|
146
149
|
}
|
|
150
|
+
function snapToStep(value, { base, step, min, max }) {
|
|
151
|
+
if (!(step > 0)) return clampNumber(value, min, max);
|
|
152
|
+
const aligned = base + Math.round((value - base) / step) * step;
|
|
153
|
+
if (aligned < min) {
|
|
154
|
+
return clampNumber(base + Math.ceil((min - base) / step) * step, min, max);
|
|
155
|
+
}
|
|
156
|
+
if (aligned > max) {
|
|
157
|
+
return clampNumber(base + Math.floor((max - base) / step) * step, min, max);
|
|
158
|
+
}
|
|
159
|
+
return aligned;
|
|
160
|
+
}
|
|
147
161
|
function valueToPercent(value, min, max) {
|
|
148
162
|
if (max <= min) return 0;
|
|
149
163
|
return (value - min) / (max - min) * 100;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RangeSlider.cjs","sources":["../../src/components/RangeSlider.tsx"],"sourcesContent":["import { cn } from \"../lib/utils\";\n\nexport type RangeSliderValue = [number, number];\n\nexport type RangeSliderProps = {\n min: number;\n max: number;\n value: RangeSliderValue;\n onChange: (value: RangeSliderValue) => void;\n step?: number;\n ariaLabelMin?: string;\n ariaLabelMax?: string;\n /** Shows number fields below the track for exact keyboard entry. */\n editable?: boolean;\n className?: string;\n trackClassName?: string;\n rangeClassName?: string;\n thumbClassName?: string;\n};\n\nconst thumbClassName =\n \"[&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:mt-[-5px] [&::-webkit-slider-thumb]:size-3.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border [&::-webkit-slider-thumb]:border-background/80 [&::-webkit-slider-thumb]:bg-muted-foreground [&::-webkit-slider-thumb]:shadow-sm [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:size-3.5 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border [&::-moz-range-thumb]:border-background/80 [&::-moz-range-thumb]:bg-muted-foreground [&::-moz-range-thumb]:shadow-sm\";\n\nexport function RangeSlider({\n min,\n max,\n value,\n onChange,\n step = 1,\n ariaLabelMin = \"Minimum value\",\n ariaLabelMax = \"Maximum value\",\n editable = false,\n className,\n trackClassName,\n rangeClassName,\n thumbClassName: thumbOverrideClassName,\n}: RangeSliderProps) {\n const [lower, upper] = normalizeRangeValue(value, min, max);\n\n return (\n <div className={cn(editable && \"space-y-2\", className)}>\n <div className=\"relative h-6\">\n <div\n className={cn(\n \"absolute left-0 right-0 top-1/2 h-1 -translate-y-1/2 rounded-full bg-border\",\n trackClassName,\n )}\n />\n <div\n className={cn(\n \"absolute top-1/2 h-1 -translate-y-1/2 rounded-full bg-primary/70\",\n rangeClassName,\n )}\n style={{\n left: `${valueToPercent(lower, min, max)}%`,\n right: `${100 - valueToPercent(upper, min, max)}%`,\n }}\n />\n <input\n type=\"range\"\n aria-label={ariaLabelMin}\n min={min}\n max={max}\n step={step}\n value={lower}\n className={cn(baseSliderClassName, thumbClassName, thumbOverrideClassName)}\n style={{ zIndex: lower >= max ? 30 : 20 }}\n onChange={(event) => {\n const nextLower = clampNumber(Number(event.target.value), min, max);\n onChange([Math.min(nextLower, upper), upper]);\n }}\n />\n <input\n type=\"range\"\n aria-label={ariaLabelMax}\n min={min}\n max={max}\n step={step}\n value={upper}\n className={cn(baseSliderClassName, thumbClassName, thumbOverrideClassName)}\n onChange={(event) => {\n const nextUpper = clampNumber(Number(event.target.value), min, max);\n onChange([lower, Math.max(nextUpper, lower)]);\n }}\n />\n </div>\n {editable ? (\n <div className=\"grid grid-cols-2 gap-2\">\n <EditableBound\n label=\"Min\"\n ariaLabel={`Edit ${lowercaseFirst(ariaLabelMin)}`}\n min={min}\n max={upper}\n step={step}\n value={lower}\n onChange={(next) => onChange([next, upper])}\n />\n <EditableBound\n label=\"Max\"\n ariaLabel={`Edit ${lowercaseFirst(ariaLabelMax)}`}\n min={lower}\n max={max}\n step={step}\n value={upper}\n onChange={(next) => onChange([lower, next])}\n />\n </div>\n ) : null}\n </div>\n );\n}\n\nfunction EditableBound({\n label,\n ariaLabel,\n min,\n max,\n step,\n value,\n onChange,\n}: {\n label: string;\n ariaLabel: string;\n min: number;\n max: number;\n step: number;\n value: number;\n onChange: (value: number) => void;\n}) {\n return (\n <label className=\"grid grid-cols-[auto_minmax(0,1fr)] items-center gap-1.5 text-[11px] text-muted-foreground\">\n <span>{label}</span>\n <input\n aria-label={ariaLabel}\n className=\"h-7 min-w-0 rounded-md border border-input bg-background px-2 text-right font-mono text-xs tabular-nums text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n max={max}\n min={min}\n step={step}\n type=\"number\"\n value={value}\n onChange={(event) => {\n if (event.currentTarget.value === \"\") return;\n const next = Number(event.currentTarget.value);\n if (!Number.isFinite(next)) throw new Error(`${ariaLabel} must be a finite number`);\n onChange(clampNumber(next, min, max));\n }}\n />\n </label>\n );\n}\n\nconst baseSliderClassName =\n \"pointer-events-none absolute inset-0 h-6 w-full appearance-none bg-transparent [&::-webkit-slider-runnable-track]:h-1 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-transparent [&::-moz-range-track]:h-1 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-transparent\";\n\nfunction normalizeRangeValue(value: RangeSliderValue, min: number, max: number): RangeSliderValue {\n const lower = clampNumber(Math.min(value[0], value[1]), min, max);\n const upper = clampNumber(Math.max(value[0], value[1]), min, max);\n return [lower, upper];\n}\n\nfunction clampNumber(value: number, min: number, max: number) {\n return Math.min(max, Math.max(min, value));\n}\n\nfunction valueToPercent(value: number, min: number, max: number) {\n if (max <= min) return 0;\n return ((value - min) / (max - min)) * 100;\n}\n\nfunction lowercaseFirst(value: string): string {\n return value.charAt(0).toLowerCase() + value.slice(1);\n}\n"],"names":["cn","jsxs","jsx"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RangeSlider.cjs","sources":["../../src/components/RangeSlider.tsx"],"sourcesContent":["import { cn } from \"../lib/utils\";\n\nexport type RangeSliderValue = [number, number];\n\nexport type RangeSliderProps = {\n min: number;\n max: number;\n value: RangeSliderValue;\n onChange: (value: RangeSliderValue) => void;\n step?: number;\n ariaLabelMin?: string;\n ariaLabelMax?: string;\n /** Shows number fields below the track for exact keyboard entry. */\n editable?: boolean;\n className?: string;\n trackClassName?: string;\n rangeClassName?: string;\n thumbClassName?: string;\n};\n\nconst thumbClassName =\n \"[&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:mt-[-5px] [&::-webkit-slider-thumb]:size-3.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border [&::-webkit-slider-thumb]:border-background/80 [&::-webkit-slider-thumb]:bg-muted-foreground [&::-webkit-slider-thumb]:shadow-sm [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:size-3.5 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border [&::-moz-range-thumb]:border-background/80 [&::-moz-range-thumb]:bg-muted-foreground [&::-moz-range-thumb]:shadow-sm\";\n\nexport function RangeSlider({\n min,\n max,\n value,\n onChange,\n step = 1,\n ariaLabelMin = \"Minimum value\",\n ariaLabelMax = \"Maximum value\",\n editable = false,\n className,\n trackClassName,\n rangeClassName,\n thumbClassName: thumbOverrideClassName,\n}: RangeSliderProps) {\n const [lower, upper] = normalizeRangeValue(value, min, max);\n\n return (\n <div className={cn(editable && \"space-y-2\", className)}>\n <div className=\"relative h-6\">\n <div\n className={cn(\n \"absolute left-0 right-0 top-1/2 h-1 -translate-y-1/2 rounded-full bg-border\",\n trackClassName,\n )}\n />\n <div\n className={cn(\n \"absolute top-1/2 h-1 -translate-y-1/2 rounded-full bg-primary/70\",\n rangeClassName,\n )}\n style={{\n left: `${valueToPercent(lower, min, max)}%`,\n right: `${100 - valueToPercent(upper, min, max)}%`,\n }}\n />\n <input\n type=\"range\"\n aria-label={ariaLabelMin}\n min={min}\n max={max}\n step={step}\n value={lower}\n className={cn(baseSliderClassName, thumbClassName, thumbOverrideClassName)}\n style={{ zIndex: lower >= max ? 30 : 20 }}\n onChange={(event) => {\n const nextLower = clampNumber(Number(event.target.value), min, max);\n onChange([Math.min(nextLower, upper), upper]);\n }}\n />\n <input\n type=\"range\"\n aria-label={ariaLabelMax}\n min={min}\n max={max}\n step={step}\n value={upper}\n className={cn(baseSliderClassName, thumbClassName, thumbOverrideClassName)}\n onChange={(event) => {\n const nextUpper = clampNumber(Number(event.target.value), min, max);\n onChange([lower, Math.max(nextUpper, lower)]);\n }}\n />\n </div>\n {editable ? (\n <div className=\"grid grid-cols-2 gap-2\">\n <EditableBound\n label=\"Min\"\n ariaLabel={`Edit ${lowercaseFirst(ariaLabelMin)}`}\n min={min}\n max={upper}\n step={step}\n stepBase={min}\n value={lower}\n onChange={(next) => onChange([next, upper])}\n />\n <EditableBound\n label=\"Max\"\n ariaLabel={`Edit ${lowercaseFirst(ariaLabelMax)}`}\n min={lower}\n max={max}\n step={step}\n stepBase={min}\n value={upper}\n onChange={(next) => onChange([lower, next])}\n />\n </div>\n ) : null}\n </div>\n );\n}\n\nfunction EditableBound({\n label,\n ariaLabel,\n min,\n max,\n step,\n stepBase,\n value,\n onChange,\n}: {\n label: string;\n ariaLabel: string;\n min: number;\n max: number;\n step: number;\n /** Origin the `step` grid is measured from — the slider's own `min`. */\n stepBase: number;\n value: number;\n onChange: (value: number) => void;\n}) {\n return (\n <label className=\"grid grid-cols-[auto_minmax(0,1fr)] items-center gap-1.5 text-[11px] text-muted-foreground\">\n <span>{label}</span>\n <input\n aria-label={ariaLabel}\n className=\"h-7 min-w-0 rounded-md border border-input bg-background px-2 text-right font-mono text-xs tabular-nums text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n max={max}\n min={min}\n step={step}\n type=\"number\"\n value={value}\n onChange={(event) => {\n if (event.currentTarget.value === \"\") return;\n const next = Number(event.currentTarget.value);\n if (!Number.isFinite(next)) throw new Error(`${ariaLabel} must be a finite number`);\n onChange(snapToStep(next, { base: stepBase, step, min, max }));\n }}\n />\n </label>\n );\n}\n\nconst baseSliderClassName =\n \"pointer-events-none absolute inset-0 h-6 w-full appearance-none bg-transparent [&::-webkit-slider-runnable-track]:h-1 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-transparent [&::-moz-range-track]:h-1 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-transparent\";\n\nfunction normalizeRangeValue(value: RangeSliderValue, min: number, max: number): RangeSliderValue {\n const lower = clampNumber(Math.min(value[0], value[1]), min, max);\n const upper = clampNumber(Math.max(value[0], value[1]), min, max);\n return [lower, upper];\n}\n\nfunction clampNumber(value: number, min: number, max: number) {\n return Math.min(max, Math.max(min, value));\n}\n\n/**\n * Nearest `step`-aligned value measured from `base`, kept inside `[min, max]`.\n * Mirrors what the range inputs enforce, so typed entry cannot produce a value\n * the thumbs could never reach.\n */\nfunction snapToStep(\n value: number,\n { base, step, min, max }: { base: number; step: number; min: number; max: number },\n): number {\n if (!(step > 0)) return clampNumber(value, min, max);\n const aligned = base + Math.round((value - base) / step) * step;\n if (aligned < min) {\n return clampNumber(base + Math.ceil((min - base) / step) * step, min, max);\n }\n if (aligned > max) {\n return clampNumber(base + Math.floor((max - base) / step) * step, min, max);\n }\n return aligned;\n}\n\nfunction valueToPercent(value: number, min: number, max: number) {\n if (max <= min) return 0;\n return ((value - min) / (max - min)) * 100;\n}\n\nfunction lowercaseFirst(value: string): string {\n return value.charAt(0).toLowerCase() + value.slice(1);\n}\n"],"names":["cn","jsxs","jsx"],"mappings":"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"}
|