diff --git a/apps/registry/app/[locale]/changelog/page.tsx b/apps/registry/app/[locale]/changelog/page.tsx
index cfa2fa18..9995646d 100644
--- a/apps/registry/app/[locale]/changelog/page.tsx
+++ b/apps/registry/app/[locale]/changelog/page.tsx
@@ -1,7 +1,8 @@
-import { Badge, Breadcrumb, Button, MDXContent, Sidebar } from "@vllnt/ui";
+import { Badge, Breadcrumb, Button, MDXContent } from "@vllnt/ui";
import type { Metadata } from "next";
import { getTranslations, setRequestLocale } from "next-intl/server";
+import { PlatformSidebar } from "@/components/platform-sidebar";
import { Link, type Locale } from "@/i18n/routing";
import { type ChangelogTypeFilter, getChangelogEntries } from "@/lib/changelog";
import { breadcrumbTrailLd, jsonLdScript } from "@/lib/jsonld";
@@ -182,7 +183,7 @@ export default async function ChangelogPage({
}}
type="application/ld+json"
/>
-
+
;
+ searchParams: Promise;
};
const metadata_map = componentMetadata as Record<
@@ -149,7 +157,10 @@ export async function generateMetadata(props: Props): Promise {
}
export default async function ComponentPage(props: Props) {
- const { locale, slug } = await props.params;
+ const [{ locale, slug }, query] = await Promise.all([
+ props.params,
+ props.searchParams,
+ ]);
setRequestLocale(locale);
const t = await getTranslations("pages.component");
const common = await getTranslations("common");
@@ -183,27 +194,48 @@ export default async function ComponentPage(props: Props) {
const playgroundExample = getPlaygroundExample(component);
const registryPackageVersion = getRegistryPackageVersion(registry.version);
const supportsNative = component.platforms.includes("native");
+ const platform = getPlatform(query.platform) ?? "web";
+ const isNative = platform === "native";
+ const supportsActivePlatform = component.platforms.includes(platform);
// Read component source for code display
let componentCode = "";
- try {
- const isChartComponent = ["area-chart", "bar-chart", "line-chart"].includes(
- component.name,
- );
-
- const sourcePath = isChartComponent
- ? path.join(
- process.cwd(),
- "..",
- "..",
- "packages",
- "ui",
- "src",
- "components",
- "chart",
- `${component.name}.tsx`,
- )
- : path.join(
+ if (!isNative) {
+ try {
+ const isChartComponent = [
+ "area-chart",
+ "bar-chart",
+ "line-chart",
+ ].includes(component.name);
+
+ const sourcePath = isChartComponent
+ ? path.join(
+ process.cwd(),
+ "..",
+ "..",
+ "packages",
+ "ui",
+ "src",
+ "components",
+ "chart",
+ `${component.name}.tsx`,
+ )
+ : path.join(
+ process.cwd(),
+ "..",
+ "..",
+ "packages",
+ "ui",
+ "src",
+ "components",
+ component.name,
+ `${component.name}.tsx`,
+ );
+
+ try {
+ componentCode = await readFile(sourcePath, "utf8");
+ } catch {
+ const directPath = path.join(
process.cwd(),
"..",
"..",
@@ -211,32 +243,19 @@ export default async function ComponentPage(props: Props) {
"ui",
"src",
"components",
- component.name,
`${component.name}.tsx`,
);
-
- try {
- componentCode = await readFile(sourcePath, "utf8");
+ componentCode = await readFile(directPath, "utf8");
+ }
} catch {
- const directPath = path.join(
- process.cwd(),
- "..",
- "..",
- "packages",
- "ui",
- "src",
- "components",
- `${component.name}.tsx`,
- );
- componentCode = await readFile(directPath, "utf8");
+ // Source file not found — skip code section
}
- } catch {
- // Source file not found — skip code section
}
const installCommand = `pnpm dlx shadcn@latest add https://ui.vllnt.com/r/${component.name}.json`;
- const componentMdx = await getComponentContent(slug, locale);
+ const localizedComponent = await getComponentContent(slug, locale);
+ const componentMdx = isNative ? undefined : localizedComponent;
const mdxKit = buildComponentMdxKit({
componentCode,
componentName: component.name,
@@ -260,33 +279,44 @@ export default async function ComponentPage(props: Props) {
.slice(0, 6);
const relatedComponents = relatedSlugs.filter((name) =>
registry.items.some(
- (item) => item.name === name && item.type === "registry:component",
+ (item) =>
+ item.name === name &&
+ item.type === "registry:component" &&
+ item.platforms.includes(platform),
),
);
const sections = [
- ...(meta?.defaultStoryId ? [{ id: "preview", title: t("preview") }] : []),
- { id: "installation", title: t("installation") },
- ...(meta?.defaultStoryId
- ? [{ id: "storybook", title: t("storybook") }]
+ ...(!isNative && meta?.defaultStoryId
+ ? [{ id: "preview", title: t("preview") }]
: []),
- ...(componentCode ? [{ id: "code", title: t("code") }] : []),
- ...(supportsNative
- ? [{ id: "native-installation", title: t("nativeInstallation") }]
+ ...(supportsActivePlatform
+ ? [
+ {
+ id: "installation",
+ title: isNative ? t("nativeCapabilityTitle") : t("installation"),
+ },
+ ]
: []),
- ...(component.dependencies && component.dependencies.length > 0
+ ...(!isNative && meta?.defaultStoryId
+ ? [{ id: "storybook", title: t("storybook") }]
+ : []),
+ ...(!isNative && componentCode ? [{ id: "code", title: t("code") }] : []),
+ ...(!isNative && component.dependencies && component.dependencies.length > 0
? [{ id: "dependencies", title: t("dependencies") }]
: []),
- ...(seoCopy?.faqs.length ? [{ id: "faq", title: t("faq") }] : []),
+ ...(!isNative && seoCopy?.faqs.length
+ ? [{ id: "faq", title: t("faq") }]
+ : []),
...(relatedComponents.length > 0
? [{ id: "related", title: t("related") }]
: []),
] as { id: string; title: string }[];
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://ui.vllnt.com";
- const articleTitle = componentMdx?.frontmatter.title ?? displayTitle;
+ const articleTitle = localizedComponent?.frontmatter.title ?? displayTitle;
const articleDescription =
- componentMdx?.frontmatter.description ?? displayDescription;
+ localizedComponent?.frontmatter.description ?? displayDescription;
const componentUrl = canonical(`/components/${component.name}`, locale);
const ogImage = `${SITE_URL}${generateOGImageURL({
category: componentCategory ?? undefined,
@@ -303,7 +333,7 @@ export default async function ComponentPage(props: Props) {
softwareSourceCodeLd({
description: articleDescription,
image: ogImage,
- keywords: componentMdx?.frontmatter.keywords,
+ keywords: localizedComponent?.frontmatter.keywords,
locale,
name: component.name,
platforms: component.platforms,
@@ -314,7 +344,7 @@ export default async function ComponentPage(props: Props) {
description: articleDescription,
image: ogImage,
inLanguage: locale,
- keywords: componentMdx?.frontmatter.keywords,
+ keywords: localizedComponent?.frontmatter.keywords,
title: articleTitle,
url: componentUrl,
}),
@@ -332,7 +362,7 @@ export default async function ComponentPage(props: Props) {
),
])}
/>
-
{articleTitle}
-
+
{articleDescription}
+
-
-
+ {isNative ? (
+ supportsNative ? (
+
+ {t("browseNativeCatalog")}
+
+ ) : null
+ ) : (
+
+ )}
+ {isNative ? null : (
+
+ )}
{t("reportBug")}
@@ -394,7 +457,7 @@ export default async function ComponentPage(props: Props) {
{/* When to use in an AI app */}
- {whenToUse ? (
+ {!isNative && whenToUse ? (
{t("whenToUseTitle")}
@@ -402,7 +465,7 @@ export default async function ComponentPage(props: Props) {
{whenToUse}
{t("browseAiComponents")}
@@ -411,7 +474,7 @@ export default async function ComponentPage(props: Props) {
) : null}
{/* Usage block for non-AI components */}
- {!whenToUse && seoCopy?.whatItIs ? (
+ {!isNative && !whenToUse && seoCopy?.whatItIs ? (
{t("whatItIsTitle")}
@@ -422,9 +485,10 @@ export default async function ComponentPage(props: Props) {
{familyGroup ? (
{familyGroup.label}
@@ -434,7 +498,103 @@ export default async function ComponentPage(props: Props) {
) : null}
- {componentMdx ? (
+ {isNative ? (
+ supportsNative ? (
+
+
+ {t("nativeCapabilityTitle")}
+
+
+ {t("nativeCapabilityDescription")}
+
+
+
+ {t("nativeAvailableAfterCanary")}
+
+
+ {nativeRegistry.installation.command}
+
+
+ {component.native ? (
+
+
+
-
+ {t("nativeCompatibilityLabel")}
+
+ -
+ {component.native.compatibility ===
+ "portable-options"
+ ? t("nativeCompatibilityPortable")
+ : t("nativeCompatibilityAdapted")}
+
+
+
+
-
+ {t("nativeRequirementsLabel")}
+
+ -
+ React {nativeRegistry.minimumReact}+ · React Native{" "}
+ {nativeRegistry.minimumReactNative}+
+
+
+
+
-
+ {t("nativeSourcePathLabel")}
+
+ -
+ {component.native.source}
+
+
+
+ ) : null}
+
+ {t("nativeSourceTitle")}
+
+
+ {t("nativeSourceDescription")}
+
+
+ {t("nativeReadGuide")}
+
+
+ ) : (
+
+
+ {t("webOnlyTitle")}
+
+
+ {t("webOnlyDescription")}
+
+
+
+ {t("browseNativeCatalog")}
+
+
+ {t("viewWebComponent")}
+
+
+
+ )
+ ) : componentMdx ? (
) : (
<>
- {/* Preview + Playground */}
{meta?.defaultStoryId ? (
) : null}
- {/* Installation */}
{t("installation")}
@@ -460,7 +618,6 @@ export default async function ComponentPage(props: Props) {
- {/* Storybook link */}
{meta?.defaultStoryId ? (