Skip to content

Commit 7914b8c

Browse files
Feat: Update Storybook documentation, add installation guide, update utility and hook stories
1 parent 13a4e62 commit 7914b8c

18 files changed

+1118
-414
lines changed

src/hooks/useIsMobile.tsx

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
import * as React from "react";
22

3-
const MOBILE_BREAKPOINT = 768;
3+
const DEFAULT_MOBILE_BREAKPOINT = 768;
44

5-
export function useIsMobile() {
5+
export function useIsMobile(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) {
66
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
77
undefined,
88
);
99

1010
React.useEffect(() => {
11-
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
11+
const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
1212
const onChange = () => {
13-
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
13+
setIsMobile(window.innerWidth < breakpoint);
1414
};
1515
mql.addEventListener("change", onChange);
16-
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
16+
setIsMobile(window.innerWidth < breakpoint);
1717
return () => mql.removeEventListener("change", onChange);
18-
}, []);
18+
}, [breakpoint]);
1919

2020
return !!isMobile;
2121
}

src/lib/format.ts

+77-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,81 @@
11
/**
2-
* Formats numbers to K/M notation.
2+
* Formats a number with commas and optional decimal places.
33
* @param num - The number to format
4-
* @returns The formatted string with K/M suffix
4+
* @param decimals - Number of decimal places (optional)
5+
* @returns The formatted string
56
*/
6-
export const formatNumber = (num: number): string => {
7-
if (num >= 1000000) return (num / 1000000).toFixed(1) + "M";
8-
if (num >= 1000) return (num / 1000).toFixed(1) + "K";
9-
return num.toString();
7+
const number = (num: number, decimals?: number): string => {
8+
return new Intl.NumberFormat("en-US", {
9+
minimumFractionDigits: decimals,
10+
maximumFractionDigits: decimals,
11+
}).format(num);
12+
};
13+
14+
/**
15+
* Formats a number as currency with optional currency code and locale.
16+
* @param num - The number to format
17+
* @param currency - Currency code (default: "USD")
18+
* @param locale - Locale string (default: "en-US")
19+
* @returns The formatted currency string
20+
*/
21+
const currency = (
22+
num: number,
23+
currency: string = "USD",
24+
locale: string = "en-US",
25+
): string => {
26+
return new Intl.NumberFormat(locale, {
27+
style: "currency",
28+
currency,
29+
}).format(num);
30+
};
31+
32+
/**
33+
* Formats a date with optional pattern.
34+
* @param date - The date to format
35+
* @param pattern - Date pattern (optional)
36+
* @returns The formatted date string
37+
*/
38+
const date = (date: Date, pattern?: string): string => {
39+
if (pattern) {
40+
// Simple pattern implementation
41+
const year = date.getFullYear();
42+
const month = String(date.getMonth() + 1).padStart(2, "0");
43+
const day = String(date.getDate()).padStart(2, "0");
44+
return pattern
45+
.replace("yyyy", String(year))
46+
.replace("MM", month)
47+
.replace("dd", day);
48+
}
49+
return new Intl.DateTimeFormat("en-US", {
50+
year: "numeric",
51+
month: "short",
52+
day: "numeric",
53+
}).format(date);
54+
};
55+
56+
/**
57+
* Formats a time with optional pattern.
58+
* @param date - The date to format
59+
* @param pattern - Time pattern (optional)
60+
* @returns The formatted time string
61+
*/
62+
const time = (date: Date, pattern?: string): string => {
63+
if (pattern) {
64+
// Simple pattern implementation
65+
const hours24 = String(date.getHours()).padStart(2, "0");
66+
const minutes = String(date.getMinutes()).padStart(2, "0");
67+
return pattern.replace("HH", hours24).replace("mm", minutes);
68+
}
69+
return new Intl.DateTimeFormat("en-US", {
70+
hour: "numeric",
71+
minute: "numeric",
72+
hour12: true,
73+
}).format(date);
74+
};
75+
76+
export const format = {
77+
number,
78+
currency,
79+
date,
80+
time,
1081
};

src/lib/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ export { cn } from "./utils";
22
export { capitalize } from "./capitalize";
33
export { truncate } from "./truncate";
44
export { isValidURL } from "./url";
5-
export { formatNumber } from "./format";
5+
export { format } from "./format";

src/lib/truncate.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
/**
2-
* Truncates a string to a specified length and adds ellipsis.
2+
* Truncates a string to a specified length and adds a custom ending.
33
* @param str - The string to truncate
44
* @param length - The maximum length before truncation
5-
* @returns The truncated string with ellipsis if needed
5+
* @param ending - The string to append after truncation (default: "...")
6+
* @returns The truncated string with ending if needed
67
*/
7-
export const truncate = (str: string, length: number): string => {
8-
return str.length > length ? str.slice(0, length) + "..." : str;
8+
export const truncate = (
9+
str: string,
10+
length: number,
11+
ending: string = "...",
12+
): string => {
13+
return str.length > length ? str.slice(0, length) + ending : str;
914
};
+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React, { useState } from "react";
2+
import { Table, Tag, Card, CardTitle, CardContent, List } from ".";
3+
import {
4+
Select,
5+
SelectTrigger,
6+
SelectValue,
7+
SelectContent,
8+
SelectItem,
9+
} from "../../../components/shadcn/select";
10+
import {
11+
changelogGroups,
12+
versionTypes,
13+
ChangelogEntry,
14+
} from "../../data/changelog";
15+
16+
interface TableColumn<T> {
17+
key: keyof T | string;
18+
header: React.ReactNode;
19+
render?: (value: unknown, row: T, index: number) => React.ReactNode;
20+
}
21+
22+
type ChangelogEntryAsRecord = ChangelogEntry & Record<string, unknown>;
23+
24+
export const ChangelogView: React.FC = () => {
25+
const [selectedVersion, setSelectedVersion] = useState(
26+
changelogGroups.find((g) => !g.hidden)?.value,
27+
);
28+
const selectedGroup = changelogGroups.find(
29+
(g) => g.value === selectedVersion,
30+
);
31+
32+
const columns: TableColumn<ChangelogEntryAsRecord>[] = [
33+
{ key: "version", header: "Version" },
34+
{ key: "date", header: "Date" },
35+
{
36+
key: "changes",
37+
header: "Changes",
38+
render: (_: unknown, row: ChangelogEntryAsRecord) => (
39+
<List items={row.changes} />
40+
),
41+
},
42+
{
43+
key: "type",
44+
header: "Type",
45+
render: (_: unknown, row: ChangelogEntryAsRecord) => (
46+
<Tag variant={row.type.toLowerCase() as "major" | "minor" | "patch"}>
47+
{row.type}
48+
</Tag>
49+
),
50+
},
51+
];
52+
53+
return (
54+
<div className="sb-container">
55+
<div className="sb-section-title">
56+
<h1>Changelog</h1>
57+
</div>
58+
59+
<div className="sb-section">
60+
<div className="sb-section-subtitle">
61+
<h2>Version Types</h2>
62+
</div>
63+
<div className="sb-version-types">
64+
{versionTypes.map((versionType) => (
65+
<Card key={versionType.type}>
66+
<Tag
67+
variant={
68+
versionType.type.toLowerCase() as "major" | "minor" | "patch"
69+
}
70+
>
71+
{versionType.type}
72+
</Tag>
73+
<CardContent>
74+
<p className="mt-2">{versionType.description}</p>
75+
</CardContent>
76+
</Card>
77+
))}
78+
</div>
79+
80+
<div className="sb-section-subtitle">
81+
<h2>Release History</h2>
82+
</div>
83+
84+
<div className="sb-version-select">
85+
<Select value={selectedVersion} onValueChange={setSelectedVersion}>
86+
<SelectTrigger className="w-[180px] bg-[#F8F8F8] dark:bg-[#011627]">
87+
<SelectValue placeholder="Select version" />
88+
</SelectTrigger>
89+
<SelectContent>
90+
{changelogGroups
91+
.filter((group) => !group.hidden)
92+
.map((group) => (
93+
<SelectItem key={group.value} value={group.value}>
94+
{group.label}
95+
</SelectItem>
96+
))}
97+
</SelectContent>
98+
</Select>
99+
</div>
100+
101+
<div className="sb-changelog">
102+
{selectedGroup && (
103+
<Table
104+
columns={columns}
105+
data={
106+
selectedGroup.entries as unknown as ChangelogEntryAsRecord[]
107+
}
108+
/>
109+
)}
110+
</div>
111+
</div>
112+
</div>
113+
);
114+
};

src/stories/components/ui/List.tsx

+43-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,46 @@ interface ListProps {
66
bulletType?: "dot" | "arrow";
77
}
88

9+
/**
10+
* Process markdown links in text
11+
* Converts [text](url) to <a> elements
12+
*/
13+
const processMarkdownLinks = (text: string) => {
14+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
15+
const parts = [];
16+
let lastIndex = 0;
17+
let match;
18+
19+
while ((match = linkRegex.exec(text)) !== null) {
20+
// Add text before the link
21+
if (match.index > lastIndex) {
22+
parts.push(text.slice(lastIndex, match.index));
23+
}
24+
25+
// Add the link
26+
parts.push(
27+
<a
28+
key={match.index}
29+
href={match[2]}
30+
target="_blank"
31+
rel="noopener noreferrer"
32+
className="text-blue-500 dark:text-blue-400 hover:underline"
33+
>
34+
{match[1]}
35+
</a>,
36+
);
37+
38+
lastIndex = match.index + match[0].length;
39+
}
40+
41+
// Add remaining text
42+
if (lastIndex < text.length) {
43+
parts.push(text.slice(lastIndex));
44+
}
45+
46+
return parts;
47+
};
48+
949
/**
1050
* A reusable list component with dark mode support
1151
*
@@ -45,7 +85,9 @@ export const List: React.FC<ListProps> = ({
4585
>
4686
{bulletChar}
4787
</span>
48-
<span style={{ fontSize: "0.85em" }}>{item}</span>
88+
<span style={{ fontSize: "0.85em" }}>
89+
{typeof item === "string" ? processMarkdownLinks(item) : item}
90+
</span>
4991
</div>
5092
))}
5193
</div>

src/stories/components/ui/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from "./List";
33
export * from "./Table";
44
export * from "./Tag";
55
export * from "./utils";
6+
export * from "./ChangelogView";

0 commit comments

Comments
 (0)