Skip to content

About

Reusable, plug-and-play threaded comments and review trees for React. Features infinite nesting, pagination, role-based replies, emoji search, attachments, and plain JSON normalization.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

ThreadCraft

CI React 18+ TypeScript MIT license

Open the Vite example in StackBlitz

Demo

Online Local
Open the Vite example in StackBlitz npm ci β†’ npm run dev:example β†’ localhost:7002
Example Fixture Features
GitHub issue github-issue.json Nested replies, reactions, attachments, async β€œSee more”
Product review reviews.json Ratings, business replies, async β€œSee more”
AI chat ai-chat.json Provider/model selectors, Gemini and Groq key entry, live replies, top-paged history

Install and import

npm install @simplishelf/threadcraft
import {
  ChatThread,
  GitHubIssueThread,
  ReviewThread,
  ThreadedDiscussion,
} from "@simplishelf/threadcraft";
import type { ThreadCraftData } from "@simplishelf/threadcraft";
import "@simplishelf/threadcraft/styles.css";

const issueData: ThreadCraftData = {
  kind: "issue",
  title: "Export button stays disabled",
  comments: [],
};

const reviewData: ThreadCraftData = {
  kind: "review",
  title: "Linen Everyday Shirt",
  showRating: true,
  comments: [],
};

const chatData: ThreadCraftData = {
  kind: "chat",
  title: "AI Assistant",
  comments: [],
  hasMore: true,
};

export function DiscussionExample() {
  return (
    <>
      <GitHubIssueThread data={issueData} />
      <ReviewThread data={reviewData} />
      <ChatThread data={chatData} />
      <ThreadedDiscussion data={issueData} variant="issue" />
    </>
  );
}

AI provider controls

Provider and model selectors are opt-in for ChatThread. On wide layouts, the settings panel sits beside the conversation. On narrow layouts, a compact model summary opens the same controls in a dialog. A host can preconfigure credentials and hide the key field while keeping both selectors visible:

import type { ThreadCraftChatProviderOption } from "@simplishelf/threadcraft";

const chatProviders: ThreadCraftChatProviderOption[] = [
  { id: "gemini", label: "Gemini", apiKey: credentials.gemini },
  { id: "groq", label: "Groq", apiKey: credentials.groq },
];

<ChatThread
  data={chatData}
  showChatProviderControls
  chatProviders={chatProviders}
  showChatApiKeyInput={false}
  onLoadChatModels={({ provider }) => loadModelsFromYourBackend(provider)}
  onSubmitComment={(payload) => sendMessage({
    text: payload.text,
    provider: payload.chatProvider,
    model: payload.chatModel,
  })}
/>

To let demo users provide their own key, set showChatApiKeyInput to true and handle onChatApiKeyChange. Keys are not persisted or included in ThreadCraftSubmitPayload. The Vite demo sends chat requests to the selected provider from the browser; production apps can load models and submit messages through their own backend.

React Native / Expo

import { ReviewThread } from "@simplishelf/threadcraft/native";
import type { ThreadCraftData } from "@simplishelf/threadcraft/native";

const reviewData: ThreadCraftData = {
  kind: "review",
  title: "Product reviews",
  showRating: true,
  comments: [],
};

<ReviewThread data={reviewData} showHeader={false} />

The native entry uses React Native components and does not require the CSS import. When attachments are enabled, pass onPickAttachments and connect it to the app's native file picker.

Integrations

Use case Component Data
GitHub issue GitHubIssueThread kind: "issue"; optional issue status, URL, author, and body
Product review ReviewThread kind: "review"; set showRating: true to display review stars; use allowRatingInput to control rating entry
AI chat ChatThread kind: "chat"; optionally enable provider/model controls with showChatProviderControls, chatProviders, and onLoadChatModels; onSubmitComment receives the selected provider and model
Dynamic variant ThreadedDiscussion Optional variant; defaults to data.kind, then "issue"

Component options

Prop Type Default Description
data ThreadCraftData Required Metadata and initial comments
variant "issue" | "review" | "chat" data.kind ?? "issue" Used by ThreadedDiscussion; wrapper components select their own variant
currentUser string "You" Author for locally created comments
showHeader boolean true Show the issue, product, or chat header
initialRootLimit number 50 Root comments shown first; their nested replies remain visible
loadMoreCount number initialRootLimit or 50 Root comments revealed or requested per β€œSee more”
allowReplies boolean true Show reply actions
allowNewComments boolean true Show the root-level composer
allowAttachments boolean true Show attachments; native apps provide onPickAttachments
allowReactions boolean false for chat; true otherwise Show reaction actions
showStreamHeading boolean true Show the discussion, review, or message count heading
loadMorePlacement "start" | "end" "start" for chat, "end" otherwise Place a loaded page before or after the current roots
loadMoreLabel string Variant-specific Idle pagination button text
loadingMoreLabel string Variant-specific Pagination loading text
loadMoreErrorText string Variant-specific Error shown when a page request rejects
emptyMessage string Variant-specific Text shown when the discussion has no comments
inputPlaceholder string Variant-specific Composer placeholder
composerLabel string Variant-specific Accessible composer label
submitButtonLabel string Variant-specific Composer submit button text
submittingLabel string Variant-specific Composer submit loading text
allowEmoji boolean false for chat; true otherwise Show the emoji picker
typingIndicator boolean false Show a chat assistant typing indicator
typingIndicatorLabel string "AI assistant is thinking" Accessible typing indicator label
isSubmitting boolean false Disable the composer while the host app processes a message
controlledComments boolean false Sync parent-owned data.comments; recommended for AI chat
showChatProviderControls boolean false Show provider/model settings beside wide chats and in a dialog on narrow chats
chatProviders ThreadCraftChatProviderOption[] [] Provider labels, optional API keys, and preloaded models
showChatApiKeyInput boolean true Show a password input while provider controls are enabled; set false when the host supplies credentials
selectedChatProvider string First provider Controlled provider selection
selectedChatModel string First available model Controlled model selection
onChatProviderChange (provider: string) => void β€” Observe provider selection changes
onChatModelChange (model: string) => void β€” Observe model selection changes
onChatApiKeyChange (provider: string, apiKey: string) => void β€” Receive key input changes; the package does not persist keys
onLoadChatModels (request: ThreadCraftLoadChatModelsRequest) => Promise<ThreadCraftChatModel[]> β€” Fetch models for the selected provider; may call a host backend
renderCommentBody (comment: ThreadCraftComment) => ReactNode Plain text Supply an app renderer, for example sanitized Markdown
showRating boolean data.showRating Display comment ratings in review threads
allowRatingInput boolean showRating Show the root composer rating control
replyAuthorTypes ThreadCraftReplyAuthorType[] Issue/review defaults Allowed reply identities; a one-item list fixes the identity
newCommentAuthorType ThreadCraftReplyAuthorType customer for reviews, support for issues, user for chat Identity on root submissions
newCommentPosition "start" | "end" "end" Place successful root submissions at the beginning or end
identityFields ThreadCraftIdentityFields β€” Configure author name/email fields in the composer
onPickAttachments () => Promise<ThreadCraftAttachment[]> β€” Native attachment picker adapter
onLoadMore (request: ThreadCraftLoadMoreRequest) => Promise<ThreadCraftLoadMoreResult> β€” Fetch another root-comment page; chat mode places older messages at the top
onSubmitComment (payload: ThreadCraftSubmitPayload) => Promise<ThreadCraftComment | void> Local update Persist a comment; return the saved comment or void
onReact (comment: ThreadCraftComment, emoji: string) => void β€” Observe a locally applied reaction toggle

JSON shape

Discussion: ThreadCraftData

Field Type Required Meaning
id string | number Discussion identifier
kind "issue" | "review" | "chat" Default component variant
title string βœ“ Issue, product, or chat title
author string Issue opener or store name
authorAvatarUrl string Header avatar URL
createdAt string Header timestamp
body string Issue/review description
status string Issue status, such as "Open"
url string Link to the source issue or product
comments ThreadCraftComment[] βœ“ Initial comment page; nested or flat
authorTypeStyles Record<string, ThreadCraftAuthorTypeStyle> Custom chip, card, and avatar colors by authorType
showRating boolean Display rating stars on review comments
hasMore boolean Whether more root comments are available
totalRootComments number Total root count shown in the discussion heading

For chat, comments are displayed oldest to newest. Set hasMore when older messages are available. The chat pagination control appears at the top and expects onLoadMore to return older comments; controlledComments keeps the parent as the source of truth.

Comment: ThreadCraftComment

Field Type Meaning
id string | number Required comment identifier
author string Required author name
text string Preferred comment content
body string Content alias; text takes precedence
authorAvatarUrl string Author avatar URL
avatarBackgroundColor string Per-user initials avatar background override
avatarTextColor string Per-user initials avatar text override
avatarBorderColor string Per-user avatar border override
cardColor string Per-comment card background override
cardBorderColor string Per-comment card border override
cardTextColor string Per-comment card text override
createdAt string Comment timestamp; displayed inside chat bubbles in chat mode
parentId string | number | null Flat reply parent; null marks a root
quotedCommentId string | number | null Alternative flat reply parent
quotedText string Quoted parent text
quotedAuthor string Quoted parent author
replies ThreadCraftComment[] Nested replies; recursive
reactions Record<string, number> Emoji-to-count map, e.g. {"πŸ‘": 2}
attachments ThreadCraftAttachmentInput[] Image URLs or attachment objects
rating number Review rating
ratings number Legacy rating alias; rating takes precedence
isCustomer boolean Legacy customer-author flag
isMine boolean Legacy flag treated as customer-authored
authorType string customer, business, support, bot, user, assistant, or custom role

Composer identity: ThreadCraftIdentityFields

Field Shape Meaning
authorName { label, placeholder?, value?, required? } Name input configuration and initial value
authorEmail { label, placeholder?, value?, required?, keyboardType? } Email input configuration and initial value; keyboardType is "default" or "email-address"

Author type style: ThreadCraftAuthorTypeStyle

Add styles to ThreadCraftData.authorTypeStyles, keyed by the comment's authorType. Styles apply to that role on every nesting level. Per-comment avatar and card colors override role defaults.

Color values accept CSS color formats such as hex, rgb(), hsl(), and named colors. authorAvatarUrl accepts an image URL or data URL and replaces the initials avatar.

Field Type Meaning
label string Chip text, e.g. "Verified buyer"
color string Chip label and icon color; any CSS color
backgroundColor string Chip background color
borderColor string Chip border color
icon string Emoji or text shown before the chip label
cardColor string Default comment card background for this role
cardBorderColor string Default comment card border for this role
cardTextColor string Default comment text color for this role
avatarBackgroundColor string Default initials avatar background for this role
avatarTextColor string Default initials color for this role
avatarBorderColor string Default avatar border for this role

Attachment: ThreadCraftAttachmentInput

Shape Fields
URL string "https://example.com/photo.png"
Object name: string required; url?: string; dataUrl?: string; mimeType?: string; size?: number

Comment relationships and content

Input Normalization
Nested replies Preserved as a reply tree
Flat parentId Attached to the matching comment ID
Flat quotedCommentId Attached to the matching comment ID
quotedText or Markdown > quote Used as quote context; matching earlier comment text can infer a parent
Markdown image ![alt](https://...) or HTML <img src="https://..."> in text/body Extracted as an image attachment

GitHub issue JSON

{
  "id": 842,
  "kind": "issue",
  "title": "Export button stays disabled after choosing a date range",
  "author": "Priya Shah",
  "authorAvatarUrl": "https://example.com/priya.png",
  "createdAt": "2026-09-28T14:32:00.000Z",
  "body": "The export button stays disabled after I choose a valid date range.",
  "status": "Open",
  "url": "https://github.com/example/app/issues/842",
  "hasMore": true,
  "totalRootComments": 60,
  "authorTypeStyles": {
    "customer": {
      "label": "Customer",
      "color": "#2546A8",
      "backgroundColor": "#EEF2FF",
      "borderColor": "#C7D2FE",
      "icon": "πŸ‘€",
      "cardColor": "#F3F5FF",
      "cardBorderColor": "#D7DFFF",
      "cardTextColor": "#24304A",
      "avatarBackgroundColor": "#E5ECFF",
      "avatarTextColor": "#315EFB",
      "avatarBorderColor": "#FFFFFF"
    },
    "business": {
      "label": "Business",
      "color": "#087E70",
      "backgroundColor": "#E8FAF5",
      "borderColor": "#A7F3D0",
      "icon": "πŸͺ",
      "cardColor": "#EFFBF7",
      "cardBorderColor": "#BDEBDD",
      "cardTextColor": "#1D3B35",
      "avatarBackgroundColor": "#D8F5EA",
      "avatarTextColor": "#087E70",
      "avatarBorderColor": "#FFFFFF"
    },
    "support": {
      "label": "Support",
      "color": "#673AB7",
      "backgroundColor": "#F3EDFF",
      "borderColor": "#D9C7FF",
      "icon": "πŸ› οΈ",
      "cardColor": "#FBF8FF",
      "cardBorderColor": "#E6D9FF",
      "cardTextColor": "#352A4D",
      "avatarBackgroundColor": "#EFE6FF",
      "avatarTextColor": "#6941C6",
      "avatarBorderColor": "#FFFFFF"
    }
  },
  "comments": [
    {
      "id": "issue-1",
      "author": "Priya Shah",
      "authorType": "customer",
      "authorAvatarUrl": "https://example.com/priya.png",
      "avatarBackgroundColor": "#DBEAFE",
      "avatarTextColor": "#1D4ED8",
      "avatarBorderColor": "#FFFFFF",
      "cardColor": "#FFF7ED",
      "cardBorderColor": "#FED7AA",
      "cardTextColor": "#7C2D12",
      "createdAt": "2026-09-28T15:04:00.000Z",
      "text": "I can reproduce this in Chrome.",
      "reactions": { "πŸ‘": 2, "❀️": 1 },
      "attachments": [
        "https://example.com/screenshot.png",
        {
          "name": "steps.png",
          "url": "https://example.com/steps.png",
          "dataUrl": "data:image/png;base64,...",
          "mimeType": "image/png",
          "size": 24000
        }
      ],
      "replies": [
        {
          "id": "issue-1-reply",
          "author": "Maya Chen",
          "authorType": "support",
          "authorAvatarUrl": "https://example.com/maya.png",
          "createdAt": "2026-09-28T15:31:00.000Z",
          "parentId": "issue-1",
          "quotedCommentId": "issue-1",
          "quotedText": "I can reproduce this in Chrome.",
          "quotedAuthor": "Priya Shah",
          "text": "We found a stale state update and are preparing a fix.",
          "reactions": { "πŸŽ‰": 1 },
          "attachments": [],
          "replies": [
            {
              "id": "issue-1-reply-2",
              "author": "Priya Shah",
              "authorType": "customer",
              "isCustomer": true,
              "isMine": true,
              "body": "Thank you for the update.",
              "replies": []
            }
          ]
        }
      ]
    }
  ]
}

Product review JSON

{
  "id": "linen-shirt",
  "kind": "review",
  "title": "Linen Everyday Shirt",
  "author": "ThreadCraft Store",
  "authorAvatarUrl": "https://example.com/store.png",
  "createdAt": "2026-09-26T10:00:00.000Z",
  "body": "Customer reviews and business replies.",
  "url": "https://example.com/products/linen-shirt",
  "showRating": true,
  "hasMore": true,
  "totalRootComments": 60,
  "authorTypeStyles": {
    "customer": {
      "label": "Verified buyer",
      "color": "#2546A8",
      "backgroundColor": "#EEF2FF",
      "borderColor": "#C7D2FE",
      "icon": "πŸ›οΈ",
      "cardColor": "#F3F5FF",
      "cardBorderColor": "#D7DFFF",
      "cardTextColor": "#24304A",
      "avatarBackgroundColor": "#E5ECFF",
      "avatarTextColor": "#315EFB",
      "avatarBorderColor": "#FFFFFF"
    },
    "business": {
      "label": "ThreadCraft Store",
      "color": "#087E70",
      "backgroundColor": "#E8FAF5",
      "borderColor": "#A7F3D0",
      "icon": "πŸͺ",
      "cardColor": "#EFFBF7",
      "cardBorderColor": "#BDEBDD",
      "cardTextColor": "#1D3B35",
      "avatarBackgroundColor": "#D8F5EA",
      "avatarTextColor": "#087E70",
      "avatarBorderColor": "#FFFFFF"
    },
    "support": {
      "label": "Support",
      "color": "#673AB7",
      "backgroundColor": "#F3EDFF",
      "borderColor": "#D9C7FF",
      "icon": "πŸ’¬",
      "cardColor": "#FBF8FF",
      "cardBorderColor": "#E6D9FF",
      "cardTextColor": "#352A4D",
      "avatarBackgroundColor": "#EFE6FF",
      "avatarTextColor": "#6941C6",
      "avatarBorderColor": "#FFFFFF"
    }
  },
  "comments": [
    {
      "id": "review-1",
      "author": "Morgan Reed",
      "authorType": "customer",
      "authorAvatarUrl": "https://example.com/morgan.png",
      "avatarBackgroundColor": "#FCE7F3",
      "avatarTextColor": "#9D174D",
      "createdAt": "2026-09-26T13:20:00.000Z",
      "text": "Comfortable fabric and a good fit.",
      "rating": 5,
      "ratings": 5,
      "reactions": { "❀️": 3, "πŸ‘": 2 },
      "replies": [
        {
          "id": "review-1-reply",
          "author": "ThreadCraft Store",
          "authorType": "business",
          "authorAvatarUrl": "https://example.com/store.png",
          "text": "Thanks for sharing your experience!",
          "replies": []
        }
      ]
    }
  ]
}

Pagination, replies, and callbacks

API Shape Behavior
onLoadMore request { offset: number, limit: number } Root-comment offset and page size
onLoadMore result { comments: ThreadCraftComment[], hasMore?: boolean } Adds unique roots; chat mode prepends older messages
onSubmitComment payload { text, parentId?, attachments, rating?, authorType?, authorName?, authorEmail?, chatProvider?, chatModel? } Chat fields are included when provider controls are enabled; API keys are never included
Reply roles customer | business | support | user Issue: Support/Customer; review: Customer/Business; chat root: User
onReact (comment, emoji) => void Runs after the component toggles the reaction locally
Submit payload field Type Included when
text string Every submission
parentId string | number Reply
attachments ThreadCraftAttachment[] Every submission; empty when none selected
rating number Root review when rating input is enabled
authorType "customer" | "business" | "support" | "user" Reply or configured root identity
authorName string When the composer includes a name field
authorEmail string When the composer includes an email field
chatProvider string When chat provider controls are enabled
chatModel string When chat provider controls are enabled and a model is selected
Built-in image picker Limit
Files per comment 10
File size 5 MB each
Accepted files Images
Web picker output dataUrl, mimeType, size, name
Native picker onPickAttachments returns ThreadCraftAttachment[]

Exports

Export Kind
GitHubIssueThread, ReviewThread, ChatThread, ThreadedDiscussion Components
GitHubIssueThread, ReviewThread, ChatThread, ThreadedDiscussion from @simplishelf/threadcraft/native React Native components
ThreadCraftId, ThreadCraftVariant, ThreadCraftAttachment, ThreadCraftAttachmentInput, ThreadCraftAuthorTypeStyle, ThreadCraftChatModel, ThreadCraftChatProviderOption, ThreadCraftComment, ThreadCraftData, ThreadCraftDiscussionProps, ThreadCraftIdentityField, ThreadCraftIdentityFields, ThreadCraftLoadChatModelsRequest, ThreadCraftLoadMoreRequest, ThreadCraftLoadMoreResult, ThreadCraftReplyAuthorType, ThreadCraftSubmitPayload Types
buildCommentTree, appendReplyToTree, appendUniqueRootComments, prependUniqueRootComments, countRootComments Helpers

Development

npm ci
npm test
npm run lint
npm run build
npm run dev:example
Resource Path
Vite example examples/vite
Example fixtures examples/vite/src/test-data
License MIT

About

Reusable, plug-and-play threaded comments and review trees for React. Features infinite nesting, pagination, role-based replies, emoji search, attachments, and plain JSON normalization.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages