From b2e0c0ef68ac7eccc3e73dced05d6fbdaba088d0 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Mon, 20 Apr 2026 13:34:19 -0400 Subject: [PATCH 1/5] feat: add blog tag expandable list --- .../components/pages/post-page.internal.tsx | 55 +++++++++++++++---- .../client/components/shared/tags-list.tsx | 40 +++++++++++++- .../blog/client/localization/blog-common.ts | 2 + 3 files changed, 84 insertions(+), 13 deletions(-) diff --git a/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.tsx b/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.tsx index 4cfdd9a6..708c1caf 100644 --- a/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.tsx +++ b/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.tsx @@ -24,6 +24,10 @@ import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/con import { WhenVisible } from "@workspace/ui/components/when-visible"; import { PostNavigationSkeleton } from "../loading/post-navigation-skeleton"; import { RecentPostsCarouselSkeleton } from "../loading/recent-posts-carousel-skeleton"; +import { ChevronDown, ChevronUp } from "lucide-react"; +import { useState } from "react"; + +const MAX_VISIBLE_POST_TAGS = 15; // Internal component with actual page content export function PostPage({ slug }: { slug: string }) { @@ -151,28 +155,57 @@ export function PostPage({ slug }: { slug: string }) { } function PostHeaderTop({ post }: { post: SerializedPost }) { - const { Link } = usePluginOverrides< + const { Link, localization } = usePluginOverrides< BlogPluginOverrides, Partial >("blog", { Link: DefaultLink, + localization: BLOG_LOCALIZATION, }); const basePath = useBasePath(); + const [showAll, setShowAll] = useState(false); + + const allTags = post.tags ?? []; + const hasMore = allTags.length > MAX_VISIBLE_POST_TAGS; + const visibleTags = + showAll || !hasMore ? allTags : allTags.slice(0, MAX_VISIBLE_POST_TAGS); + return (
{formatDate(post.createdAt, "MMMM d, yyyy")} - {post.tags && post.tags.length > 0 && ( - <> - {post.tags.map((tag) => ( - - - {tag.name} - - - ))} - + {visibleTags.map((tag) => ( + + + {tag.name} + + + ))} + {hasMore && ( + + + )}
); diff --git a/packages/stack/src/plugins/blog/client/components/shared/tags-list.tsx b/packages/stack/src/plugins/blog/client/components/shared/tags-list.tsx index 732149a2..19b0d7f3 100644 --- a/packages/stack/src/plugins/blog/client/components/shared/tags-list.tsx +++ b/packages/stack/src/plugins/blog/client/components/shared/tags-list.tsx @@ -5,30 +5,66 @@ import type { BlogPluginOverrides } from "../../overrides"; import { DefaultLink } from "./defaults"; import { Badge } from "@workspace/ui/components/badge"; import { useSuspenseTags } from "../../hooks/blog-hooks"; +import { BLOG_LOCALIZATION } from "../../localization"; +import { ChevronDown, ChevronUp } from "lucide-react"; +import { useState } from "react"; + +const MAX_VISIBLE_TAGS = 15; export function TagsList() { const { tags } = useSuspenseTags(); - const { Link } = usePluginOverrides< + const { Link, localization } = usePluginOverrides< BlogPluginOverrides, Partial >("blog", { Link: DefaultLink, + localization: BLOG_LOCALIZATION, }); const basePath = useBasePath(); + const [showAll, setShowAll] = useState(false); if (!tags || tags.length === 0) { return null; } + const hasMore = tags.length > MAX_VISIBLE_TAGS; + const visibleTags = + showAll || !hasMore ? tags : tags.slice(0, MAX_VISIBLE_TAGS); + return (
- {tags.map((tag) => ( + {visibleTags.map((tag) => ( {tag.name} ))} + {hasMore && ( + + + + )}
); } diff --git a/packages/stack/src/plugins/blog/client/localization/blog-common.ts b/packages/stack/src/plugins/blog/client/localization/blog-common.ts index bde8138f..f6624d55 100644 --- a/packages/stack/src/plugins/blog/client/localization/blog-common.ts +++ b/packages/stack/src/plugins/blog/client/localization/blog-common.ts @@ -4,4 +4,6 @@ export const BLOG_COMMON = { BLOG_PAGE_NOT_FOUND_TITLE: "Not Found", BLOG_PAGE_NOT_FOUND_DESCRIPTION: "The page you are looking for does not exist.", + BLOG_TAGS_SHOW_ALL: "Show all tags", + BLOG_TAGS_SHOW_LESS: "Show fewer tags", }; From f22a88eeba7b1548a6529f4bc703f988b485fd9b Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Mon, 20 Apr 2026 13:38:44 -0400 Subject: [PATCH 2/5] build: update shadcn schema --- packages/stack/registry/btst-blog.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/stack/registry/btst-blog.json b/packages/stack/registry/btst-blog.json index e830291b..bf40d80d 100644 --- a/packages/stack/registry/btst-blog.json +++ b/packages/stack/registry/btst-blog.json @@ -190,7 +190,7 @@ { "path": "btst/blog/client/components/pages/post-page.internal.tsx", "type": "registry:component", - "content": "\"use client\";\n\nimport { usePluginOverrides, useBasePath } from \"@btst/stack/context\";\nimport { formatDate } from \"date-fns\";\nimport {\n\tuseSuspensePost,\n\tuseNextPreviousPosts,\n\tuseRecentPosts,\n} from \"@btst/stack/plugins/blog/client/hooks\";\nimport { EmptyList } from \"../shared/empty-list\";\nimport { MarkdownContent } from \"../shared/markdown-content\";\nimport { PageHeader } from \"../shared/page-header\";\nimport { PageWrapper } from \"../shared/page-wrapper\";\nimport type { BlogPluginOverrides } from \"../../overrides\";\nimport { DefaultImage, DefaultLink } from \"../shared/defaults\";\nimport { BLOG_LOCALIZATION } from \"../../localization\";\nimport { PostNavigation } from \"../shared/post-navigation\";\nimport { RecentPostsCarousel } from \"../shared/recent-posts-carousel\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { useRouteLifecycle } from \"@/hooks/use-route-lifecycle\";\nimport { OnThisPage, OnThisPageSelect } from \"../shared/on-this-page\";\nimport type { SerializedPost } from \"../../../types\";\nimport { useRegisterPageAIContext } from \"@btst/stack/plugins/ai-chat/client/context\";\nimport { WhenVisible } from \"@/components/ui/when-visible\";\nimport { PostNavigationSkeleton } from \"../loading/post-navigation-skeleton\";\nimport { RecentPostsCarouselSkeleton } from \"../loading/recent-posts-carousel-skeleton\";\n\n// Internal component with actual page content\nexport function PostPage({ slug }: { slug: string }) {\n\tconst overrides = usePluginOverrides<\n\t\tBlogPluginOverrides,\n\t\tPartial\n\t>(\"blog\", {\n\t\tImage: DefaultImage,\n\t\tlocalization: BLOG_LOCALIZATION,\n\t});\n\tconst { Image, localization } = overrides;\n\n\t// Call lifecycle hooks\n\tuseRouteLifecycle({\n\t\trouteName: \"post\",\n\t\tcontext: {\n\t\t\tpath: `/blog/${slug}`,\n\t\t\tparams: { slug },\n\t\t\tisSSR: typeof window === \"undefined\",\n\t\t},\n\t\toverrides,\n\t\tbeforeRenderHook: (overrides, context) => {\n\t\t\tif (overrides.onBeforePostPageRendered) {\n\t\t\t\treturn overrides.onBeforePostPageRendered(slug, context);\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\t});\n\n\tconst { post } = useSuspensePost(slug ?? \"\");\n\n\tconst { previousPost, nextPost } = useNextPreviousPosts(\n\t\tpost?.createdAt ?? new Date(),\n\t\t{\n\t\t\tenabled: !!post,\n\t\t},\n\t);\n\n\tconst { recentPosts } = useRecentPosts({\n\t\tlimit: 5,\n\t\texcludeSlug: slug,\n\t\tenabled: !!post,\n\t});\n\n\t// Register page AI context so the chat can summarize and discuss this post\n\tuseRegisterPageAIContext(\n\t\tpost\n\t\t\t? {\n\t\t\t\t\trouteName: \"blog-post\",\n\t\t\t\t\tpageDescription:\n\t\t\t\t\t\t`Blog post: \"${post.title}\"\\nAuthor: ${post.authorId ?? \"Unknown\"}\\n\\n${post.content ?? \"\"}`.slice(\n\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t16000,\n\t\t\t\t\t\t),\n\t\t\t\t\tsuggestions: [\n\t\t\t\t\t\t\"Summarize this post\",\n\t\t\t\t\t\t\"What are the key takeaways?\",\n\t\t\t\t\t\t\"Explain this in simpler terms\",\n\t\t\t\t\t],\n\t\t\t\t}\n\t\t\t: null,\n\t);\n\n\tif (!slug || !post) {\n\t\treturn (\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t);\n\t}\n\n\treturn (\n\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\n\t);\n}\n\nfunction PostHeaderTop({ post }: { post: SerializedPost }) {\n\tconst { Link } = usePluginOverrides<\n\t\tBlogPluginOverrides,\n\t\tPartial\n\t>(\"blog\", {\n\t\tLink: DefaultLink,\n\t});\n\tconst basePath = useBasePath();\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t{formatDate(post.createdAt, \"MMMM d, yyyy\")}\n\t\t\t\n\t\t\t{post.tags && post.tags.length > 0 && (\n\t\t\t\t<>\n\t\t\t\t\t{post.tags.map((tag) => (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{tag.name}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t))}\n\t\t\t\t\n\t\t\t)}\n\t\t
\n\t);\n}\n", + "content": "\"use client\";\n\nimport { usePluginOverrides, useBasePath } from \"@btst/stack/context\";\nimport { formatDate } from \"date-fns\";\nimport {\n\tuseSuspensePost,\n\tuseNextPreviousPosts,\n\tuseRecentPosts,\n} from \"@btst/stack/plugins/blog/client/hooks\";\nimport { EmptyList } from \"../shared/empty-list\";\nimport { MarkdownContent } from \"../shared/markdown-content\";\nimport { PageHeader } from \"../shared/page-header\";\nimport { PageWrapper } from \"../shared/page-wrapper\";\nimport type { BlogPluginOverrides } from \"../../overrides\";\nimport { DefaultImage, DefaultLink } from \"../shared/defaults\";\nimport { BLOG_LOCALIZATION } from \"../../localization\";\nimport { PostNavigation } from \"../shared/post-navigation\";\nimport { RecentPostsCarousel } from \"../shared/recent-posts-carousel\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { useRouteLifecycle } from \"@/hooks/use-route-lifecycle\";\nimport { OnThisPage, OnThisPageSelect } from \"../shared/on-this-page\";\nimport type { SerializedPost } from \"../../../types\";\nimport { useRegisterPageAIContext } from \"@btst/stack/plugins/ai-chat/client/context\";\nimport { WhenVisible } from \"@/components/ui/when-visible\";\nimport { PostNavigationSkeleton } from \"../loading/post-navigation-skeleton\";\nimport { RecentPostsCarouselSkeleton } from \"../loading/recent-posts-carousel-skeleton\";\nimport { ChevronDown, ChevronUp } from \"lucide-react\";\nimport { useState } from \"react\";\n\nconst MAX_VISIBLE_POST_TAGS = 15;\n\n// Internal component with actual page content\nexport function PostPage({ slug }: { slug: string }) {\n\tconst overrides = usePluginOverrides<\n\t\tBlogPluginOverrides,\n\t\tPartial\n\t>(\"blog\", {\n\t\tImage: DefaultImage,\n\t\tlocalization: BLOG_LOCALIZATION,\n\t});\n\tconst { Image, localization } = overrides;\n\n\t// Call lifecycle hooks\n\tuseRouteLifecycle({\n\t\trouteName: \"post\",\n\t\tcontext: {\n\t\t\tpath: `/blog/${slug}`,\n\t\t\tparams: { slug },\n\t\t\tisSSR: typeof window === \"undefined\",\n\t\t},\n\t\toverrides,\n\t\tbeforeRenderHook: (overrides, context) => {\n\t\t\tif (overrides.onBeforePostPageRendered) {\n\t\t\t\treturn overrides.onBeforePostPageRendered(slug, context);\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\t});\n\n\tconst { post } = useSuspensePost(slug ?? \"\");\n\n\tconst { previousPost, nextPost } = useNextPreviousPosts(\n\t\tpost?.createdAt ?? new Date(),\n\t\t{\n\t\t\tenabled: !!post,\n\t\t},\n\t);\n\n\tconst { recentPosts } = useRecentPosts({\n\t\tlimit: 5,\n\t\texcludeSlug: slug,\n\t\tenabled: !!post,\n\t});\n\n\t// Register page AI context so the chat can summarize and discuss this post\n\tuseRegisterPageAIContext(\n\t\tpost\n\t\t\t? {\n\t\t\t\t\trouteName: \"blog-post\",\n\t\t\t\t\tpageDescription:\n\t\t\t\t\t\t`Blog post: \"${post.title}\"\\nAuthor: ${post.authorId ?? \"Unknown\"}\\n\\n${post.content ?? \"\"}`.slice(\n\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t16000,\n\t\t\t\t\t\t),\n\t\t\t\t\tsuggestions: [\n\t\t\t\t\t\t\"Summarize this post\",\n\t\t\t\t\t\t\"What are the key takeaways?\",\n\t\t\t\t\t\t\"Explain this in simpler terms\",\n\t\t\t\t\t],\n\t\t\t\t}\n\t\t\t: null,\n\t);\n\n\tif (!slug || !post) {\n\t\treturn (\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t);\n\t}\n\n\treturn (\n\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\n\t);\n}\n\nfunction PostHeaderTop({ post }: { post: SerializedPost }) {\n\tconst { Link, localization } = usePluginOverrides<\n\t\tBlogPluginOverrides,\n\t\tPartial\n\t>(\"blog\", {\n\t\tLink: DefaultLink,\n\t\tlocalization: BLOG_LOCALIZATION,\n\t});\n\tconst basePath = useBasePath();\n\tconst [showAll, setShowAll] = useState(false);\n\n\tconst allTags = post.tags ?? [];\n\tconst hasMore = allTags.length > MAX_VISIBLE_POST_TAGS;\n\tconst visibleTags =\n\t\tshowAll || !hasMore ? allTags : allTags.slice(0, MAX_VISIBLE_POST_TAGS);\n\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t{formatDate(post.createdAt, \"MMMM d, yyyy\")}\n\t\t\t\n\t\t\t{visibleTags.map((tag) => (\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{tag.name}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t))}\n\t\t\t{hasMore && (\n\t\t\t\t\n\t\t\t\t\t setShowAll((prev) => !prev)}\n\t\t\t\t\t\taria-expanded={showAll}\n\t\t\t\t\t\taria-label={\n\t\t\t\t\t\t\tshowAll\n\t\t\t\t\t\t\t\t? localization.BLOG_TAGS_SHOW_LESS\n\t\t\t\t\t\t\t\t: localization.BLOG_TAGS_SHOW_ALL\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttitle={\n\t\t\t\t\t\t\tshowAll\n\t\t\t\t\t\t\t\t? localization.BLOG_TAGS_SHOW_LESS\n\t\t\t\t\t\t\t\t: localization.BLOG_TAGS_SHOW_ALL\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\t{showAll ? (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t)}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t)}\n\t\t
\n\t);\n}\n", "target": "src/components/btst/blog/client/components/pages/post-page.internal.tsx" }, { @@ -310,7 +310,7 @@ { "path": "btst/blog/client/components/shared/tags-list.tsx", "type": "registry:component", - "content": "\"use client\";\n\nimport { usePluginOverrides, useBasePath } from \"@btst/stack/context\";\nimport type { BlogPluginOverrides } from \"../../overrides\";\nimport { DefaultLink } from \"./defaults\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { useSuspenseTags } from \"@btst/stack/plugins/blog/client/hooks\";\n\nexport function TagsList() {\n\tconst { tags } = useSuspenseTags();\n\tconst { Link } = usePluginOverrides<\n\t\tBlogPluginOverrides,\n\t\tPartial\n\t>(\"blog\", {\n\t\tLink: DefaultLink,\n\t});\n\tconst basePath = useBasePath();\n\n\tif (!tags || tags.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn (\n\t\t
\n\t\t\t{tags.map((tag) => (\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{tag.name}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t))}\n\t\t
\n\t);\n}\n", + "content": "\"use client\";\n\nimport { usePluginOverrides, useBasePath } from \"@btst/stack/context\";\nimport type { BlogPluginOverrides } from \"../../overrides\";\nimport { DefaultLink } from \"./defaults\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { useSuspenseTags } from \"@btst/stack/plugins/blog/client/hooks\";\nimport { BLOG_LOCALIZATION } from \"../../localization\";\nimport { ChevronDown, ChevronUp } from \"lucide-react\";\nimport { useState } from \"react\";\n\nconst MAX_VISIBLE_TAGS = 15;\n\nexport function TagsList() {\n\tconst { tags } = useSuspenseTags();\n\tconst { Link, localization } = usePluginOverrides<\n\t\tBlogPluginOverrides,\n\t\tPartial\n\t>(\"blog\", {\n\t\tLink: DefaultLink,\n\t\tlocalization: BLOG_LOCALIZATION,\n\t});\n\tconst basePath = useBasePath();\n\tconst [showAll, setShowAll] = useState(false);\n\n\tif (!tags || tags.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst hasMore = tags.length > MAX_VISIBLE_TAGS;\n\tconst visibleTags =\n\t\tshowAll || !hasMore ? tags : tags.slice(0, MAX_VISIBLE_TAGS);\n\n\treturn (\n\t\t
\n\t\t\t{visibleTags.map((tag) => (\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{tag.name}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t))}\n\t\t\t{hasMore && (\n\t\t\t\t\n\t\t\t\t\t setShowAll((prev) => !prev)}\n\t\t\t\t\t\taria-expanded={showAll}\n\t\t\t\t\t\taria-label={\n\t\t\t\t\t\t\tshowAll\n\t\t\t\t\t\t\t\t? localization.BLOG_TAGS_SHOW_LESS\n\t\t\t\t\t\t\t\t: localization.BLOG_TAGS_SHOW_ALL\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttitle={\n\t\t\t\t\t\t\tshowAll\n\t\t\t\t\t\t\t\t? localization.BLOG_TAGS_SHOW_LESS\n\t\t\t\t\t\t\t\t: localization.BLOG_TAGS_SHOW_ALL\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\t{showAll ? (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t)}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t)}\n\t\t
\n\t);\n}\n", "target": "src/components/btst/blog/client/components/shared/tags-list.tsx" }, { @@ -322,7 +322,7 @@ { "path": "btst/blog/client/localization/blog-common.ts", "type": "registry:lib", - "content": "export const BLOG_COMMON = {\n\tBLOG_GENERIC_ERROR_TITLE: \"Something went wrong\",\n\tBLOG_GENERIC_ERROR_MESSAGE: \"An unexpected error occurred.\",\n\tBLOG_PAGE_NOT_FOUND_TITLE: \"Not Found\",\n\tBLOG_PAGE_NOT_FOUND_DESCRIPTION:\n\t\t\"The page you are looking for does not exist.\",\n};\n", + "content": "export const BLOG_COMMON = {\n\tBLOG_GENERIC_ERROR_TITLE: \"Something went wrong\",\n\tBLOG_GENERIC_ERROR_MESSAGE: \"An unexpected error occurred.\",\n\tBLOG_PAGE_NOT_FOUND_TITLE: \"Not Found\",\n\tBLOG_PAGE_NOT_FOUND_DESCRIPTION:\n\t\t\"The page you are looking for does not exist.\",\n\tBLOG_TAGS_SHOW_ALL: \"Show all tags\",\n\tBLOG_TAGS_SHOW_LESS: \"Show fewer tags\",\n};\n", "target": "src/components/btst/blog/client/localization/blog-common.ts" }, { From 565cfa33aeab3c0fdd8af8431253a037978efa82 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Mon, 20 Apr 2026 13:39:06 -0400 Subject: [PATCH 3/5] chore: bump version --- packages/stack/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stack/package.json b/packages/stack/package.json index c0c83ab5..e8f48e6d 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -1,6 +1,6 @@ { "name": "@btst/stack", - "version": "2.11.7", + "version": "2.11.8", "description": "A composable, plugin-based library for building full-stack applications.", "repository": { "type": "git", From e7c3052b3ae7366584e312ce5acffc18052fbe17 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 20 Apr 2026 17:51:58 +0000 Subject: [PATCH 4/5] refactor(blog): extract CollapsibleTagList to eliminate duplicated tag expand/collapse logic Co-authored-by: Ollie --- .../components/pages/post-page.internal.tsx | 58 +------------- .../shared/collapsible-tag-list.tsx | 76 +++++++++++++++++++ .../client/components/shared/tags-list.tsx | 56 +------------- 3 files changed, 82 insertions(+), 108 deletions(-) create mode 100644 packages/stack/src/plugins/blog/client/components/shared/collapsible-tag-list.tsx diff --git a/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.tsx b/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.tsx index 708c1caf..b2f34ab3 100644 --- a/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.tsx +++ b/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.tsx @@ -1,6 +1,6 @@ "use client"; -import { usePluginOverrides, useBasePath } from "@btst/stack/context"; +import { usePluginOverrides } from "@btst/stack/context"; import { formatDate } from "date-fns"; import { useSuspensePost, @@ -12,11 +12,10 @@ import { MarkdownContent } from "../shared/markdown-content"; import { PageHeader } from "../shared/page-header"; import { PageWrapper } from "../shared/page-wrapper"; import type { BlogPluginOverrides } from "../../overrides"; -import { DefaultImage, DefaultLink } from "../shared/defaults"; +import { DefaultImage } from "../shared/defaults"; import { BLOG_LOCALIZATION } from "../../localization"; import { PostNavigation } from "../shared/post-navigation"; import { RecentPostsCarousel } from "../shared/recent-posts-carousel"; -import { Badge } from "@workspace/ui/components/badge"; import { useRouteLifecycle } from "@workspace/ui/hooks/use-route-lifecycle"; import { OnThisPage, OnThisPageSelect } from "../shared/on-this-page"; import type { SerializedPost } from "../../../types"; @@ -24,10 +23,7 @@ import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/con import { WhenVisible } from "@workspace/ui/components/when-visible"; import { PostNavigationSkeleton } from "../loading/post-navigation-skeleton"; import { RecentPostsCarouselSkeleton } from "../loading/recent-posts-carousel-skeleton"; -import { ChevronDown, ChevronUp } from "lucide-react"; -import { useState } from "react"; - -const MAX_VISIBLE_POST_TAGS = 15; +import { CollapsibleTagList } from "../shared/collapsible-tag-list"; // Internal component with actual page content export function PostPage({ slug }: { slug: string }) { @@ -155,58 +151,12 @@ export function PostPage({ slug }: { slug: string }) { } function PostHeaderTop({ post }: { post: SerializedPost }) { - const { Link, localization } = usePluginOverrides< - BlogPluginOverrides, - Partial - >("blog", { - Link: DefaultLink, - localization: BLOG_LOCALIZATION, - }); - const basePath = useBasePath(); - const [showAll, setShowAll] = useState(false); - - const allTags = post.tags ?? []; - const hasMore = allTags.length > MAX_VISIBLE_POST_TAGS; - const visibleTags = - showAll || !hasMore ? allTags : allTags.slice(0, MAX_VISIBLE_POST_TAGS); - return (
{formatDate(post.createdAt, "MMMM d, yyyy")} - {visibleTags.map((tag) => ( - - - {tag.name} - - - ))} - {hasMore && ( - - - - )} +
); } diff --git a/packages/stack/src/plugins/blog/client/components/shared/collapsible-tag-list.tsx b/packages/stack/src/plugins/blog/client/components/shared/collapsible-tag-list.tsx new file mode 100644 index 00000000..40c55f5e --- /dev/null +++ b/packages/stack/src/plugins/blog/client/components/shared/collapsible-tag-list.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { usePluginOverrides, useBasePath } from "@btst/stack/context"; +import type { BlogPluginOverrides } from "../../overrides"; +import { DefaultLink } from "./defaults"; +import { Badge } from "@workspace/ui/components/badge"; +import { BLOG_LOCALIZATION } from "../../localization"; +import { ChevronDown, ChevronUp } from "lucide-react"; +import { useState } from "react"; +import type { SerializedTag } from "../../../types"; + +const MAX_VISIBLE_TAGS = 15; + +interface CollapsibleTagListProps { + tags: SerializedTag[]; + maxVisible?: number; +} + +export function CollapsibleTagList({ + tags, + maxVisible = MAX_VISIBLE_TAGS, +}: CollapsibleTagListProps) { + const { Link, localization } = usePluginOverrides< + BlogPluginOverrides, + Partial + >("blog", { + Link: DefaultLink, + localization: BLOG_LOCALIZATION, + }); + const basePath = useBasePath(); + const [showAll, setShowAll] = useState(false); + + if (!tags || tags.length === 0) { + return null; + } + + const hasMore = tags.length > maxVisible; + const visibleTags = showAll || !hasMore ? tags : tags.slice(0, maxVisible); + + return ( + <> + {visibleTags.map((tag) => ( + + + {tag.name} + + + ))} + {hasMore && ( + + + + )} + + ); +} diff --git a/packages/stack/src/plugins/blog/client/components/shared/tags-list.tsx b/packages/stack/src/plugins/blog/client/components/shared/tags-list.tsx index 19b0d7f3..04383336 100644 --- a/packages/stack/src/plugins/blog/client/components/shared/tags-list.tsx +++ b/packages/stack/src/plugins/blog/client/components/shared/tags-list.tsx @@ -1,70 +1,18 @@ "use client"; -import { usePluginOverrides, useBasePath } from "@btst/stack/context"; -import type { BlogPluginOverrides } from "../../overrides"; -import { DefaultLink } from "./defaults"; -import { Badge } from "@workspace/ui/components/badge"; import { useSuspenseTags } from "../../hooks/blog-hooks"; -import { BLOG_LOCALIZATION } from "../../localization"; -import { ChevronDown, ChevronUp } from "lucide-react"; -import { useState } from "react"; - -const MAX_VISIBLE_TAGS = 15; +import { CollapsibleTagList } from "./collapsible-tag-list"; export function TagsList() { const { tags } = useSuspenseTags(); - const { Link, localization } = usePluginOverrides< - BlogPluginOverrides, - Partial - >("blog", { - Link: DefaultLink, - localization: BLOG_LOCALIZATION, - }); - const basePath = useBasePath(); - const [showAll, setShowAll] = useState(false); if (!tags || tags.length === 0) { return null; } - const hasMore = tags.length > MAX_VISIBLE_TAGS; - const visibleTags = - showAll || !hasMore ? tags : tags.slice(0, MAX_VISIBLE_TAGS); - return (
- {visibleTags.map((tag) => ( - - - {tag.name} - - - ))} - {hasMore && ( - - - - )} +
); } From 6ad3de5e74c9feed9a2af322c241b0f3f694aa4d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 20 Apr 2026 18:02:54 +0000 Subject: [PATCH 5/5] chore: update shadcn registry [skip ci] --- packages/stack/registry/btst-blog.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/stack/registry/btst-blog.json b/packages/stack/registry/btst-blog.json index bf40d80d..9da40478 100644 --- a/packages/stack/registry/btst-blog.json +++ b/packages/stack/registry/btst-blog.json @@ -190,7 +190,7 @@ { "path": "btst/blog/client/components/pages/post-page.internal.tsx", "type": "registry:component", - "content": "\"use client\";\n\nimport { usePluginOverrides, useBasePath } from \"@btst/stack/context\";\nimport { formatDate } from \"date-fns\";\nimport {\n\tuseSuspensePost,\n\tuseNextPreviousPosts,\n\tuseRecentPosts,\n} from \"@btst/stack/plugins/blog/client/hooks\";\nimport { EmptyList } from \"../shared/empty-list\";\nimport { MarkdownContent } from \"../shared/markdown-content\";\nimport { PageHeader } from \"../shared/page-header\";\nimport { PageWrapper } from \"../shared/page-wrapper\";\nimport type { BlogPluginOverrides } from \"../../overrides\";\nimport { DefaultImage, DefaultLink } from \"../shared/defaults\";\nimport { BLOG_LOCALIZATION } from \"../../localization\";\nimport { PostNavigation } from \"../shared/post-navigation\";\nimport { RecentPostsCarousel } from \"../shared/recent-posts-carousel\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { useRouteLifecycle } from \"@/hooks/use-route-lifecycle\";\nimport { OnThisPage, OnThisPageSelect } from \"../shared/on-this-page\";\nimport type { SerializedPost } from \"../../../types\";\nimport { useRegisterPageAIContext } from \"@btst/stack/plugins/ai-chat/client/context\";\nimport { WhenVisible } from \"@/components/ui/when-visible\";\nimport { PostNavigationSkeleton } from \"../loading/post-navigation-skeleton\";\nimport { RecentPostsCarouselSkeleton } from \"../loading/recent-posts-carousel-skeleton\";\nimport { ChevronDown, ChevronUp } from \"lucide-react\";\nimport { useState } from \"react\";\n\nconst MAX_VISIBLE_POST_TAGS = 15;\n\n// Internal component with actual page content\nexport function PostPage({ slug }: { slug: string }) {\n\tconst overrides = usePluginOverrides<\n\t\tBlogPluginOverrides,\n\t\tPartial\n\t>(\"blog\", {\n\t\tImage: DefaultImage,\n\t\tlocalization: BLOG_LOCALIZATION,\n\t});\n\tconst { Image, localization } = overrides;\n\n\t// Call lifecycle hooks\n\tuseRouteLifecycle({\n\t\trouteName: \"post\",\n\t\tcontext: {\n\t\t\tpath: `/blog/${slug}`,\n\t\t\tparams: { slug },\n\t\t\tisSSR: typeof window === \"undefined\",\n\t\t},\n\t\toverrides,\n\t\tbeforeRenderHook: (overrides, context) => {\n\t\t\tif (overrides.onBeforePostPageRendered) {\n\t\t\t\treturn overrides.onBeforePostPageRendered(slug, context);\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\t});\n\n\tconst { post } = useSuspensePost(slug ?? \"\");\n\n\tconst { previousPost, nextPost } = useNextPreviousPosts(\n\t\tpost?.createdAt ?? new Date(),\n\t\t{\n\t\t\tenabled: !!post,\n\t\t},\n\t);\n\n\tconst { recentPosts } = useRecentPosts({\n\t\tlimit: 5,\n\t\texcludeSlug: slug,\n\t\tenabled: !!post,\n\t});\n\n\t// Register page AI context so the chat can summarize and discuss this post\n\tuseRegisterPageAIContext(\n\t\tpost\n\t\t\t? {\n\t\t\t\t\trouteName: \"blog-post\",\n\t\t\t\t\tpageDescription:\n\t\t\t\t\t\t`Blog post: \"${post.title}\"\\nAuthor: ${post.authorId ?? \"Unknown\"}\\n\\n${post.content ?? \"\"}`.slice(\n\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t16000,\n\t\t\t\t\t\t),\n\t\t\t\t\tsuggestions: [\n\t\t\t\t\t\t\"Summarize this post\",\n\t\t\t\t\t\t\"What are the key takeaways?\",\n\t\t\t\t\t\t\"Explain this in simpler terms\",\n\t\t\t\t\t],\n\t\t\t\t}\n\t\t\t: null,\n\t);\n\n\tif (!slug || !post) {\n\t\treturn (\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t);\n\t}\n\n\treturn (\n\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\n\t);\n}\n\nfunction PostHeaderTop({ post }: { post: SerializedPost }) {\n\tconst { Link, localization } = usePluginOverrides<\n\t\tBlogPluginOverrides,\n\t\tPartial\n\t>(\"blog\", {\n\t\tLink: DefaultLink,\n\t\tlocalization: BLOG_LOCALIZATION,\n\t});\n\tconst basePath = useBasePath();\n\tconst [showAll, setShowAll] = useState(false);\n\n\tconst allTags = post.tags ?? [];\n\tconst hasMore = allTags.length > MAX_VISIBLE_POST_TAGS;\n\tconst visibleTags =\n\t\tshowAll || !hasMore ? allTags : allTags.slice(0, MAX_VISIBLE_POST_TAGS);\n\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t{formatDate(post.createdAt, \"MMMM d, yyyy\")}\n\t\t\t\n\t\t\t{visibleTags.map((tag) => (\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{tag.name}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t))}\n\t\t\t{hasMore && (\n\t\t\t\t\n\t\t\t\t\t setShowAll((prev) => !prev)}\n\t\t\t\t\t\taria-expanded={showAll}\n\t\t\t\t\t\taria-label={\n\t\t\t\t\t\t\tshowAll\n\t\t\t\t\t\t\t\t? localization.BLOG_TAGS_SHOW_LESS\n\t\t\t\t\t\t\t\t: localization.BLOG_TAGS_SHOW_ALL\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttitle={\n\t\t\t\t\t\t\tshowAll\n\t\t\t\t\t\t\t\t? localization.BLOG_TAGS_SHOW_LESS\n\t\t\t\t\t\t\t\t: localization.BLOG_TAGS_SHOW_ALL\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\t{showAll ? (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t)}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t)}\n\t\t
\n\t);\n}\n", + "content": "\"use client\";\n\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport { formatDate } from \"date-fns\";\nimport {\n\tuseSuspensePost,\n\tuseNextPreviousPosts,\n\tuseRecentPosts,\n} from \"@btst/stack/plugins/blog/client/hooks\";\nimport { EmptyList } from \"../shared/empty-list\";\nimport { MarkdownContent } from \"../shared/markdown-content\";\nimport { PageHeader } from \"../shared/page-header\";\nimport { PageWrapper } from \"../shared/page-wrapper\";\nimport type { BlogPluginOverrides } from \"../../overrides\";\nimport { DefaultImage } from \"../shared/defaults\";\nimport { BLOG_LOCALIZATION } from \"../../localization\";\nimport { PostNavigation } from \"../shared/post-navigation\";\nimport { RecentPostsCarousel } from \"../shared/recent-posts-carousel\";\nimport { useRouteLifecycle } from \"@/hooks/use-route-lifecycle\";\nimport { OnThisPage, OnThisPageSelect } from \"../shared/on-this-page\";\nimport type { SerializedPost } from \"../../../types\";\nimport { useRegisterPageAIContext } from \"@btst/stack/plugins/ai-chat/client/context\";\nimport { WhenVisible } from \"@/components/ui/when-visible\";\nimport { PostNavigationSkeleton } from \"../loading/post-navigation-skeleton\";\nimport { RecentPostsCarouselSkeleton } from \"../loading/recent-posts-carousel-skeleton\";\nimport { CollapsibleTagList } from \"../shared/collapsible-tag-list\";\n\n// Internal component with actual page content\nexport function PostPage({ slug }: { slug: string }) {\n\tconst overrides = usePluginOverrides<\n\t\tBlogPluginOverrides,\n\t\tPartial\n\t>(\"blog\", {\n\t\tImage: DefaultImage,\n\t\tlocalization: BLOG_LOCALIZATION,\n\t});\n\tconst { Image, localization } = overrides;\n\n\t// Call lifecycle hooks\n\tuseRouteLifecycle({\n\t\trouteName: \"post\",\n\t\tcontext: {\n\t\t\tpath: `/blog/${slug}`,\n\t\t\tparams: { slug },\n\t\t\tisSSR: typeof window === \"undefined\",\n\t\t},\n\t\toverrides,\n\t\tbeforeRenderHook: (overrides, context) => {\n\t\t\tif (overrides.onBeforePostPageRendered) {\n\t\t\t\treturn overrides.onBeforePostPageRendered(slug, context);\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\t});\n\n\tconst { post } = useSuspensePost(slug ?? \"\");\n\n\tconst { previousPost, nextPost } = useNextPreviousPosts(\n\t\tpost?.createdAt ?? new Date(),\n\t\t{\n\t\t\tenabled: !!post,\n\t\t},\n\t);\n\n\tconst { recentPosts } = useRecentPosts({\n\t\tlimit: 5,\n\t\texcludeSlug: slug,\n\t\tenabled: !!post,\n\t});\n\n\t// Register page AI context so the chat can summarize and discuss this post\n\tuseRegisterPageAIContext(\n\t\tpost\n\t\t\t? {\n\t\t\t\t\trouteName: \"blog-post\",\n\t\t\t\t\tpageDescription:\n\t\t\t\t\t\t`Blog post: \"${post.title}\"\\nAuthor: ${post.authorId ?? \"Unknown\"}\\n\\n${post.content ?? \"\"}`.slice(\n\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t16000,\n\t\t\t\t\t\t),\n\t\t\t\t\tsuggestions: [\n\t\t\t\t\t\t\"Summarize this post\",\n\t\t\t\t\t\t\"What are the key takeaways?\",\n\t\t\t\t\t\t\"Explain this in simpler terms\",\n\t\t\t\t\t],\n\t\t\t\t}\n\t\t\t: null,\n\t);\n\n\tif (!slug || !post) {\n\t\treturn (\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t);\n\t}\n\n\treturn (\n\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\n\t);\n}\n\nfunction PostHeaderTop({ post }: { post: SerializedPost }) {\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t{formatDate(post.createdAt, \"MMMM d, yyyy\")}\n\t\t\t\n\t\t\t\n\t\t
\n\t);\n}\n", "target": "src/components/btst/blog/client/components/pages/post-page.internal.tsx" }, { @@ -211,6 +211,12 @@ "content": "\"use client\";\n\nimport { lazy } from \"react\";\nimport { usePluginOverrides } from \"@btst/stack/context\";\nimport type { BlogPluginOverrides } from \"../../overrides\";\nimport { ComposedRoute } from \"@btst/stack/client/components\";\nimport { DefaultError } from \"../shared/default-error\";\nimport { PostsLoading } from \"../loading\";\nimport { NotFoundPage } from \"./404-page\";\n\n// Lazy load the internal component with actual page content\nconst TagPage = lazy(() =>\n\timport(\"./tag-page.internal\").then((m) => ({ default: m.TagPage })),\n);\n\n// Exported wrapped component with error and loading boundaries\nexport function TagPageComponent({ tagSlug }: { tagSlug: string }) {\n\tconst { onRouteError } = usePluginOverrides(\"blog\");\n\treturn (\n\t\t {\n\t\t\t\tif (onRouteError) {\n\t\t\t\t\tonRouteError(\"tag\", error, {\n\t\t\t\t\t\tpath: `/blog/tag/${tagSlug}`,\n\t\t\t\t\t\tisSSR: typeof window === \"undefined\",\n\t\t\t\t\t\ttagSlug,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}}\n\t\t/>\n\t);\n}\n", "target": "src/components/btst/blog/client/components/pages/tag-page.tsx" }, + { + "path": "btst/blog/client/components/shared/collapsible-tag-list.tsx", + "type": "registry:component", + "content": "\"use client\";\n\nimport { usePluginOverrides, useBasePath } from \"@btst/stack/context\";\nimport type { BlogPluginOverrides } from \"../../overrides\";\nimport { DefaultLink } from \"./defaults\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { BLOG_LOCALIZATION } from \"../../localization\";\nimport { ChevronDown, ChevronUp } from \"lucide-react\";\nimport { useState } from \"react\";\nimport type { SerializedTag } from \"../../../types\";\n\nconst MAX_VISIBLE_TAGS = 15;\n\ninterface CollapsibleTagListProps {\n\ttags: SerializedTag[];\n\tmaxVisible?: number;\n}\n\nexport function CollapsibleTagList({\n\ttags,\n\tmaxVisible = MAX_VISIBLE_TAGS,\n}: CollapsibleTagListProps) {\n\tconst { Link, localization } = usePluginOverrides<\n\t\tBlogPluginOverrides,\n\t\tPartial\n\t>(\"blog\", {\n\t\tLink: DefaultLink,\n\t\tlocalization: BLOG_LOCALIZATION,\n\t});\n\tconst basePath = useBasePath();\n\tconst [showAll, setShowAll] = useState(false);\n\n\tif (!tags || tags.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst hasMore = tags.length > maxVisible;\n\tconst visibleTags = showAll || !hasMore ? tags : tags.slice(0, maxVisible);\n\n\treturn (\n\t\t<>\n\t\t\t{visibleTags.map((tag) => (\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{tag.name}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t))}\n\t\t\t{hasMore && (\n\t\t\t\t\n\t\t\t\t\t setShowAll((prev) => !prev)}\n\t\t\t\t\t\taria-expanded={showAll}\n\t\t\t\t\t\taria-label={\n\t\t\t\t\t\t\tshowAll\n\t\t\t\t\t\t\t\t? localization.BLOG_TAGS_SHOW_LESS\n\t\t\t\t\t\t\t\t: localization.BLOG_TAGS_SHOW_ALL\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttitle={\n\t\t\t\t\t\t\tshowAll\n\t\t\t\t\t\t\t\t? localization.BLOG_TAGS_SHOW_LESS\n\t\t\t\t\t\t\t\t: localization.BLOG_TAGS_SHOW_ALL\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\t{showAll ? (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t)}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t)}\n\t\t\n\t);\n}\n", + "target": "src/components/btst/blog/client/components/shared/collapsible-tag-list.tsx" + }, { "path": "btst/blog/client/components/shared/default-error.tsx", "type": "registry:component", @@ -310,7 +316,7 @@ { "path": "btst/blog/client/components/shared/tags-list.tsx", "type": "registry:component", - "content": "\"use client\";\n\nimport { usePluginOverrides, useBasePath } from \"@btst/stack/context\";\nimport type { BlogPluginOverrides } from \"../../overrides\";\nimport { DefaultLink } from \"./defaults\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { useSuspenseTags } from \"@btst/stack/plugins/blog/client/hooks\";\nimport { BLOG_LOCALIZATION } from \"../../localization\";\nimport { ChevronDown, ChevronUp } from \"lucide-react\";\nimport { useState } from \"react\";\n\nconst MAX_VISIBLE_TAGS = 15;\n\nexport function TagsList() {\n\tconst { tags } = useSuspenseTags();\n\tconst { Link, localization } = usePluginOverrides<\n\t\tBlogPluginOverrides,\n\t\tPartial\n\t>(\"blog\", {\n\t\tLink: DefaultLink,\n\t\tlocalization: BLOG_LOCALIZATION,\n\t});\n\tconst basePath = useBasePath();\n\tconst [showAll, setShowAll] = useState(false);\n\n\tif (!tags || tags.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst hasMore = tags.length > MAX_VISIBLE_TAGS;\n\tconst visibleTags =\n\t\tshowAll || !hasMore ? tags : tags.slice(0, MAX_VISIBLE_TAGS);\n\n\treturn (\n\t\t
\n\t\t\t{visibleTags.map((tag) => (\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{tag.name}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t))}\n\t\t\t{hasMore && (\n\t\t\t\t\n\t\t\t\t\t setShowAll((prev) => !prev)}\n\t\t\t\t\t\taria-expanded={showAll}\n\t\t\t\t\t\taria-label={\n\t\t\t\t\t\t\tshowAll\n\t\t\t\t\t\t\t\t? localization.BLOG_TAGS_SHOW_LESS\n\t\t\t\t\t\t\t\t: localization.BLOG_TAGS_SHOW_ALL\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttitle={\n\t\t\t\t\t\t\tshowAll\n\t\t\t\t\t\t\t\t? localization.BLOG_TAGS_SHOW_LESS\n\t\t\t\t\t\t\t\t: localization.BLOG_TAGS_SHOW_ALL\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\t{showAll ? (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t)}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t)}\n\t\t
\n\t);\n}\n", + "content": "\"use client\";\n\nimport { useSuspenseTags } from \"@btst/stack/plugins/blog/client/hooks\";\nimport { CollapsibleTagList } from \"./collapsible-tag-list\";\n\nexport function TagsList() {\n\tconst { tags } = useSuspenseTags();\n\n\tif (!tags || tags.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn (\n\t\t
\n\t\t\t\n\t\t
\n\t);\n}\n", "target": "src/components/btst/blog/client/components/shared/tags-list.tsx" }, {