docs: migrate UI docs (#13245)

* docs: create a new UI docs project (#13233)

* docs: create a new UI docs project

* fix installation errors

* docs: migrate UI docs content to new project (#13241)

* Fix content

* added examples for some components

* finish adding examples

* lint fix

* fix build errors

* delete empty files

* path fixes + refactor

* fix build error
This commit is contained in:
Shahed Nasser
2025-08-20 11:42:25 +03:00
committed by GitHub
parent 5b7a041246
commit d1a1135328
748 changed files with 13079 additions and 7319 deletions
+191
View File
@@ -0,0 +1,191 @@
"use client"
import React from "react"
import { CopyButton, H2, Hr, MarkdownContent, useColorMode } from "docs-ui"
import { colors as allColors } from "@/config/colors"
import { clx } from "@medusajs/ui"
import slugify from "slugify"
type Color = {
name: string
code: string
}
type ColorsTable = {
[k: string]: {
description?: string
colors: Color[]
}
}
interface ColorBlockProps extends React.HTMLAttributes<HTMLDivElement> {
colour: Color
}
const ColorBlock = ({ colour, className, ...props }: ColorBlockProps) => {
const [mounted, setMounted] = React.useState(false)
React.useEffect(() => setMounted(true), [])
if (!mounted) {
return (
<div className="flex w-fit flex-row items-center gap-x-2">
<div
className={
"border-medusa-border-base h-[48px] w-[48px] rounded-lg border p-1"
}
>
<div
className={clx(
"bg-medusa-bg-component h-full w-full animate-pulse rounded-[4px]",
className
)}
{...props}
/>
</div>
<div className="flex flex-col items-start">
<div className="bg-medusa-bg-component h-[20px] w-[85px] animate-pulse rounded-sm" />
<div className="bg-medusa-bg-subtle h-[20px] w-[120px] animate-pulse rounded-sm" />
</div>
</div>
)
}
return (
<div className="flex flex-row items-center gap-x-2">
<div
className={
"border-medusa-border-base h-[48px] w-[48px] rounded-lg border p-1"
}
>
<div
className={clx("h-full w-full rounded-[4px]", className)}
style={{ background: colour.code }}
{...props}
/>
</div>
<div className="flex flex-col items-start">
<p className="txt-compact-xsmall-plus text-medusa-fg-basetext-start">
{cssVarToTailwindClass(colour.name)}
</p>
<p className="txt-compact-xsmall text-medusa-fg-subtle">
{colour.code}
</p>
</div>
</div>
)
}
const cssVarToTailwindClass = (name: string) => {
if (name.startsWith("--bg") || name.startsWith("--button")) {
return name.replace("-", "bg-ui")
}
if (name.startsWith("--fg")) {
return name.replace("-", "text-ui")
}
if (name.startsWith("--border")) {
return name.replace("-", "border-ui")
}
if (name.startsWith("--tag")) {
if (name.includes("bg")) {
return name.replace("-", "bg-ui")
}
if (name.includes("border")) {
return name.replace("-", "border-ui")
}
if (name.includes("icon") || name.includes("text")) {
return name.replace("-", "text-ui")
}
}
if (name.startsWith("--contrast") || name.startsWith("--alpha")) {
return name.replace("-", "bg-ui")
}
return name
}
const transformPrefixToTitle = (prefix: string) => {
switch (prefix) {
case "bg":
return "Background"
case "fg":
return "Foreground"
default:
return prefix.charAt(0).toUpperCase() + prefix.slice(1)
}
}
const getDescriptionOfSection = (title: string) => {
switch (title) {
case "Alpha":
case "Contrast":
return "These colors can be used for foreground (using `text-` prefix), background (using `bg-` prefix), and border (using `border-` prefix) elements."
default:
return ""
}
}
const Colors = () => {
const { colorMode } = useColorMode()
const colors: ColorsTable = {}
for (const [tag, value] of Object.entries(allColors[colorMode])) {
const prefixMatch = tag.match(/(--[a-zA-Z]+)/gi)
if (!prefixMatch) {
return
}
const prefix = transformPrefixToTitle(prefixMatch[0].replace("--", ""))
if (!colors[prefix]) {
colors[prefix] = {
description: getDescriptionOfSection(prefix),
colors: [],
}
}
colors[prefix].colors.push({
name: tag,
code: value as string,
})
}
const sortedSections = Object.entries(colors).sort((a, b) => {
return a[0].localeCompare(b[0])
})
for (const [, sectionData] of sortedSections) {
sectionData.colors.sort((a, b) => {
return a.name < b.name ? -1 : 1
})
}
return (
<div>
{sortedSections.map(([section, sectionData], index) => (
<div className="mb-16" key={`colours-section-${section}`}>
<H2 id={slugify(section)}>{section}</H2>
{sectionData.description && (
<MarkdownContent>{sectionData.description}</MarkdownContent>
)}
<div className="xs:grid-cols-2 mb-8 grid grid-cols-1 gap-4 gap-y-10 sm:grid-cols-3 ">
{sectionData.colors.map((colour) => (
<CopyButton
text={cssVarToTailwindClass(colour.name)}
key={`colours-section-${section}-${colour.name}`}
>
<ColorBlock colour={colour} />
</CopyButton>
))}
</div>
{index !== sortedSections.length - 1 && <Hr />}
</div>
))}
</div>
)
}
export { Colors }
@@ -0,0 +1,86 @@
"use client"
import { Spinner } from "@medusajs/icons"
import { Tabs, clx } from "@medusajs/ui"
import { CodeBlock } from "docs-ui"
import * as React from "react"
import Feedback from "@/components/Feedback"
import { ExampleRegistry } from "@/specs/examples.mjs"
interface ComponentExampleProps extends React.HTMLAttributes<HTMLDivElement> {
name: string
disableCenterAlignPreview?: boolean
hideFeedback?: boolean
}
export function ComponentExample({
children,
name,
disableCenterAlignPreview = false,
hideFeedback = false,
...props
}: ComponentExampleProps) {
const Preview = React.useMemo(() => {
const Component = ExampleRegistry[name]?.component
if (!Component) {
return <p>Component {name} not found in registry</p>
}
return <Component />
}, [name])
const CodeElement = children as React.ReactElement
const Code = JSON.parse(
(CodeElement.props as Record<string, string>).codeLinesJSON
).join("\n")
return (
<div className="relative my-4 flex flex-col space-y-2" {...props}>
<Tabs defaultValue="preview" className="relative mr-auto w-full">
<div className="flex flex-col pb-3">
<Tabs.List>
<Tabs.Trigger value="preview">Preview</Tabs.Trigger>
<Tabs.Trigger value="code">Code</Tabs.Trigger>
</Tabs.List>
<Tabs.Content
value="preview"
className="relative data-[state=active]:mt-4"
>
<div
className={clx(
"bg-medusa-bg-base border-medusa-border-base flex max-h-[400px] min-h-[400px]",
"w-full overflow-auto justify-center rounded-md border px-10 py-5",
!disableCenterAlignPreview && "items-center"
)}
>
<React.Suspense
fallback={
<div className="text-medusa-fg-muted flex items-center text-sm">
<Spinner className="animate-spin" />
</div>
}
>
{Preview}
</React.Suspense>
</div>
</Tabs.Content>
<Tabs.Content
value="code"
className="relative data-[state=active]:mt-4"
>
<CodeBlock source={Code} lang="tsx" />
</Tabs.Content>
</div>
</Tabs>
{!hideFeedback && (
<Feedback
title={`example ${name}`}
question="Was this example helpful?"
showDottedSeparator={false}
/>
)}
</div>
)
}
@@ -0,0 +1,93 @@
import { Documentation } from "react-docgen"
import { Suspense } from "react"
import { Spinner } from "@medusajs/icons"
import { PropTable } from "../PropsTable"
import { Container } from "@medusajs/ui"
import Feedback from "../Feedback"
import { H3, MarkdownContent } from "docs-ui"
import MDXComponents from "../MDXComponents"
import slugify from "slugify"
type ComponentReferenceProps = {
mainComponent: string
componentsToShow?: string[]
specsSrc?: string
hideFeedback?: boolean
}
const ComponentReference = ({
mainComponent,
componentsToShow = [mainComponent],
specsSrc,
hideFeedback = false,
}: ComponentReferenceProps) => {
if (!specsSrc) {
return <></>
}
const specs = JSON.parse(specsSrc) as Documentation[]
return (
<>
{componentsToShow.map((component, index) => {
const componentSpec = specs?.find(
(spec) => spec.displayName === component
)
const hasProps =
componentSpec?.props && Object.keys(componentSpec.props).length > 0
const componentName =
componentsToShow.length > 1
? componentSpec?.displayName || component
: ""
const componentSlug = slugify(componentName)
return (
<Suspense
fallback={
<div className="text-medusa-fg-muted flex flex-1 items-center justify-center">
<Spinner className="animate-spin" />
</div>
}
key={index}
>
{componentSpec && (
<>
{componentsToShow.length > 1 && (
<H3 id={componentSlug}>{componentName}</H3>
)}
{componentSpec.description && (
<MarkdownContent components={MDXComponents}>
{componentSpec.description}
</MarkdownContent>
)}
{hasProps && (
<>
<Container className="mb-6 mt-8 overflow-hidden p-0">
<Suspense
fallback={
<div className="text-medusa-fg-muted flex flex-1 items-center justify-center">
<Spinner className="animate-spin" />
</div>
}
>
<PropTable props={componentSpec.props!} />
</Suspense>
</Container>
{!hideFeedback && (
<Feedback
title={`props of ${component}`}
question="Was this helpful?"
showDottedSeparator={false}
/>
)}
</>
)}
</>
)}
</Suspense>
)
})}
</>
)
}
export { ComponentReference }
@@ -0,0 +1,37 @@
"use client"
import { EditButton as UiEditButton } from "docs-ui"
import { usePathname } from "next/navigation"
import { useCallback, useEffect, useState } from "react"
const EditButton = () => {
const pathname = usePathname()
const [editDate, setEditDate] = useState<string | undefined>()
const loadEditDate = useCallback(async () => {
const generatedEditDates = (await import("../../generated/edit-dates.mjs"))
.generatedEditDates
setEditDate(
(generatedEditDates as Record<string, string>)[
`app${pathname.replace(/\/$/, "")}/page.mdx`
]
)
}, [pathname])
useEffect(() => {
void loadEditDate()
}, [loadEditDate])
if (!editDate) {
return <></>
}
return (
<UiEditButton
filePath={`/www/apps/ui/app${pathname.replace(/\/$/, "")}/page.mdx`}
editDate={editDate}
/>
)
}
export default EditButton
+28
View File
@@ -0,0 +1,28 @@
"use client"
import {
Feedback as UiFeedback,
FeedbackProps as UiFeedbackProps,
} from "docs-ui"
import { usePathname } from "next/navigation"
import { basePathUrl } from "../../utils/base-path-url"
import { useMemo } from "react"
type FeedbackProps = Omit<UiFeedbackProps, "event" | "pathName">
const Feedback = (props: FeedbackProps) => {
const pathname = usePathname()
const feedbackPathname = useMemo(() => basePathUrl(pathname), [pathname])
return (
<UiFeedback
event="survey"
pathName={feedbackPathname}
question="Was this guide helpful?"
{...props}
/>
)
}
export default Feedback
@@ -0,0 +1,15 @@
import React from "react"
import { Card } from "docs-ui"
import { basePathUrl } from "@/utils/base-path-url"
export const FigmaCard = () => {
return (
<Card
title="Medusa UI"
text="Colors, type, icons and components"
href="https://www.figma.com/community/file/1278648465968635936/Medusa-UI"
image={basePathUrl("/images/figma.png")}
iconClassName="!p-0"
/>
)
}
+17
View File
@@ -0,0 +1,17 @@
"use client"
import { Footer as UiFooter } from "docs-ui"
import Feedback from "../Feedback"
import EditButton from "../EditButton"
const Footer = () => {
return (
<UiFooter
showPagination={true}
feedbackComponent={<Feedback className="my-2" />}
editComponent={<EditButton />}
/>
)
}
export default Footer
+136
View File
@@ -0,0 +1,136 @@
import { InformationCircleSolid } from "@medusajs/icons"
import {
HookData,
HookDataMap,
EnumType,
FunctionType,
ObjectType,
} from "@/types/ui"
import { InlineCode, Table, Tooltip } from "docs-ui"
interface HookTableProps {
props: HookDataMap
isReturn?: boolean
}
const HookTable = ({ props, isReturn = false }: HookTableProps) => {
return (
<Table className="!mb-0">
<Table.Header className="border-t-0">
<Table.Row>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Type</Table.HeaderCell>
<Table.HeaderCell className="!text-right">
{isReturn ? "Description" : "Default"}
</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body className="border-b-0 [&_tr:last-child]:border-b-0">
{/* eslint-disable-next-line react/prop-types */}
{props.map((propData, index) => (
<Row key={index} {...propData} isReturn={isReturn} />
))}
</Table.Body>
</Table>
)
}
interface RowProps extends HookData {
isReturn?: boolean
}
const Row = ({
value,
type,
description,
default: defaultValue,
isReturn = false,
}: RowProps) => {
const isEnum = (t: unknown): t is EnumType => {
return (t as EnumType).type !== undefined && (t as EnumType).type === "enum"
}
const isObject = (t: unknown): t is ObjectType => {
return (
(t as ObjectType).type !== undefined &&
(t as ObjectType).type === "object"
)
}
const isFunction = (t: unknown): t is FunctionType => {
return (
(t as FunctionType).type !== undefined &&
(t as FunctionType).type === "function"
)
}
const isComplexType = isEnum(type) || isObject(type) || isFunction(type)
return (
<Table.Row className="code-body">
<Table.Cell>
<div className="flex items-center gap-x-1">
<InlineCode>{value}</InlineCode>
{!isReturn && description && (
<Tooltip content={description} className="max-w-[350px] text-left">
<InformationCircleSolid className="text-medusa-fg-subtle" />
</Tooltip>
)}
</div>
</Table.Cell>
<Table.Cell>
{!isComplexType && type.toString()}
{isEnum(type) && (
<Tooltip
content={type.values.map((v) => `"${v}"`).join(" | ")}
className="font-mono"
>
<div className="flex items-center gap-x-1">
<span>enum</span>
<InformationCircleSolid className="text-medusa-fg-subtle" />
</div>
</Tooltip>
)}
{isObject(type) && (
<Tooltip
tooltipChildren={<pre>{type.shape}</pre>}
className="font-mono max-w-[500px]"
tooltipClassName="!text-left"
>
<div className="flex items-center gap-x-1">
<span>{type.name}</span>
<InformationCircleSolid className="text-medusa-fg-subtle" />
</div>
</Tooltip>
)}
{isFunction(type) && (
<Tooltip
tooltipChildren={<pre>{type.signature}</pre>}
className="font-mono max-w-[500px]"
>
<div className="flex items-center gap-x-1">
<span>function</span>
<InformationCircleSolid className="text-medusa-fg-subtle" />
</div>
</Tooltip>
)}
</Table.Cell>
<Table.Cell className="!text-right">
{isReturn ? (
description ? (
<span>{description}</span>
) : (
<span className="text-medusa-fg-muted"> - </span>
)
) : defaultValue !== undefined && defaultValue !== null ? (
<InlineCode>{defaultValue.toString()}</InlineCode>
) : (
<span className="text-medusa-fg-muted"> - </span>
)}
</Table.Cell>
</Table.Row>
)
}
export { HookTable }
@@ -0,0 +1,51 @@
import { Spinner } from "@medusajs/icons"
import { Container } from "@medusajs/ui"
import * as React from "react"
import { HookRegistry } from "@/specs/hooks"
import Feedback from "../Feedback"
type HookValuesProps = {
hook: string
hideFeedback?: boolean
}
const HookValues = ({ hook, hideFeedback = false }: HookValuesProps) => {
const Props = React.useMemo(() => {
const Table = HookRegistry[hook]?.table
if (!Table) {
return (
<div className="flex min-h-[200px] w-full items-center justify-center">
<p className="txt-compact-small">
No API reference found for{" "}
<span className="txt-compact-small-plus">{hook}</span>
</p>
</div>
)
}
return <Table />
}, [hook])
return (
<>
<Container className="mb-6 mt-8 overflow-hidden p-0">
<React.Suspense
fallback={
<div className="text-medusa-fg-muted flex flex-1 items-center justify-center">
<Spinner className="animate-spin" />
</div>
}
>
{Props}
</React.Suspense>
</Container>
{!hideFeedback && (
<Feedback title={`props of ${hook}`} showDottedSeparator={false} />
)}
</>
)
}
export { HookValues }
@@ -0,0 +1,94 @@
"use client"
import * as Icons from "@medusajs/icons"
import { Container, Input, Text } from "@medusajs/ui"
import clsx from "clsx"
import { CopyButton } from "docs-ui"
import * as React from "react"
const iconNames = Object.keys(Icons).filter((name) => name !== "default")
const IconSearch = () => {
const [query, setQuery] = React.useState<string | undefined>("")
return (
<div className="mt-8 flex flex-col gap-y-2">
<Input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<Container>
<SearchResults query={query} />
</Container>
</div>
)
}
const SearchResults = ({ query = "" }: { query?: string }) => {
const cleanQuery = escapeStringRegexp(query.trim().replace(/\s/g, " "))
const results = iconNames.filter((name) =>
new RegExp(`\\b${cleanQuery}`, "gi").test(name)
)
if (results.length === 0) {
return (
<div
className={clsx(
"text-medusa-text-muted dark:text-medusa-text-muted-dark",
"flex min-h-[300px] items-center justify-center"
)}
>
<Text>
No results found for{" "}
<Text weight={"plus"} asChild>
<span>{query}</span>
</Text>
</Text>
</div>
)
}
return (
<div className="grid w-full grid-cols-4 gap-8 md:grid-cols-6 lg:grid-cols-8">
{results.map((name) => {
return (
<div
key={name}
className="flex h-full w-full items-center justify-center"
>
<CopyButton text={name} tooltipText={name} handleTouch>
<div
className={clsx(
"border-medusa-border-base",
"flex h-10 w-10 items-center justify-center rounded-lg border"
)}
>
<span className="sr-only">Icon named {name}</span>
<div
className={clsx(
"bg-medusa-bg-component text-medusa-fg-base",
"flex h-8 w-8 items-center justify-center rounded-[4px]"
)}
>
{React.createElement(Icons[name as keyof typeof Icons])}
</div>
</div>
</CopyButton>
</div>
)
})}
</div>
)
}
// https://github.com/sindresorhus/escape-string-regexp/blob/main/index.js
function escapeStringRegexp(string: unknown) {
if (typeof string !== "string") {
throw new TypeError("Expected a string")
}
return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d")
}
export { IconSearch }
@@ -0,0 +1,16 @@
import type { MDXComponents as MDXComponentsType } from "mdx/types"
import {
Link,
MDXComponents as UiMdxComponents,
InlineThemeImage,
InlineIcon,
} from "docs-ui"
const MDXComponents: MDXComponentsType = {
...UiMdxComponents,
a: Link,
InlineThemeImage,
InlineIcon,
}
export default MDXComponents
+196
View File
@@ -0,0 +1,196 @@
"use client"
import { InformationCircleSolid } from "@medusajs/icons"
import { PropData, PropDataMap, PropSpecType } from "@/types/ui"
import { useCallback, useMemo } from "react"
import { InlineCode, MarkdownContent, Table, Tooltip } from "docs-ui"
type PropTableProps = {
props: PropDataMap
}
const PropTable = ({ props }: PropTableProps) => {
return (
<Table className="!mb-0">
<Table.Header className="border-t-0">
<Table.Row>
<Table.HeaderCell>Prop</Table.HeaderCell>
<Table.HeaderCell>Type</Table.HeaderCell>
<Table.HeaderCell className="!text-right">Default</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body className="border-b-0 [&_tr:last-child]:border-b-0">
{Object.entries(props).map(([propName, propData]) => (
<Row key={propName} propName={propName} propData={propData} />
))}
</Table.Body>
</Table>
)
}
type RowProps = {
propName: string
propData: PropData
}
type TypeNode = {
text: string
tooltipContent?: string
canBeCopied?: boolean
}
const Row = ({
propName,
propData: { tsType: tsType, defaultValue, description },
}: RowProps) => {
const normalizeRaw = (str: string): string => {
return str
.replaceAll("\\|", "|")
.replaceAll("&#60;", "<")
.replaceAll("&#62;", ">")
}
const getTypeRaw = useCallback((type: PropSpecType): string => {
let raw = "raw" in type ? type.raw || type.name : type.name
if ("type" in type) {
if (type.type === "object") {
raw = `{\n ${type.signature.properties
.map((property) => `${property.key}: ${property.value.name}`)
.join("\n ")}\n}`
} else {
raw = type.raw
}
} else if (type.name === "Array" && "elements" in type) {
raw = type.elements.map((element) => getTypeRaw(element)).join(" | ")
}
return normalizeRaw(raw)
}, [])
const getTypeText = useCallback((type: PropSpecType): string => {
if (type?.name === "signature" && "type" in type) {
return type.type
} else if (type?.name === "Array" && type.raw) {
return normalizeRaw(type.raw) || "array"
}
return type.name || ""
}, [])
const getTypeTooltipContent = useCallback(
(type: PropSpecType): string | undefined => {
if (
(type?.name === "signature" && "type" in type) ||
(type?.name === "Array" && type.raw) ||
("raw" in type && type.raw)
) {
return getTypeRaw(type)
}
return undefined
},
[getTypeRaw]
)
const typeNodes = useMemo((): TypeNode[] => {
const typeNodes: TypeNode[] = []
if (tsType?.name === "union" && "elements" in tsType) {
tsType.elements.forEach((element) => {
if (
("elements" in element && element.elements.length) ||
"signature" in element
) {
const elementTypeText = getTypeText(element)
const elementTooltipContent = getTypeTooltipContent(element)
typeNodes.push({
text: elementTypeText,
tooltipContent:
elementTypeText !== elementTooltipContent
? elementTooltipContent
: undefined,
})
} else if ("value" in element) {
typeNodes.push({
text: element.value,
canBeCopied: true,
})
} else if ("raw" in element) {
typeNodes.push({
text: getTypeText(element),
tooltipContent: getTypeTooltipContent(element),
})
} else {
typeNodes.push({
text: element.name,
})
}
})
} else if (tsType) {
typeNodes.push({
text: getTypeText(tsType),
tooltipContent: getTypeTooltipContent(tsType),
})
}
return typeNodes
}, [tsType, getTypeText, getTypeTooltipContent])
const defaultVal: string | undefined = defaultValue?.value as string
return (
<Table.Row>
<Table.Cell>
<div className="flex items-center gap-x-1">
<InlineCode>{propName}</InlineCode>
{description && (
<Tooltip
tooltipChildren={
<MarkdownContent
allowedElements={["a", "code"]}
unwrapDisallowed={true}
>
{description}
</MarkdownContent>
}
>
<InformationCircleSolid className="text-medusa-fg-subtle" />
</Tooltip>
)}
</div>
</Table.Cell>
<Table.Cell>
<div className="flex items-center flex-wrap gap-1 py-1">
{typeNodes.map((typeNode, index) => (
<div key={index} className="flex items-center gap-x-1">
{index > 0 && <span>|</span>}
{typeNode.tooltipContent && (
<Tooltip
tooltipChildren={<pre>{typeNode.tooltipContent}</pre>}
className="font-mono !max-w-none"
tooltipClassName="!text-left"
>
<div className="flex items-center gap-x-1">
<code>{typeNode.text}</code>
<InformationCircleSolid className="text-medusa-fg-subtle" />
</div>
</Tooltip>
)}
{!typeNode.tooltipContent && (
<>
{typeNode.canBeCopied && (
<InlineCode>{typeNode.text}</InlineCode>
)}
{!typeNode.canBeCopied && <code>{typeNode.text}</code>}
</>
)}
</div>
))}
</div>
</Table.Cell>
<Table.Cell className="text-right">
{defaultVal && <InlineCode>{defaultVal}</InlineCode>}
{!defaultVal && " - "}
</Table.Cell>
</Table.Row>
)
}
export { PropTable }