retail-design-system 1.0.0
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/.github/workflows/release.yml +46 -0
- package/.oxfmtrc.json +17 -0
- package/.oxlintrc.json +132 -0
- package/.vscode/extensions.json +3 -0
- package/.vscode/settings.json +13 -0
- package/README.md +56 -0
- package/apps/storybook/.storybook/main.ts +8 -0
- package/apps/storybook/.storybook/preview.css +9 -0
- package/apps/storybook/.storybook/preview.ts +6 -0
- package/apps/storybook/package.json +24 -0
- package/apps/storybook/stories/button.stories.ts +118 -0
- package/apps/storybook/stories/input.stories.ts +127 -0
- package/apps/storybook/stories/label.stories.ts +98 -0
- package/apps/storybook/tsconfig.app.json +24 -0
- package/apps/storybook/tsconfig.json +4 -0
- package/apps/storybook/tsconfig.node.json +22 -0
- package/apps/storybook/vite.config.ts +15 -0
- package/apps/web/app/(sidebar)/components/[...slugs]/get-child-block.ts +17 -0
- package/apps/web/app/(sidebar)/components/[...slugs]/get-component-page-match.ts +56 -0
- package/apps/web/app/(sidebar)/components/[...slugs]/get-direct-child-block.ts +22 -0
- package/apps/web/app/(sidebar)/components/[...slugs]/layout.tsx +25 -0
- package/apps/web/app/(sidebar)/components/[...slugs]/page.tsx +32 -0
- package/apps/web/app/(sidebar)/components/[...slugs]/pascal-to-kebab-case.ts +9 -0
- package/apps/web/app/(sidebar)/components/button2/page.tsx +154 -0
- package/apps/web/app/(sidebar)/components/input/page.tsx +98 -0
- package/apps/web/app/(sidebar)/experiments/2025-10-22/mayhem-mode-card-badge.tsx +9 -0
- package/apps/web/app/(sidebar)/experiments/2025-10-22/mayhem-mode-coin-active-badge.tsx +14 -0
- package/apps/web/app/(sidebar)/experiments/2025-10-22/mayhem-mode-coin-inactive-badge.tsx +12 -0
- package/apps/web/app/(sidebar)/experiments/2025-10-22/mayhem-mode-create-coin.tsx +44 -0
- package/apps/web/app/(sidebar)/experiments/2025-10-22/mayhem-mode-dialog-icon.tsx +47 -0
- package/apps/web/app/(sidebar)/experiments/2025-10-22/page.tsx +167 -0
- package/apps/web/app/(sidebar)/experiments/2025-11-04/filters.tsx +90 -0
- package/apps/web/app/(sidebar)/experiments/2025-11-04/page.tsx +18 -0
- package/apps/web/app/(sidebar)/layout.tsx +17 -0
- package/apps/web/app/(sidebar)/primitives/colors/page.tsx +49 -0
- package/apps/web/app/favicon.ico +0 -0
- package/apps/web/app/layout.tsx +39 -0
- package/apps/web/app/page.tsx +14 -0
- package/apps/web/app/providers.tsx +15 -0
- package/apps/web/components/dialog.tsx +21 -0
- package/apps/web/components/logo.tsx +11 -0
- package/apps/web/components/logomark.tsx +21 -0
- package/apps/web/components/logotype.tsx +25 -0
- package/apps/web/components/notion/notion-block-content.tsx +401 -0
- package/apps/web/components/notion/notion-docs-blocks.tsx +18 -0
- package/apps/web/components/notion/notion-docs-code-page.tsx +20 -0
- package/apps/web/components/notion/notion-docs-layout.tsx +52 -0
- package/apps/web/components/notion/notion-revalidate-button-client.tsx +14 -0
- package/apps/web/components/notion/notion-revalidate-button.tsx +20 -0
- package/apps/web/components/notion/notion-rich-text-segments.tsx +55 -0
- package/apps/web/components/notion/notion-tabs.tsx +38 -0
- package/apps/web/components/notion/notion.ts +223 -0
- package/apps/web/components/sidebar-client.tsx +60 -0
- package/apps/web/components/sidebar-server.tsx +185 -0
- package/apps/web/components/tooltip.tsx +53 -0
- package/apps/web/components/topbar.tsx +14 -0
- package/apps/web/next.config.ts +10 -0
- package/apps/web/package.json +42 -0
- package/apps/web/postcss.config.mjs +5 -0
- package/apps/web/public/2025-10-22-dialog-banner.png +0 -0
- package/apps/web/public/pump-logomark.svg +7 -0
- package/apps/web/styles/custom.css +31 -0
- package/apps/web/styles/font.css +8 -0
- package/apps/web/styles/global.css +5 -0
- package/apps/web/styles/tailwind-reset.css +102 -0
- package/apps/web/styles/tailwind.css +140 -0
- package/apps/web/tsconfig.json +34 -0
- package/bun.lock +1249 -0
- package/bunfig.toml +2 -0
- package/package.json +41 -0
- package/packages/ui/global.d.ts +4 -0
- package/packages/ui/package.json +49 -0
- package/packages/ui/src/components/button/button-spinner.module.css +95 -0
- package/packages/ui/src/components/button/button-spinner.tsx +18 -0
- package/packages/ui/src/components/button/button.module.css +144 -0
- package/packages/ui/src/components/button/button.tsx +102 -0
- package/packages/ui/src/components/button-link/button-link.tsx +46 -0
- package/packages/ui/src/components/column/column.module.css +4 -0
- package/packages/ui/src/components/column/column.tsx +65 -0
- package/packages/ui/src/components/row/row.module.css +4 -0
- package/packages/ui/src/components/row/row.tsx +65 -0
- package/packages/ui/src/components/spacer/spacer.module.css +3 -0
- package/packages/ui/src/components/spacer/spacer.tsx +30 -0
- package/packages/ui/src/components/switch/switch.module.css +62 -0
- package/packages/ui/src/components/switch/switch.tsx +58 -0
- package/packages/ui/src/components/tabs/tabs-panel.module.css +4 -0
- package/packages/ui/src/components/tabs/tabs-panel.tsx +21 -0
- package/packages/ui/src/components/tabs/tabs.module.css +5 -0
- package/packages/ui/src/components/tabs/tabs.tsx +21 -0
- package/packages/ui/src/components/tabs-underline/tabs-underline-indicator.module.css +10 -0
- package/packages/ui/src/components/tabs-underline/tabs-underline-indicator.tsx +33 -0
- package/packages/ui/src/components/tabs-underline/tabs-underline-list.module.css +8 -0
- package/packages/ui/src/components/tabs-underline/tabs-underline-list.tsx +27 -0
- package/packages/ui/src/components/tabs-underline/tabs-underline-tab.module.css +24 -0
- package/packages/ui/src/components/tabs-underline/tabs-underline-tab.tsx +30 -0
- package/packages/ui/src/foundations/colors/colors.ts +475 -0
- package/packages/ui/src/foundations/colors/generate-css.ts +34 -0
- package/packages/ui/src/foundations/colors/retail-design-system.css +116 -0
- package/packages/ui/src/foundations/colors/tailwind-v3.ts +18 -0
- package/packages/ui/src/foundations/colors/tailwind-v4.css +116 -0
- package/packages/ui/src/index.ts +34 -0
- package/packages/ui/src/input.module.css +57 -0
- package/packages/ui/src/input.tsx +49 -0
- package/packages/ui/src/label.module.css +8 -0
- package/packages/ui/src/label.tsx +23 -0
- package/packages/ui/tsconfig.json +14 -0
- package/packages/ui/tsup.config.ts +31 -0
- package/scripts/clean.sh +69 -0
- package/scripts/sort-package-json.sh +30 -0
- package/turbo.json +15 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { evaluate } from "@mdx-js/mdx"
|
|
2
|
+
import { IconPlusLarge } from "@pump-fun/icons-filled"
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
ButtonLink,
|
|
6
|
+
Column,
|
|
7
|
+
Input,
|
|
8
|
+
Label,
|
|
9
|
+
Row,
|
|
10
|
+
Spacer,
|
|
11
|
+
Tabs,
|
|
12
|
+
TabsUnderlineList,
|
|
13
|
+
TabsUnderlineTab,
|
|
14
|
+
TabsPanel,
|
|
15
|
+
Switch,
|
|
16
|
+
} from "@pump-fun/retail-design-system"
|
|
17
|
+
import Link from "next/link"
|
|
18
|
+
import type { ElementType, JSX } from "react"
|
|
19
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime"
|
|
20
|
+
import { highlight } from "sugar-high"
|
|
21
|
+
import type { NotionBlock, NotionRichTextSegment } from "@/components/notion/notion"
|
|
22
|
+
import { NotionRichTextSegments } from "@/components/notion/notion-rich-text-segments"
|
|
23
|
+
|
|
24
|
+
interface NotionBlockContentProps {
|
|
25
|
+
block: NotionBlock
|
|
26
|
+
blockIndex: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface RenderHeadingProps {
|
|
30
|
+
blockIndex: number
|
|
31
|
+
className: string
|
|
32
|
+
level: "h1" | "h2" | "h3"
|
|
33
|
+
segments: NotionRichTextSegment[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function NotionBlockContent(props: NotionBlockContentProps): Promise<JSX.Element> {
|
|
37
|
+
const { block, blockIndex } = props
|
|
38
|
+
|
|
39
|
+
if (block.type === "h2") {
|
|
40
|
+
return renderHeading({
|
|
41
|
+
blockIndex,
|
|
42
|
+
className: "text-text-primary text-24 font-500 leading-[1.3]",
|
|
43
|
+
level: "h2",
|
|
44
|
+
segments: block.segments,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (block.type === "h3") {
|
|
49
|
+
return renderHeading({
|
|
50
|
+
blockIndex,
|
|
51
|
+
className: "text-text-primary text-20 font-500 leading-[1.4]",
|
|
52
|
+
level: "h3",
|
|
53
|
+
segments: block.segments,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (block.type === "paragraph") {
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
<p className="text-text-primary text-16 leading-1-625">
|
|
61
|
+
<NotionRichTextSegments blockIndex={blockIndex} segments={block.segments} />
|
|
62
|
+
</p>
|
|
63
|
+
<Spacer className="h-24" />
|
|
64
|
+
</>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (block.type === "image") {
|
|
69
|
+
return (
|
|
70
|
+
<>
|
|
71
|
+
{/* Notion serves image URLs from varying hosts, so render a plain image tag. */}
|
|
72
|
+
{/* oxlint-disable-next-line @next/next/no-img-element */}
|
|
73
|
+
<img alt={block.alt} className="rounded-12 h-auto w-full" loading="lazy" src={block.url} />
|
|
74
|
+
<Spacer className="h-24" />
|
|
75
|
+
</>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (block.type === "code") {
|
|
80
|
+
const linkCards = renderLinkCards(block.code)
|
|
81
|
+
|
|
82
|
+
if (linkCards) {
|
|
83
|
+
return (
|
|
84
|
+
<>
|
|
85
|
+
<div className="flex flex-wrap gap-8">{linkCards}</div>
|
|
86
|
+
<Spacer className="h-8" />
|
|
87
|
+
</>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const mappedComponentElements = await renderMappedComponents(block.code)
|
|
92
|
+
|
|
93
|
+
if (mappedComponentElements) {
|
|
94
|
+
return (
|
|
95
|
+
<>
|
|
96
|
+
{/* Showcase component */}
|
|
97
|
+
<div className="rounded-12 bg-bg-secondary border-border-secondary flex flex-wrap items-center justify-center gap-16 border px-16 py-48">
|
|
98
|
+
{mappedComponentElements}
|
|
99
|
+
</div>
|
|
100
|
+
<Spacer className="h-24" />
|
|
101
|
+
</>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const codeHTML = highlight(block.code)
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<>
|
|
109
|
+
<pre className="rounded-12 bg-bg-secondary border-border-secondary text-13 overflow-x-auto border p-16 whitespace-break-spaces">
|
|
110
|
+
{/* oxlint-disable-next-line react/no-danger */}
|
|
111
|
+
<code aria-label={block.language} dangerouslySetInnerHTML={{ __html: codeHTML }} />
|
|
112
|
+
</pre>
|
|
113
|
+
<Spacer className="h-24" />
|
|
114
|
+
</>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// oxlint-disable-next-line react/jsx-no-useless-fragment returns empty fragment for typescript
|
|
119
|
+
return <></>
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getHeadingId(segments: NotionRichTextSegment[], blockIndex: number): string {
|
|
123
|
+
const headingText = getHeadingText(segments)
|
|
124
|
+
|
|
125
|
+
const headingSlug = headingText
|
|
126
|
+
.toLowerCase()
|
|
127
|
+
.normalize("NFKD")
|
|
128
|
+
.replaceAll(/[\u0300-\u036F]/g, "")
|
|
129
|
+
.replaceAll(/[^a-z0-9]+/g, "-")
|
|
130
|
+
.replaceAll(/^-+|-+$/g, "")
|
|
131
|
+
|
|
132
|
+
return headingSlug || `heading-${blockIndex}`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getHeadingText(segments: NotionRichTextSegment[]): string {
|
|
136
|
+
return segments
|
|
137
|
+
.map((segment) => segment.text)
|
|
138
|
+
.join("")
|
|
139
|
+
.trim()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderHeading(props: RenderHeadingProps): JSX.Element {
|
|
143
|
+
const { blockIndex, className, level, segments } = props
|
|
144
|
+
|
|
145
|
+
const Heading = level
|
|
146
|
+
const headingText = getHeadingText(segments)
|
|
147
|
+
const headingId = getHeadingId(segments, blockIndex)
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<>
|
|
151
|
+
<Spacer className="h-24" />
|
|
152
|
+
<Heading className={`${className} group scroll-mt-[80px]`} id={headingId}>
|
|
153
|
+
<a
|
|
154
|
+
aria-label={headingText ? `Link to ${headingText}` : "Link to heading"}
|
|
155
|
+
className="relative inline-block max-w-full no-underline"
|
|
156
|
+
href={`#${headingId}`}
|
|
157
|
+
>
|
|
158
|
+
<span
|
|
159
|
+
aria-hidden
|
|
160
|
+
className="text-text-muted pointer-events-none absolute top-0 right-full pr-8 opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100"
|
|
161
|
+
>
|
|
162
|
+
#
|
|
163
|
+
</span>
|
|
164
|
+
{renderHeadingSegments(blockIndex, segments)}
|
|
165
|
+
</a>
|
|
166
|
+
</Heading>
|
|
167
|
+
<Spacer className="h-12" />
|
|
168
|
+
</>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function renderHeadingSegments(
|
|
173
|
+
blockIndex: number,
|
|
174
|
+
segments: NotionRichTextSegment[],
|
|
175
|
+
): JSX.Element[] {
|
|
176
|
+
return segments.map((segment, segmentIndex) => {
|
|
177
|
+
// The surrounding heading link owns hash navigation, so avoid nested anchors here.
|
|
178
|
+
let segmentContent: JSX.Element | string = segment.text
|
|
179
|
+
|
|
180
|
+
if (segment.code) {
|
|
181
|
+
segmentContent = (
|
|
182
|
+
<code className="rounded-4 bg-bg-secondary text-14 px-4 py-2 font-mono">
|
|
183
|
+
{segmentContent}
|
|
184
|
+
</code>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (segment.bold) {
|
|
189
|
+
segmentContent = <strong>{segmentContent}</strong>
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (segment.italic) {
|
|
193
|
+
segmentContent = <em>{segmentContent}</em>
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (segment.strikethrough) {
|
|
197
|
+
segmentContent = <s>{segmentContent}</s>
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (segment.underline) {
|
|
201
|
+
segmentContent = <u>{segmentContent}</u>
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return <span key={`${blockIndex}-${segmentIndex}`}>{segmentContent}</span>
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const LINKS_MARKER = "// links"
|
|
209
|
+
const COMPONENT_MARKER = "// component"
|
|
210
|
+
|
|
211
|
+
const components: Record<string, ElementType> = {
|
|
212
|
+
Button,
|
|
213
|
+
ButtonLink,
|
|
214
|
+
Column,
|
|
215
|
+
IconPlus: IconPlusLarge,
|
|
216
|
+
Input,
|
|
217
|
+
Label,
|
|
218
|
+
Link,
|
|
219
|
+
Row,
|
|
220
|
+
Spacer,
|
|
221
|
+
Switch,
|
|
222
|
+
Tabs,
|
|
223
|
+
TabsPanel,
|
|
224
|
+
TabsUnderlineList,
|
|
225
|
+
TabsUnderlineTab,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
interface LinkCard {
|
|
229
|
+
badge: string
|
|
230
|
+
href: string
|
|
231
|
+
label: string
|
|
232
|
+
meta: string
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function formatLinkMeta(href: string): string {
|
|
236
|
+
if (href.startsWith("/")) {
|
|
237
|
+
return href
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const url = new URL(href)
|
|
241
|
+
const hostname = url.hostname.replace(/^www\./, "")
|
|
242
|
+
const pathname = url.pathname === "/" ? "" : url.pathname.replace(/\/$/, "")
|
|
243
|
+
return `${hostname}${pathname}`
|
|
244
|
+
} catch {
|
|
245
|
+
return href
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function inferLinkBadge(label: string, href: string): string {
|
|
250
|
+
const normalizedText = `${label} ${href}`.toLowerCase()
|
|
251
|
+
if (normalizedText.includes("github")) {
|
|
252
|
+
return "GH"
|
|
253
|
+
}
|
|
254
|
+
if (normalizedText.includes("figma")) {
|
|
255
|
+
return "FG"
|
|
256
|
+
}
|
|
257
|
+
if (normalizedText.includes("story")) {
|
|
258
|
+
return "SB"
|
|
259
|
+
}
|
|
260
|
+
if (normalizedText.includes("download")) {
|
|
261
|
+
return "DL"
|
|
262
|
+
}
|
|
263
|
+
const compactLabel = label.replaceAll(/[^a-z0-9]/gi, "").toUpperCase()
|
|
264
|
+
return compactLabel.length >= 2 ? compactLabel.slice(0, 2) : "LK"
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function inferLinkLabel(href: string): string {
|
|
268
|
+
const normalizedHref = href.toLowerCase()
|
|
269
|
+
if (normalizedHref.includes("github")) {
|
|
270
|
+
return "GitHub"
|
|
271
|
+
}
|
|
272
|
+
if (normalizedHref.includes("figma")) {
|
|
273
|
+
return "Figma"
|
|
274
|
+
}
|
|
275
|
+
if (normalizedHref.includes("story")) {
|
|
276
|
+
return "Storybook"
|
|
277
|
+
}
|
|
278
|
+
return "Link"
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function isExternalHref(href: string): boolean {
|
|
282
|
+
return /^(?:https?:\/\/|mailto:)/.test(href)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isLinkHref(segment: string): boolean {
|
|
286
|
+
return /^(?:https?:\/\/|mailto:|\/)/.test(segment)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function parseLinkCard(line: string): LinkCard | undefined {
|
|
290
|
+
const trimmedLine = line.trim()
|
|
291
|
+
if (trimmedLine.length === 0 || trimmedLine.startsWith("//")) {
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
const segments = trimmedLine
|
|
295
|
+
.split("::")
|
|
296
|
+
.map((segment) => segment.trim())
|
|
297
|
+
.filter(Boolean)
|
|
298
|
+
if (segments.length === 0) {
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
const hrefIndex = segments.findIndex((segment) => isLinkHref(segment))
|
|
302
|
+
if (hrefIndex === -1) {
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
const href = segments[hrefIndex]
|
|
306
|
+
const textSegments = segments.filter((_segment, index) => index !== hrefIndex)
|
|
307
|
+
const label = textSegments[0] ?? inferLinkLabel(href)
|
|
308
|
+
return {
|
|
309
|
+
badge: inferLinkBadge(label, href),
|
|
310
|
+
href,
|
|
311
|
+
label,
|
|
312
|
+
meta: textSegments[1] ?? formatLinkMeta(href),
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function parseLinkCards(code: string): LinkCard[] | undefined {
|
|
317
|
+
const trimmedCode = code.trim()
|
|
318
|
+
if (!trimmedCode.startsWith(LINKS_MARKER)) {
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
const linkCards = trimmedCode
|
|
322
|
+
.slice(LINKS_MARKER.length)
|
|
323
|
+
.split("\n")
|
|
324
|
+
.flatMap((line) => {
|
|
325
|
+
const linkCard = parseLinkCard(line)
|
|
326
|
+
return linkCard ? [linkCard] : []
|
|
327
|
+
})
|
|
328
|
+
if (linkCards.length === 0) {
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
return linkCards
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function renderLinkCards(code: string): JSX.Element[] | undefined {
|
|
335
|
+
const linkCards = parseLinkCards(code)
|
|
336
|
+
if (!linkCards) {
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
return linkCards.map((linkCard) => {
|
|
340
|
+
const isExternal = isExternalHref(linkCard.href)
|
|
341
|
+
return (
|
|
342
|
+
<a
|
|
343
|
+
className="bg-bg-secondary border-border-secondary hover:bg-gray-3 group inline-flex max-w-full items-center gap-6 rounded-full border px-6 py-4 transition-colors"
|
|
344
|
+
href={linkCard.href}
|
|
345
|
+
key={`${linkCard.label}-${linkCard.href}`}
|
|
346
|
+
rel={isExternal ? "noreferrer" : undefined}
|
|
347
|
+
target={isExternal ? "_blank" : undefined}
|
|
348
|
+
>
|
|
349
|
+
<div className="bg-gray-1 border-border-secondary text-gray-11 group-hover:text-gray-12 text-13 font-500 flex size-24 shrink-0 items-center justify-center rounded-full border font-mono uppercase transition-colors">
|
|
350
|
+
{linkCard.badge}
|
|
351
|
+
</div>
|
|
352
|
+
<p className="text-gray-12 text-13 font-500 min-w-0 truncate pr-2 leading-none whitespace-nowrap">
|
|
353
|
+
{linkCard.label}
|
|
354
|
+
</p>
|
|
355
|
+
</a>
|
|
356
|
+
)
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function renderMappedComponents(code: string): Promise<JSX.Element | undefined> {
|
|
361
|
+
const trimmedCode = code.trim()
|
|
362
|
+
|
|
363
|
+
if (!trimmedCode.startsWith(COMPONENT_MARKER)) {
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const mdxSource = trimmedCode.slice(COMPONENT_MARKER.length).trim()
|
|
368
|
+
if (!mdxSource) {
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const scopedMdxSource = scopeMappedComponentReferences(mdxSource)
|
|
373
|
+
try {
|
|
374
|
+
const evaluated = await evaluate(scopedMdxSource, {
|
|
375
|
+
Fragment,
|
|
376
|
+
development: false,
|
|
377
|
+
jsx,
|
|
378
|
+
jsxs,
|
|
379
|
+
})
|
|
380
|
+
const MdxContent = evaluated.default
|
|
381
|
+
return <MdxContent components={components} />
|
|
382
|
+
} catch {
|
|
383
|
+
// Invalid component markup falls back to the highlighted code block renderer.
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function scopeMappedComponentReferences(mdxSource: string): string {
|
|
388
|
+
// MDX expressions like `as={Link}` resolve identifiers lexically, not from `components`.
|
|
389
|
+
// Rewrite mapped references to `props.components.*` so evaluated snippets can resolve them.
|
|
390
|
+
const componentReferencePattern = new RegExp(
|
|
391
|
+
`=\\{\\s*(${Object.keys(components)
|
|
392
|
+
.map((componentName) => componentName.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`))
|
|
393
|
+
.join("|")})\\s*\\}`,
|
|
394
|
+
"g",
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
return mdxSource.replaceAll(
|
|
398
|
+
componentReferencePattern,
|
|
399
|
+
(_match, componentName) => `={props.components.${componentName}}`,
|
|
400
|
+
)
|
|
401
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { NotionBlock } from "@/components/notion/notion"
|
|
2
|
+
import { NotionBlockContent } from "@/components/notion/notion-block-content"
|
|
3
|
+
|
|
4
|
+
interface NotionDocsBlocksProps {
|
|
5
|
+
blocks: NotionBlock[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function NotionDocsBlocks(props: NotionDocsBlocksProps) {
|
|
9
|
+
const { blocks } = props
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
{blocks.map((block, index) => (
|
|
14
|
+
<NotionBlockContent block={block} blockIndex={index} key={index} />
|
|
15
|
+
))}
|
|
16
|
+
</>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cacheTag } from "next/cache"
|
|
2
|
+
import { getNotionPage } from "@/components/notion/notion"
|
|
3
|
+
import { NotionDocsBlocks } from "@/components/notion/notion-docs-blocks"
|
|
4
|
+
|
|
5
|
+
interface NotionDocsCodePageProps {
|
|
6
|
+
pageId: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function NotionDocsCodePage(props: NotionDocsCodePageProps) {
|
|
10
|
+
"use cache"
|
|
11
|
+
|
|
12
|
+
const { pageId } = props
|
|
13
|
+
|
|
14
|
+
cacheTag("notion")
|
|
15
|
+
cacheTag(`notion-page:${pageId}`)
|
|
16
|
+
|
|
17
|
+
const { blocks } = await getNotionPage(pageId)
|
|
18
|
+
|
|
19
|
+
return <NotionDocsBlocks blocks={blocks} />
|
|
20
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Column, Spacer } from "@pump-fun/retail-design-system"
|
|
2
|
+
import { cacheTag } from "next/cache"
|
|
3
|
+
import type { PropsWithChildren } from "react"
|
|
4
|
+
import { getNotionPage } from "@/components/notion/notion"
|
|
5
|
+
import { NotionDocsBlocks } from "@/components/notion/notion-docs-blocks"
|
|
6
|
+
import { NotionTabs } from "@/components/notion/notion-tabs"
|
|
7
|
+
|
|
8
|
+
type NotionDocsLayoutProps = PropsWithChildren<{
|
|
9
|
+
childPageIds?: string[]
|
|
10
|
+
pageId: string
|
|
11
|
+
}>
|
|
12
|
+
|
|
13
|
+
export async function NotionDocsLayout(props: NotionDocsLayoutProps) {
|
|
14
|
+
"use cache"
|
|
15
|
+
|
|
16
|
+
const { children, pageId } = props
|
|
17
|
+
|
|
18
|
+
cacheTag("notion")
|
|
19
|
+
cacheTag(`notion-page:${pageId}`)
|
|
20
|
+
|
|
21
|
+
const { blocks, lastEditedTime, tabs, title } = await getNotionPage(pageId)
|
|
22
|
+
|
|
23
|
+
const formattedUpdatedTime = new Intl.DateTimeFormat("en-US", {
|
|
24
|
+
dateStyle: "long",
|
|
25
|
+
timeStyle: "short",
|
|
26
|
+
}).format(new Date(lastEditedTime))
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Column>
|
|
30
|
+
<Column className="mx-auto w-full max-w-[620px]">
|
|
31
|
+
{/* Title */}
|
|
32
|
+
<h1 className="text-36 font-500 leading-[1.2]">{title}</h1>
|
|
33
|
+
<Spacer height={8} />
|
|
34
|
+
|
|
35
|
+
{/* Last Updated Time */}
|
|
36
|
+
<p className="text-gray-11 text-14 flex items-center gap-x-8">
|
|
37
|
+
<span>Last updated on {formattedUpdatedTime}</span>
|
|
38
|
+
</p>
|
|
39
|
+
<Spacer height={16} />
|
|
40
|
+
|
|
41
|
+
{/* Description and links */}
|
|
42
|
+
<NotionDocsBlocks blocks={blocks} />
|
|
43
|
+
<Spacer height={24} />
|
|
44
|
+
|
|
45
|
+
{/* Tabs */}
|
|
46
|
+
{tabs.length > 0 && <NotionTabs tabs={tabs} />}
|
|
47
|
+
|
|
48
|
+
{children}
|
|
49
|
+
</Column>
|
|
50
|
+
</Column>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Button } from "@pump-fun/retail-design-system"
|
|
4
|
+
import { useFormStatus } from "react-dom"
|
|
5
|
+
|
|
6
|
+
export function NotionRevalidateButtonClient() {
|
|
7
|
+
const { pending } = useFormStatus()
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Button isLoading={pending} type="submit" variant="secondary">
|
|
11
|
+
{pending ? "Refreshing..." : "Refresh Notion content cache"}
|
|
12
|
+
</Button>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { revalidatePath, revalidateTag } from "next/cache"
|
|
2
|
+
import { NotionRevalidateButtonClient } from "@/components/notion/notion-revalidate-button-client"
|
|
3
|
+
|
|
4
|
+
export function NotionRevalidateButton() {
|
|
5
|
+
return (
|
|
6
|
+
<div className="mx-auto w-full max-w-[620px]">
|
|
7
|
+
<form action={revalidateNotionPageAction}>
|
|
8
|
+
<NotionRevalidateButtonClient />
|
|
9
|
+
</form>
|
|
10
|
+
</div>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// oxlint-disable-next-line require-await
|
|
15
|
+
async function revalidateNotionPageAction() {
|
|
16
|
+
"use server"
|
|
17
|
+
|
|
18
|
+
revalidateTag("notion", "max")
|
|
19
|
+
revalidatePath("/")
|
|
20
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
import type { NotionRichTextSegment } from "@/components/notion/notion"
|
|
3
|
+
|
|
4
|
+
interface NotionRichTextSegmentsProps {
|
|
5
|
+
blockIndex: number
|
|
6
|
+
segments: NotionRichTextSegment[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function NotionRichTextSegments(props: NotionRichTextSegmentsProps) {
|
|
10
|
+
const { blockIndex, segments } = props
|
|
11
|
+
|
|
12
|
+
return segments.map((segment, segmentIndex) => {
|
|
13
|
+
let segmentContent: ReactNode = segment.text
|
|
14
|
+
|
|
15
|
+
if (segment.code) {
|
|
16
|
+
segmentContent = (
|
|
17
|
+
<code className="rounded-4 bg-bg-secondary text-14 px-4 py-2 font-mono">
|
|
18
|
+
{segmentContent}
|
|
19
|
+
</code>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (segment.bold) {
|
|
24
|
+
segmentContent = <strong>{segmentContent}</strong>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (segment.italic) {
|
|
28
|
+
segmentContent = <em>{segmentContent}</em>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (segment.strikethrough) {
|
|
32
|
+
segmentContent = <s>{segmentContent}</s>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (segment.underline) {
|
|
36
|
+
segmentContent = <u>{segmentContent}</u>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (segment.href) {
|
|
40
|
+
return (
|
|
41
|
+
<a
|
|
42
|
+
className="decoration-gray-9 underline decoration-1 underline-offset-2 hover:no-underline"
|
|
43
|
+
href={segment.href}
|
|
44
|
+
key={`${blockIndex}-${segmentIndex}`}
|
|
45
|
+
rel="noreferrer"
|
|
46
|
+
target="_blank"
|
|
47
|
+
>
|
|
48
|
+
{segmentContent}
|
|
49
|
+
</a>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return <span key={`${blockIndex}-${segmentIndex}`}>{segmentContent}</span>
|
|
54
|
+
})
|
|
55
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Tabs, TabsUnderlineList, TabsUnderlineTab } from "@pump-fun/retail-design-system"
|
|
4
|
+
import Link, { type LinkProps } from "next/link"
|
|
5
|
+
import { usePathname } from "next/navigation"
|
|
6
|
+
import type { NotionTab } from "@/components/notion/notion"
|
|
7
|
+
|
|
8
|
+
interface NotionTabsProps {
|
|
9
|
+
tabs: NotionTab[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function NotionTabs(props: NotionTabsProps) {
|
|
13
|
+
const { tabs } = props
|
|
14
|
+
const pathname = usePathname()
|
|
15
|
+
|
|
16
|
+
if (tabs.length === 0) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const selectedValue = tabs.some((tab) => tab.href === pathname) ? pathname : tabs[0].href
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Tabs value={selectedValue}>
|
|
24
|
+
<TabsUnderlineList>
|
|
25
|
+
{tabs.map((tab, index) => (
|
|
26
|
+
<TabsUnderlineTab
|
|
27
|
+
isNativeButton={false}
|
|
28
|
+
key={index}
|
|
29
|
+
render={<Link href={tab.href as LinkProps<string>["href"]} />}
|
|
30
|
+
value={tab.href}
|
|
31
|
+
>
|
|
32
|
+
{tab.label}
|
|
33
|
+
</TabsUnderlineTab>
|
|
34
|
+
))}
|
|
35
|
+
</TabsUnderlineList>
|
|
36
|
+
</Tabs>
|
|
37
|
+
)
|
|
38
|
+
}
|