elseware-ui 2.31.10 → 2.31.12

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/index.mjs CHANGED
@@ -17578,9 +17578,12 @@ function CommentContent({ comment }) {
17578
17578
  var CommentContent_default = CommentContent;
17579
17579
 
17580
17580
  // src/compositions/comment-thread/utils/formatCommentDate.ts
17581
- function formatCommentDate(date) {
17582
- const now = /* @__PURE__ */ new Date();
17581
+ function formatCommentDate(date, format = "relative") {
17583
17582
  const target = new Date(date);
17583
+ if (format === "absolute") {
17584
+ return target.toLocaleString();
17585
+ }
17586
+ const now = /* @__PURE__ */ new Date();
17584
17587
  const seconds = Math.floor((now.getTime() - target.getTime()) / 1e3);
17585
17588
  const intervals = [
17586
17589
  {
@@ -17612,13 +17615,13 @@ function formatCommentDate(date) {
17612
17615
  }
17613
17616
  return "Just now";
17614
17617
  }
17615
- function CommentHeader({ comment }) {
17618
+ function CommentHeader({ comment, config }) {
17616
17619
  const author = comment.author;
17617
17620
  const displayName = author?.firstName || author?.lastName ? `${author?.firstName ?? ""} ${author?.lastName ?? ""}`.trim() : author?.username ?? "Unknown";
17618
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
17621
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [
17619
17622
  /* @__PURE__ */ jsx(Avatar, { src: author?.avatar, size: "sm" }),
17620
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-wrap min-w-0", children: [
17621
- /* @__PURE__ */ jsx("span", { className: "font-semibold text-sm text-gray-900 dark:text-gray-100", children: displayName }),
17623
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [
17624
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: displayName }),
17622
17625
  author?.username && /* @__PURE__ */ jsxs(Fragment, { children: [
17623
17626
  /* @__PURE__ */ jsxs("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: [
17624
17627
  "@",
@@ -17626,7 +17629,7 @@ function CommentHeader({ comment }) {
17626
17629
  ] }),
17627
17630
  /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: "\xB7" })
17628
17631
  ] }),
17629
- /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: formatCommentDate(comment.createdAt) }),
17632
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: formatCommentDate(comment.createdAt, config.dateFormat) }),
17630
17633
  comment.edited && /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 dark:text-gray-500", children: "(edited)" })
17631
17634
  ] })
17632
17635
  ] });
@@ -17719,20 +17722,29 @@ var CommentMenu_default = CommentMenu;
17719
17722
  function CommentRepliesToggle({
17720
17723
  repliesCount,
17721
17724
  collapsed,
17725
+ loading = false,
17722
17726
  onToggle
17723
17727
  }) {
17724
17728
  if (repliesCount <= 0) {
17725
17729
  return null;
17726
17730
  }
17727
- const label = collapsed ? `View ${repliesCount} ${repliesCount === 1 ? "reply" : "replies"}` : "Hide replies";
17731
+ let label = "";
17732
+ if (loading) {
17733
+ label = "Loading replies...";
17734
+ } else if (collapsed) {
17735
+ label = `View ${repliesCount} ${repliesCount === 1 ? "reply" : "replies"}`;
17736
+ } else {
17737
+ label = "Hide replies";
17738
+ }
17728
17739
  return /* @__PURE__ */ jsxs(
17729
17740
  "button",
17730
17741
  {
17731
17742
  type: "button",
17743
+ disabled: loading,
17732
17744
  onClick: onToggle,
17733
- className: "inline-flex w-fit items-center gap-1 text-sm font-medium text-blue-500 transition-colors hover:text-blue-400",
17745
+ className: "inline-flex w-fit items-center gap-1 text-sm font-medium text-blue-500 transition-colors hover:text-blue-400 disabled:cursor-not-allowed disabled:opacity-60",
17734
17746
  children: [
17735
- collapsed ? /* @__PURE__ */ jsx(BiChevronDown, { size: 18 }) : /* @__PURE__ */ jsx(BiChevronUp, { size: 18 }),
17747
+ !loading && (collapsed ? /* @__PURE__ */ jsx(BiChevronDown, { size: 18 }) : /* @__PURE__ */ jsx(BiChevronUp, { size: 18 })),
17736
17748
  /* @__PURE__ */ jsx("span", { children: label })
17737
17749
  ]
17738
17750
  }
@@ -17763,18 +17775,24 @@ function ReplyComposer({
17763
17775
  var ReplyComposer_default = ReplyComposer;
17764
17776
  function CommentItem({
17765
17777
  comment,
17778
+ replies = [],
17779
+ allReplies = {},
17766
17780
  depth = 0,
17767
17781
  config,
17768
17782
  components,
17769
17783
  onLike,
17770
17784
  onDislike,
17771
17785
  onReply,
17786
+ onLoadReplies,
17772
17787
  onEdit,
17773
17788
  onDelete,
17774
17789
  onReport
17775
17790
  }) {
17776
- const [collapsed, setCollapsed] = useState(false);
17791
+ const [collapsed, setCollapsed] = useState(true);
17777
17792
  const [isReplying, setIsReplying] = useState(false);
17793
+ const [menuOpen, setMenuOpen] = useState(false);
17794
+ const [loadingReplies, setLoadingReplies] = useState(false);
17795
+ const [repliesLoaded, setRepliesLoaded] = useState(false);
17778
17796
  const resolvedDepth = Math.min(depth, config.maxDepth);
17779
17797
  const {
17780
17798
  Header: HeaderComponent = CommentHeader_default,
@@ -17787,100 +17805,108 @@ function CommentItem({
17787
17805
  async function handleReply(content) {
17788
17806
  await onReply?.(comment, content);
17789
17807
  setIsReplying(false);
17808
+ setCollapsed(false);
17809
+ setRepliesLoaded(true);
17790
17810
  }
17791
- const [menuOpen, setMenuOpen] = useState(false);
17792
- return /* @__PURE__ */ jsxs(
17811
+ async function handleToggleReplies() {
17812
+ const nextCollapsed = !collapsed;
17813
+ if (!nextCollapsed && !repliesLoaded && onLoadReplies) {
17814
+ try {
17815
+ setLoadingReplies(true);
17816
+ await onLoadReplies(comment);
17817
+ setRepliesLoaded(true);
17818
+ } finally {
17819
+ setLoadingReplies(false);
17820
+ }
17821
+ }
17822
+ setCollapsed(nextCollapsed);
17823
+ }
17824
+ return /* @__PURE__ */ jsx(
17793
17825
  "div",
17794
17826
  {
17795
17827
  className: "flex flex-col gap-4",
17796
17828
  style: {
17797
17829
  paddingLeft: resolvedDepth * config.indentation
17798
17830
  },
17799
- children: [
17800
- /* @__PURE__ */ jsx("div", { className: "flex gap-3", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col gap-3 min-w-0", children: [
17801
- /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
17802
- /* @__PURE__ */ jsx(HeaderComponent, { comment }),
17803
- /* @__PURE__ */ jsx(
17804
- MenuComponent,
17805
- {
17806
- comment,
17807
- open: menuOpen,
17808
- setOpen: setMenuOpen,
17809
- canEdit: comment.permissions?.canEdit,
17810
- canDelete: comment.permissions?.canDelete,
17811
- canReport: comment.permissions?.canReport,
17812
- onEdit: () => onEdit?.(comment),
17813
- onDelete: () => onDelete?.(comment),
17814
- onReport: () => onReport?.(comment)
17815
- }
17816
- )
17817
- ] }),
17818
- /* @__PURE__ */ jsx(ContentComponent, { comment }),
17831
+ children: /* @__PURE__ */ jsx("div", { className: "flex gap-3", children: /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-3", children: [
17832
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
17833
+ /* @__PURE__ */ jsx(HeaderComponent, { comment, config }),
17819
17834
  /* @__PURE__ */ jsx(
17820
- ActionsComponent,
17835
+ MenuComponent,
17821
17836
  {
17822
17837
  comment,
17823
- onLike: () => onLike?.(comment),
17824
- onDislike: () => onDislike?.(comment),
17825
- onReply: () => setIsReplying((prev) => !prev)
17826
- }
17827
- ),
17828
- isReplying && /* @__PURE__ */ jsx(
17829
- ReplyComposerComponent,
17830
- {
17831
- avatar: comment.author?.avatar,
17832
- username: comment.author?.username,
17833
- placeholder: `Reply to @${comment.author?.username}`,
17834
- submitLabel: "Reply",
17835
- onSubmit: handleReply
17836
- }
17837
- ),
17838
- !!comment.replies?.length && /* @__PURE__ */ jsx(
17839
- RepliesToggleComponent,
17840
- {
17841
- collapsed,
17842
- repliesCount: comment.replies.length,
17843
- onToggle: () => setCollapsed((prev) => !prev)
17838
+ open: menuOpen,
17839
+ setOpen: setMenuOpen,
17840
+ canEdit: comment.permissions?.canEdit,
17841
+ canDelete: comment.permissions?.canDelete,
17842
+ canReport: comment.permissions?.canReport,
17843
+ onEdit: () => onEdit?.(comment),
17844
+ onDelete: () => onDelete?.(comment),
17845
+ onReport: () => onReport?.(comment)
17844
17846
  }
17845
17847
  )
17846
- ] }) }),
17847
- !collapsed && !!comment.replies?.length && /* @__PURE__ */ jsx(
17848
- "div",
17848
+ ] }),
17849
+ /* @__PURE__ */ jsx(ContentComponent, { comment }),
17850
+ /* @__PURE__ */ jsx(
17851
+ ActionsComponent,
17849
17852
  {
17850
- className: cn(
17851
- "border-l border-gray-200 dark:border-neutral-800",
17852
- "pl-4 ml-5"
17853
- ),
17854
- children: /* @__PURE__ */ jsx(
17855
- CommentList_default,
17856
- {
17857
- comments: comment.replies,
17858
- depth: depth + 1,
17859
- config,
17860
- components,
17861
- onLike,
17862
- onDislike,
17863
- onReply,
17864
- onEdit,
17865
- onDelete,
17866
- onReport
17867
- }
17868
- )
17853
+ comment,
17854
+ onLike: () => onLike?.(comment),
17855
+ onDislike: () => onDislike?.(comment),
17856
+ onReply: () => setIsReplying((prev) => !prev)
17869
17857
  }
17870
- )
17871
- ]
17858
+ ),
17859
+ isReplying && /* @__PURE__ */ jsx(
17860
+ ReplyComposerComponent,
17861
+ {
17862
+ avatar: comment.author?.avatar,
17863
+ username: comment.author?.username,
17864
+ placeholder: `Reply to @${comment.author?.username}`,
17865
+ submitLabel: "Reply",
17866
+ onSubmit: handleReply
17867
+ }
17868
+ ),
17869
+ comment.replyCount > 0 && /* @__PURE__ */ jsx(
17870
+ RepliesToggleComponent,
17871
+ {
17872
+ collapsed,
17873
+ loading: loadingReplies,
17874
+ repliesCount: comment.replyCount,
17875
+ onToggle: handleToggleReplies
17876
+ }
17877
+ ),
17878
+ !collapsed && replies.length > 0 && /* @__PURE__ */ jsx("div", { className: "ml-5 border-l border-gray-200 pl-4 dark:border-neutral-800", children: /* @__PURE__ */ jsx(
17879
+ CommentList_default,
17880
+ {
17881
+ comments: replies,
17882
+ replies: allReplies,
17883
+ depth: depth + 1,
17884
+ config,
17885
+ components,
17886
+ onLike,
17887
+ onDislike,
17888
+ onReply,
17889
+ onLoadReplies,
17890
+ onEdit,
17891
+ onDelete,
17892
+ onReport
17893
+ }
17894
+ ) })
17895
+ ] }) })
17872
17896
  }
17873
17897
  );
17874
17898
  }
17875
17899
  var CommentItem_default = CommentItem;
17876
17900
  function CommentList({
17877
17901
  comments,
17902
+ replies = {},
17878
17903
  depth = 0,
17879
17904
  config,
17880
17905
  components,
17881
17906
  onLike,
17882
17907
  onDislike,
17883
17908
  onReply,
17909
+ onLoadReplies,
17884
17910
  onEdit,
17885
17911
  onDelete,
17886
17912
  onReport
@@ -17892,12 +17918,15 @@ function CommentList({
17892
17918
  CommentItem_default,
17893
17919
  {
17894
17920
  comment,
17921
+ replies: replies[comment.id] ?? [],
17922
+ allReplies: replies,
17895
17923
  depth,
17896
17924
  config,
17897
17925
  components,
17898
17926
  onLike,
17899
17927
  onDislike,
17900
17928
  onReply,
17929
+ onLoadReplies,
17901
17930
  onEdit,
17902
17931
  onDelete,
17903
17932
  onReport
@@ -17907,64 +17936,20 @@ function CommentList({
17907
17936
  }
17908
17937
  var CommentList_default = CommentList;
17909
17938
 
17910
- // src/compositions/comment-thread/utils/buildCommentTree.ts
17911
- function buildCommentTree(comments) {
17912
- const map = /* @__PURE__ */ new Map();
17913
- const roots = [];
17914
- comments.forEach((comment) => {
17915
- map.set(comment.id, {
17916
- ...comment,
17917
- replies: []
17918
- });
17919
- });
17920
- comments.forEach((comment) => {
17921
- const current = map.get(comment.id);
17922
- if (!comment.parentCommentId) {
17923
- roots.push(current);
17924
- return;
17925
- }
17926
- const parent = map.get(comment.parentCommentId);
17927
- if (parent) {
17928
- parent.replies.push(current);
17929
- }
17930
- });
17931
- return roots;
17932
- }
17933
-
17934
17939
  // src/compositions/comment-thread/utils/updateComment.ts
17935
17940
  function updateComment(comments, commentId, updater) {
17936
- return comments.map((comment) => {
17937
- if (comment.id === commentId) {
17938
- return updater(comment);
17939
- }
17940
- return {
17941
- ...comment,
17942
- replies: comment.replies ? updateComment(comment.replies, commentId, updater) : []
17943
- };
17944
- });
17945
- }
17946
-
17947
- // src/compositions/comment-thread/utils/insertReply.ts
17948
- function insertReply(comments, parentId, reply) {
17949
- return comments.map((comment) => {
17950
- if (comment.id === parentId) {
17951
- return {
17952
- ...comment,
17953
- replies: [...comment.replies || [], reply]
17954
- };
17955
- }
17956
- return {
17957
- ...comment,
17958
- replies: comment.replies ? insertReply(comment.replies, parentId, reply) : []
17959
- };
17960
- });
17941
+ return comments.map(
17942
+ (comment) => comment.id === commentId ? updater(comment) : comment
17943
+ );
17961
17944
  }
17962
17945
 
17963
17946
  // src/compositions/comment-thread/constants/comment.constants.ts
17964
17947
  var DEFAULT_COMMENT_THREAD_CONFIG = {
17965
17948
  maxDepth: 5,
17966
17949
  indentation: 24,
17967
- dateFormat: "relative"
17950
+ dateFormat: "relative",
17951
+ commentsPerPage: 20,
17952
+ repliesPerPage: 10
17968
17953
  };
17969
17954
  function CommentThread({
17970
17955
  currentUser,
@@ -17978,6 +17963,9 @@ function CommentThread({
17978
17963
  }) {
17979
17964
  const [comments, setComments] = useState([]);
17980
17965
  const [isLoadingComments, setIsLoadingComments] = useState(true);
17966
+ const [replyCache, setReplyCache] = useState(
17967
+ {}
17968
+ );
17981
17969
  const ComposerComponent = components?.Composer ?? CommentComposer_default;
17982
17970
  const resolvedConfig = {
17983
17971
  ...DEFAULT_COMMENT_THREAD_CONFIG,
@@ -17986,14 +17974,27 @@ function CommentThread({
17986
17974
  useEffect(() => {
17987
17975
  async function loadComments() {
17988
17976
  try {
17989
- const result = await dataSource.getComments();
17990
- setComments(buildCommentTree(result));
17977
+ const result = await dataSource.getComments({
17978
+ parentCommentId: null,
17979
+ limit: resolvedConfig.commentsPerPage
17980
+ });
17981
+ setComments(result.items);
17991
17982
  } finally {
17992
17983
  setIsLoadingComments(false);
17993
17984
  }
17994
17985
  }
17995
17986
  loadComments();
17996
- }, [dataSource]);
17987
+ }, [dataSource, resolvedConfig.commentsPerPage]);
17988
+ function updateReplyCacheComment(cache, commentId, updater) {
17989
+ return Object.fromEntries(
17990
+ Object.entries(cache).map(([parentId, replies]) => [
17991
+ parentId,
17992
+ replies.map(
17993
+ (reply) => reply.id === commentId ? updater(reply) : reply
17994
+ )
17995
+ ])
17996
+ );
17997
+ }
17997
17998
  async function handleCreateComment(content) {
17998
17999
  const comment = await dataSource.createComment({
17999
18000
  content,
@@ -18006,7 +18007,35 @@ function CommentThread({
18006
18007
  content,
18007
18008
  parentCommentId: parentComment.id
18008
18009
  });
18009
- setComments((prev) => insertReply(prev, parentComment.id, reply));
18010
+ setReplyCache((prev) => ({
18011
+ ...prev,
18012
+ [parentComment.id]: [...prev[parentComment.id] ?? [], reply]
18013
+ }));
18014
+ const incrementReplyCount = (items) => items.map(
18015
+ (item) => item.id === parentComment.id ? {
18016
+ ...item,
18017
+ replyCount: item.replyCount + 1
18018
+ } : item
18019
+ );
18020
+ setComments((prev) => incrementReplyCount(prev));
18021
+ setReplyCache(
18022
+ (prev) => updateReplyCacheComment(prev, parentComment.id, (item) => ({
18023
+ ...item,
18024
+ replyCount: item.replyCount + 1
18025
+ }))
18026
+ );
18027
+ }
18028
+ async function handleLoadReplies(comment) {
18029
+ if (replyCache[comment.id]) {
18030
+ return;
18031
+ }
18032
+ const result = await dataSource.getReplies(comment.id, {
18033
+ limit: resolvedConfig.repliesPerPage
18034
+ });
18035
+ setReplyCache((prev) => ({
18036
+ ...prev,
18037
+ [comment.id]: result.items
18038
+ }));
18010
18039
  }
18011
18040
  async function handleLike(comment) {
18012
18041
  const optimistic = {
@@ -18015,19 +18044,22 @@ function CommentThread({
18015
18044
  likes: comment.liked ? Math.max(comment.likes - 1, 0) : comment.likes + 1,
18016
18045
  dislikes: comment.disliked ? Math.max(comment.dislikes - 1, 0) : comment.dislikes
18017
18046
  };
18018
- setComments(
18019
- (prev) => updateComment(prev, comment.id, (item) => ({
18020
- ...item,
18021
- likes: optimistic.likes,
18022
- dislikes: optimistic.dislikes,
18023
- liked: optimistic.liked,
18024
- disliked: optimistic.disliked
18025
- }))
18026
- );
18047
+ const updater = (item) => ({
18048
+ ...item,
18049
+ likes: optimistic.likes,
18050
+ dislikes: optimistic.dislikes,
18051
+ liked: optimistic.liked,
18052
+ disliked: optimistic.disliked
18053
+ });
18054
+ setComments((prev) => updateComment(prev, comment.id, updater));
18055
+ setReplyCache((prev) => updateReplyCacheComment(prev, comment.id, updater));
18027
18056
  try {
18028
18057
  await dataSource.toggleLike(comment.id);
18029
18058
  } catch {
18030
18059
  setComments((prev) => updateComment(prev, comment.id, () => comment));
18060
+ setReplyCache(
18061
+ (prev) => updateReplyCacheComment(prev, comment.id, () => comment)
18062
+ );
18031
18063
  }
18032
18064
  }
18033
18065
  async function handleDislike(comment) {
@@ -18037,19 +18069,22 @@ function CommentThread({
18037
18069
  likes: comment.liked ? Math.max(comment.likes - 1, 0) : comment.likes,
18038
18070
  dislikes: comment.disliked ? Math.max(comment.dislikes - 1, 0) : comment.dislikes + 1
18039
18071
  };
18040
- setComments(
18041
- (prev) => updateComment(prev, comment.id, (item) => ({
18042
- ...item,
18043
- likes: optimistic.likes,
18044
- dislikes: optimistic.dislikes,
18045
- liked: optimistic.liked,
18046
- disliked: optimistic.disliked
18047
- }))
18048
- );
18072
+ const updater = (item) => ({
18073
+ ...item,
18074
+ likes: optimistic.likes,
18075
+ dislikes: optimistic.dislikes,
18076
+ liked: optimistic.liked,
18077
+ disliked: optimistic.disliked
18078
+ });
18079
+ setComments((prev) => updateComment(prev, comment.id, updater));
18080
+ setReplyCache((prev) => updateReplyCacheComment(prev, comment.id, updater));
18049
18081
  try {
18050
18082
  await dataSource.toggleDislike(comment.id);
18051
18083
  } catch {
18052
18084
  setComments((prev) => updateComment(prev, comment.id, () => comment));
18085
+ setReplyCache(
18086
+ (prev) => updateReplyCacheComment(prev, comment.id, () => comment)
18087
+ );
18053
18088
  }
18054
18089
  }
18055
18090
  if (loading || isLoadingComments) {
@@ -18069,11 +18104,13 @@ function CommentThread({
18069
18104
  CommentList_default,
18070
18105
  {
18071
18106
  comments,
18072
- components,
18107
+ replies: replyCache,
18073
18108
  config: resolvedConfig,
18109
+ components,
18074
18110
  onLike: handleLike,
18075
18111
  onDislike: handleDislike,
18076
18112
  onReply: handleReply,
18113
+ onLoadReplies: handleLoadReplies,
18077
18114
  onEdit,
18078
18115
  onDelete,
18079
18116
  onReport