diff --git a/packages/components/package.json b/packages/components/package.json index 0f564463..bf2dc183 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -66,6 +66,7 @@ "lodash": "^4.17.21", "lucide-react": "^0.453.0", "mermaid": "^11.12.2", + "react-use-rect": "^2.0.6", "shiki": "^3.17.0", "tailwind-merge": "^2.6.0" }, diff --git a/packages/components/src/components/update/update.stories.tsx b/packages/components/src/components/update/update.stories.tsx index 4e41d615..6321720b 100644 --- a/packages/components/src/components/update/update.stories.tsx +++ b/packages/components/src/components/update/update.stories.tsx @@ -5,14 +5,99 @@ const meta: Meta = { title: "Components/Update", component: Update, parameters: { - layout: "centered", + layout: "padded", }, tags: ["autodocs"], + args: { + isVisible: true, + }, }; export default meta; type Story = StoryObj; export const Default: Story = { - args: {}, + args: { + id: "update-1", + label: "Jan 15, 2024", + children:

Added support for dark mode across all components.

, + }, +}; + +export const WithDescription: Story = { + args: { + id: "update-2", + label: "Jan 10, 2024", + description: "Bug fixes and improvements", + children: ( +

+ Resolved a bug where search results were not being properly filtered + when using special characters. +

+ ), + }, +}; + +export const WithTags: Story = { + args: { + id: "update-3", + label: "Jan 5, 2024", + tags: ["performance", "backend"], + children: ( +

Optimized database queries to improve response times by 40%.

+ ), + }, +}; + +export const WithAllFeatures: Story = { + args: { + id: "update-4", + label: "Jan 1, 2024", + description: "Major release", + tags: ["release", "breaking"], + children: ( +
+

Implemented new features:

+
    +
  • Redesigned user interface
  • +
  • New API endpoints
  • +
  • Improved performance
  • +
+
+ ), + }, +}; + +export const Changelog: Story = { + render: () => ( +
+ +

Added new authentication options.

+
+ +

Fixed critical vulnerability in session handling.

+
+ +
+

Breaking changes:

+
    +
  • Updated API response format
  • +
  • Removed deprecated endpoints
  • +
+
+
+
+ ), }; diff --git a/packages/components/src/components/update/update.tsx b/packages/components/src/components/update/update.tsx index ae66f0de..e4cac4a4 100644 --- a/packages/components/src/components/update/update.tsx +++ b/packages/components/src/components/update/update.tsx @@ -1,3 +1,173 @@ -export const Update = () => { - return
Update
; +import { + type ComponentPropsWithoutRef, + forwardRef, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; +import { type Rect, useRect } from "react-use-rect"; +import { LinkIcon } from "@/icons"; +import { Classes } from "@/lib/local/selectors"; +import { cn } from "@/utils/cn"; +import { copyToClipboard } from "@/utils/copy-to-clipboard"; + +type UpdatePropsBase = { + id: string; + label: string; + description?: string; + tags?: string[]; + rss?: { + title?: string; + description?: string; + }; + onRegisterHeading?: (id: string, rect: Rect) => void; + onUnregisterHeading?: (id: string) => void; + hasContext?: boolean; + isVisible: boolean; + onCopyAnchorLink?: (id: string) => void; }; + +type UpdateProps = UpdatePropsBase & + Omit, keyof UpdatePropsBase>; + +const Update = forwardRef( + ( + { + children, + label, + id, + description, + tags, + className, + "aria-label": ariaLabel = "Navigate to changelog", + onRegisterHeading, + onUnregisterHeading, + hasContext, + isVisible, + onCopyAnchorLink, + ...props + }, + ref + ) => { + const [rect, setRect] = useState(null); + const [rectRef] = useRect(setRect); + const tagsArray = useMemo( + () => [...new Set((tags ?? []).map((tag) => tag.trim()).filter(Boolean))], + [tags] + ); + + const copyAnchorLink = useCallback(async () => { + if (typeof window === "undefined") { + return; + } + + const result = await copyToClipboard( + `${window.location.href.split("#")[0]}#${encodeURIComponent(id)}` + ); + + if (result === "success") { + window.location.hash = encodeURIComponent(id); + onCopyAnchorLink?.(id); + } + }, [id, onCopyAnchorLink]); + + useEffect(() => { + if (!(hasContext && isVisible && rect)) { + return; + } + + onRegisterHeading?.(id, rect); + + return () => { + onUnregisterHeading?.(id); + }; + }, [ + rect, + id, + hasContext, + isVisible, + onRegisterHeading, + onUnregisterHeading, + ]); + + if (!isVisible) { + return null; + } + + return ( +
+
+ + {/** biome-ignore lint/a11y/noNoninteractiveElementInteractions: TODO */} + {/** biome-ignore lint/a11y/noStaticElementInteractions: TODO */} + {/** biome-ignore lint/a11y/useKeyWithClickEvents: TODO */} +
+ {label} +
+ {tagsArray.length > 0 && ( +
+ {tagsArray.map((tag) => ( + + {tag} + + ))} +
+ )} + {description && ( +
+ {description} +
+ )} +
+ +
+
+ {children} +
+
+
+ ); + } +); + +Update.displayName = "Update"; + +export { Update }; +export type { UpdateProps }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15655b5f..4103a697 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: mermaid: specifier: ^11.12.2 version: 11.12.2 + react-use-rect: + specifier: ^2.0.6 + version: 2.0.6(react@18.3.1) shiki: specifier: ^3.17.0 version: 3.21.0 @@ -2618,6 +2621,11 @@ packages: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} + react-use-rect@2.0.6: + resolution: {integrity: sha512-as/VEIPY9SrS4E7T/rXQ59aqQ+XKvl/5E6fI5ODmrQv6WJup2CFNA9ja3BMq13ge1dhgBFOHicZC1ZKXLUQtTA==} + peerDependencies: + react: ^16.8.0 || ^17.0.2 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -5566,6 +5574,10 @@ snapshots: react-refresh@0.17.0: {} + react-use-rect@2.0.6(react@18.3.1): + dependencies: + react: 18.3.1 + react@18.3.1: dependencies: loose-envify: 1.4.0