docs: create docs workspace (#5174)

* docs: migrate ui docs to docs universe

* created yarn workspace

* added eslint and tsconfig configurations

* fix eslint configurations

* fixed eslint configurations

* shared tailwind configurations

* added shared ui package

* added more shared components

* migrating more components

* made details components shared

* move InlineCode component

* moved InputText

* moved Loading component

* Moved Modal component

* moved Select components

* Moved Tooltip component

* moved Search components

* moved ColorMode provider

* Moved Notification components and providers

* used icons package

* use UI colors in api-reference

* moved Navbar component

* used Navbar and Search in UI docs

* added Feedback to UI docs

* general enhancements

* fix color mode

* added copy colors file from ui-preset

* added features and enhancements to UI docs

* move Sidebar component and provider

* general fixes and preparations for deployment

* update docusaurus version

* adjusted versions

* fix output directory

* remove rootDirectory property

* fix yarn.lock

* moved code component

* added vale for all docs MD and MDX

* fix tests

* fix vale error

* fix deployment errors

* change ignore commands

* add output directory

* fix docs test

* general fixes

* content fixes

* fix announcement script

* added changeset

* fix vale checks

* added nofilter option

* fix vale error
This commit is contained in:
Shahed Nasser authored and GitHub committed 2023-09-21 20:57:15 +03:00
1 parent 19c5d5ba36
commit fa7c94b4cc
3209 files changed
+32188 -31018

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
module.exports = {
root: true,
extends: [
"docs"
],
parserOptions: {
project: true,
tsconfigRootDir: __dirname,
},
}
+70
View File
@@ -0,0 +1,70 @@
{
"name": "docs-ui",
"version": "0.0.0",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
},
"main": "./dist/cjs/index.js",
"types": "./dist/cjs/index.d.ts",
"module": "./dist/esm/index.js",
"exports": {
".": {
"require": {
"types": "./dist/cjs/index.d.ts",
"default": "./dist/cjs/index.js"
},
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
}
}
},
"sideEffects": false,
"files": [
"dist/**"
],
"scripts": {
"build": "yarn clean && yarn build:js:cjs && yarn build:js:esm",
"build:js:cjs": "tsc --project tsconfig.cjs.json && tsc-alias -p tsconfig.cjs.json",
"build:js:esm": "tsc --project tsconfig.esm.json && tsc-alias -p tsconfig.esm.json",
"clean": "rimraf dist",
"dev": "yarn build:js:cjs && yarn build:js:esm"
},
"devDependencies": {
"@types/react": "^17.0.1",
"@types/react-dom": "^17.0.1",
"clsx": "^2.0.0",
"cpy-cli": "^5.0.0",
"eslint-config-docs": "*",
"next": "latest",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"rimraf": "^5.0.1",
"tailwind": "*",
"tailwindcss": "^3.3.3",
"tsc-alias": "^1.8.7",
"tsup": "^5.10.1",
"typescript": "^5.1.6"
},
"peerDependencies": {
"@types/react": ">=17",
"@types/react-dom": ">=17",
"next": "*",
"react": ">=17",
"react-dom": ">=17"
},
"dependencies": {
"@medusajs/icons": "^1.0.0",
"@medusajs/ui": "^1.0.0",
"@octokit/request": "^8.1.1",
"@segment/analytics-next": "^1.55.0",
"algoliasearch": "^4.20.0",
"prism-react-renderer": "^2.0.6",
"react-instantsearch": "^7.0.3",
"react-tooltip": "^5.21.3",
"react-transition-group": "^4.4.5",
"react-uuid": "^2.0.0"
}
}
+1
View File
@@ -0,0 +1 @@
module.exports = require("tailwind/postcss.config")
@@ -0,0 +1,53 @@
import React from "react"
import clsx from "clsx"
export type BadgeVariant =
| "purple"
| "purple-dark"
| "orange"
| "orange-dark"
| "green"
| "green-dark"
| "blue"
| "blue-dark"
| "red"
| "neutral"
export type BadgeProps = {
className?: string
variant: BadgeVariant
} & React.HTMLAttributes<HTMLSpanElement>
export const Badge = ({ className, variant, children }: BadgeProps) => {
return (
<span
className={clsx(
"text-compact-x-small-plus px-docs_0.4 rounded-docs_sm border border-solid py-px text-center",
variant === "purple" &&
"bg-medusa-tag-purple-bg dark:bg-medusa-tag-purple-bg-dark text-medusa-tag-purple-text dark:text-medusa-tag-purple-text-dark border-medusa-tag-purple-border dark:border-medusa-tag-purple-border-dark",
variant === "purple-dark" &&
"bg-medusa-tag-purple-bg-dark text-medusa-tag-purple-text-dark border-medusa-tag-purple-border-dark",
variant === "orange" &&
"bg-medusa-tag-orange-bg dark:bg-medusa-tag-orange-bg-dark text-medusa-tag-orange-text dark:text-medusa-tag-orange-text-dark border-medusa-tag-orange-border dark:border-medusa-tag-orange-border-dark",
variant === "orange-dark" &&
"bg-medusa-tag-orange-bg-dark text-medusa-tag-orange-text-dark border-medusa-tag-orange-border-dark",
variant === "green" &&
"bg-medusa-tag-green-bg dark:bg-medusa-tag-green-bg-dark text-medusa-tag-green-text dark:text-medusa-tag-green-text-dark border-medusa-tag-green-border dark:border-medusa-tag-green-border-dark",
variant === "green-dark" &&
"bg-medusa-tag-green-bg-dark text-medusa-tag-green-text-dark border-medusa-tag-green-border-dark",
variant === "blue" &&
"bg-medusa-tag-blue-bg dark:bg-medusa-tag-blue-bg-dark text-medusa-tag-blue-text dark:text-medusa-tag-blue-text-dark border-medusa-tag-blue-border dark:border-medusa-tag-blue-border-dark",
variant === "blue-dark" &&
"bg-medusa-tag-blue-bg-dark text-medusa-tag-blue-text-dark border-medusa-tag-blue-border-dark",
variant === "red" &&
"bg-medusa-tag-red-bg dark:bg-medusa-tag-red-bg-dark text-medusa-tag-red-text dark:text-medusa-tag-red-text-dark border-medusa-tag-red-border dark:border-medusa-tag-red-border-dark",
variant === "neutral" &&
"bg-medusa-tag-neutral-bg dark:bg-medusa-tag-neutral-bg-dark text-medusa-tag-neutral-text dark:text-medusa-tag-neutral-text-dark border-medusa-tag-neutral-border dark:border-medusa-tag-neutral-border-dark",
"badge",
className
)}
>
{children}
</span>
)
}
@@ -0,0 +1,21 @@
import clsx from "clsx"
import React from "react"
export type BorderedProps = {
wrapperClassName?: string
} & React.HTMLAttributes<HTMLSpanElement>
export const Bordered = ({ wrapperClassName, children }: BorderedProps) => {
return (
<span
className={clsx(
"border-medusa-border-strong dark:border-medusa-border-strong-dark bg-docs-bg",
"dark:bg-docs-bg-dark mr-docs_1 inline-flex w-fit items-center justify-center rounded-docs_DEFAULT border border-solid p-[3px]",
"no-zoom-img",
wrapperClassName
)}
>
{children}
</span>
)
}
@@ -0,0 +1,51 @@
import React from "react"
import { Bordered } from "@/components/Bordered"
import clsx from "clsx"
import { IconProps } from "@medusajs/icons/dist/types"
export type BorderedIconProps = {
icon?: string
IconComponent?: React.FC<IconProps> | null
wrapperClassName?: string
iconWrapperClassName?: string
iconClassName?: string
iconColorClassName?: string
} & React.HTMLAttributes<HTMLSpanElement>
export const BorderedIcon = ({
icon = "",
IconComponent = null,
wrapperClassName,
iconWrapperClassName,
iconClassName,
iconColorClassName = "",
}: BorderedIconProps) => {
return (
<Bordered wrapperClassName={wrapperClassName}>
<span
className={clsx(
"rounded-docs_xs p-docs_0.125 bg-medusa-bg-component dark:bg-medusa-bg-component-dark inline-flex items-center justify-center",
iconWrapperClassName
)}
>
{!IconComponent && (
<img
src={icon || ""}
className={clsx(iconClassName, "bordered-icon")}
alt=""
/>
)}
{IconComponent && (
<IconComponent
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
iconClassName,
"bordered-icon",
iconColorClassName
)}
/>
)}
</span>
</Bordered>
)
}
@@ -0,0 +1,78 @@
import clsx from "clsx"
import React from "react"
export type ButtonVariants = "primary" | "secondary" | "clear"
export type ButtonType = "default" | "icon"
export type ButtonProps = {
isSelected?: boolean
disabled?: boolean
variant?: ButtonVariants
className?: string
buttonType?: ButtonType
buttonRef?: React.LegacyRef<HTMLButtonElement>
} & React.HTMLAttributes<HTMLButtonElement>
export const Button = ({
className,
children,
variant = "primary",
buttonType = "default",
buttonRef,
...props
}: ButtonProps) => {
const variantClasses = {
primary: [
"inline-flex flex-row justify-center items-center",
"py-[5px] px-docs_0.75 rounded-docs_sm cursor-pointer",
"bg-button-inverted bg-medusa-button-inverted dark:bg-button-inverted-dark dark:bg-medusa-button-inverted-dark",
"hover:bg-medusa-button-inverted-hover hover:bg-no-image dark:hover:bg-medusa-button-inverted-hover-dark hover:no-underline",
"active:bg-medusa-button-inverted-pressed active:bg-no-image dark:active:bg-medusa-button-inverted-pressed-dark",
"focus:bg-medusa-button-inverted-pressed focus:bg-no-image dark:focus:bg-medusa-button-inverted-pressed-dark",
"shadow-button-colored active:shadow-button-colored-focused focus:shadow-button-colored-focused transition-shadow",
"dark:shadow-button-colored-dark dark:active:shadow-button-colored-focused-dark dark:focus:shadow-button-colored-focused-dark",
"disabled:!bg-no-image disabled:bg-medusa-bg-disabled dark:disabled:bg-medusa-bg-disabled-dark",
"disabled:cursor-not-allowed disabled:border-medusa-border-base dark:disabled:border-medusa-border-base-dark",
"text-compact-small-plus text-medusa-fg-on-inverted dark:text-medusa-fg-on-inverted-dark",
"[&_a]:text-medusa-fg-on-inverted dark:[&_a]:text-medusa-fg-on-inverted-dark",
"disabled:text-medusa-fg-disabled dark:disabled:text-medusa-fg-disabled-dark",
"[&_a]:disabled:text-medusa-fg-disabled dark:[&_a]:disabled:text-medusa-fg-disabled-dark",
"border border-medusa-border-loud dark:border-medusa-border-loud-dark",
"select-none",
],
secondary: [
"inline-flex flex-row justify-center items-center",
"py-[5px] px-docs_0.75 rounded-docs_sm cursor-pointer",
"bg-button-neutral bg-medusa-button-neutral dark:bg-button-neutral-dark dark:bg-medusa-button-neutral-dark",
"hover:bg-medusa-button-neutral-hover hover:bg-no-image dark:hover:bg-medusa-button-neutral-hover-dark hover:no-underline",
"active:bg-medusa-button-neutral-pressed active:bg-no-image dark:active:bg-medusa-button-neutral-pressed-dark",
"focus:bg-medusa-button-neutral-pressed focus:bg-no-image dark:focus:bg-medusa-button-neutral-pressed-dark",
"disabled:!bg-no-image disabled:bg-medusa-bg-disabled dark:disabled:bg-medusa-bg-disabled-dark",
"disabled:cursor-not-allowed",
"border border-solid border-medusa-border-base dark:border-medusa-border-base-dark",
"text-compact-small-plus text-medusa-fg-base dark:text-medusa-fg-base-dark",
"[&_a]:text-medusa-fg-base dark:[&_a]:text-medusa-fg-base-dark",
"shadow-button-neutral focus:shadow-button-neutral-focused active:shadow-button-neutral-focused transition-shadow",
"dark:shadow-button-neutral dark:focus:shadow-button-neutral-focused dark:active:shadow-button-neutral-focused",
"select-none",
],
clear: ["bg-transparent shadow-none border-0 outline-none cursor-pointer"],
}
return (
<button
className={clsx(
variant === "primary" && variantClasses.primary,
variant === "secondary" && variantClasses.secondary,
variant === "clear" && variantClasses.clear,
buttonType === "icon" && "!px-docs_0.25",
className
)}
ref={buttonRef}
{...props}
>
{children}
</button>
)
}
@@ -0,0 +1,50 @@
import React from "react"
import { ArrowUpRightOnBox } from "@medusajs/icons"
import clsx from "clsx"
import { Link } from "@/components"
export type CardProps = {
icon?: React.ReactNode
title: string
text?: string
href?: string
className?: string
}
export const Card = ({ icon, title, text, href, className }: CardProps) => {
return (
<div
className={clsx(
"bg-medusa-bg-subtle dark:bg-medusa-bg-subtle-dark w-full rounded",
"shadow-card-rest dark:shadow-card-rest-dark py-docs_0.75 relative px-docs_1",
"flex items-center gap-docs_1 transition-shadow",
href && "hover:shadow-card-hover dark:hover:shadow-card-hover-dark",
className
)}
>
{icon}
<div className="flex items-center gap-docs_1 justify-between flex-1">
<div className="flex flex-col">
<span className="text-compact-medium-plus text-medusa-fg-base dark:text-medusa-fg-base-dark">
{title}
</span>
{text && (
<span className="text-compact-medium text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark">
{text}
</span>
)}
</div>
{href && (
<>
<ArrowUpRightOnBox className="text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark" />
<Link
href={href}
className="absolute left-0 top-0 h-full w-full rounded"
/>
</>
)}
</div>
</div>
)
}
@@ -0,0 +1,114 @@
"use client"
import React from "react"
import clsx from "clsx"
import { HighlightProps, Highlight, themes } from "prism-react-renderer"
import { CopyButton, useColorMode } from "docs-ui"
import { SquareTwoStackSolid } from "@medusajs/icons"
export type CodeBlockProps = {
source: string
lang?: string
className?: string
collapsed?: boolean
} & Omit<HighlightProps, "code" | "language" | "children">
export const CodeBlock = ({
source,
lang = "",
className,
collapsed = false,
...rest
}: CodeBlockProps) => {
const { colorMode } = useColorMode()
return (
<div
className={clsx(
"bg-medusa-code-bg-base dark:bg-medusa-code-bg-base-dark relative mb-docs_1 rounded-docs_DEFAULT",
"border-medusa-code-border dark:border-medusa-code-border-dark w-full max-w-full border",
"xs:after:content-[''] xs:after:rounded xs:after:absolute xs:after:right-0 xs:after:top-0 xs:after:w-[calc(10%+24px)] xs:after:h-full xs:after:bg-code-fade xs:dark:after:bg-code-fade-dark",
collapsed && "max-h-[400px] overflow-auto",
className
)}
>
<Highlight
theme={{
...themes.vsDark,
plain: {
...themes.vsDark.plain,
backgroundColor: colorMode === "light" ? "#111827" : "#1B1B1F",
},
}}
code={source.trim()}
language={lang}
{...rest}
>
{({
className: preClassName,
style,
tokens,
getLineProps,
getTokenProps,
}) => (
<>
<pre
style={{ ...style, fontStretch: "100%" }}
className={clsx(
"xs:max-w-[90%] relative !mt-0 break-words bg-transparent !outline-none",
"overflow-auto break-words rounded-docs_DEFAULT",
preClassName
)}
>
<code
className={clsx(
"text-code-body font-monospace table min-w-full pb-docs_1.5 print:whitespace-pre-wrap",
tokens.length > 1 && "pt-docs_1 pr-docs_1",
tokens.length <= 1 && "py-docs_0.5 px-docs_1"
)}
>
{tokens.map((line, i) => {
const lineProps = getLineProps({ line })
return (
<span
key={i}
{...lineProps}
className={clsx("table-row", lineProps.className)}
>
{tokens.length > 1 && (
<span
className={clsx(
"text-medusa-code-text-subtle dark:text-medusa-code-text-subtle-dark mr-docs_1 table-cell select-none",
"bg-medusa-code-bg-base dark:bg-medusa-code-bg-base-dark sticky left-0 w-[1%] px-docs_1 text-right"
)}
>
{i + 1}
</span>
)}
<span>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token })} />
))}
</span>
</span>
)
})}
</code>
</pre>
<div
className={clsx(
"absolute z-50 hidden gap-docs_1 md:flex",
tokens.length === 1 && "right-docs_0.75 top-[10px]",
tokens.length > 1 && "right-docs_1 top-docs_1"
)}
>
<CopyButton text={source} tooltipClassName="font-base">
<SquareTwoStackSolid className="text-medusa-code-icon dark:text-medusa-code-icon-dark" />
</CopyButton>
</div>
</>
)}
</Highlight>
</div>
)
}
@@ -0,0 +1,25 @@
import React from "react"
import { CodeBlock, InlineCode } from "@/components"
export type CodeMdxProps = {
className?: string
children?: React.ReactNode
}
// due to how mdx handles code blocks
// it is required that a code block specify a language
// to be considered a block. Otherwise, it will be
// considered as inline code
export const CodeMdx = ({ className, children }: CodeMdxProps) => {
if (!children) {
return <></>
}
const match = /language-(\w+)/.exec(className || "")
if (match) {
return <CodeBlock source={children as string} lang={match[1]} />
}
return <InlineCode>{children}</InlineCode>
}
@@ -0,0 +1,129 @@
"use client"
import React, { useCallback, useEffect, useMemo, useRef } from "react"
import clsx from "clsx"
import { CodeBlock, CodeBlockProps } from "@/components"
import { useTabs, BaseTabType, useScrollPositionBlocker } from "@/hooks"
export type TabType = {
code?: CodeBlockProps
codeBlock?: React.ReactNode
} & BaseTabType
export type CodeTabsProps = {
tabs: TabType[]
className?: string
group?: string
}
export const CodeTabs = ({
tabs,
className,
group = "client",
}: CodeTabsProps) => {
const { selectedTab, changeSelectedTab } = useTabs<TabType>({
tabs,
group,
})
const tabRefs: (HTMLButtonElement | null)[] = useMemo(() => [], [])
const codeTabSelectorRef = useRef<HTMLSpanElement | null>(null)
const codeTabsWrapperRef = useRef<HTMLDivElement | null>(null)
const { blockElementScrollPositionUntilNextRender } =
useScrollPositionBlocker()
const changeTabSelectorCoordinates = useCallback(
(selectedTabElm: HTMLElement) => {
if (!codeTabSelectorRef?.current || !codeTabsWrapperRef?.current) {
return
}
const selectedTabsCoordinates = selectedTabElm.getBoundingClientRect()
const tabsWrapperCoordinates =
codeTabsWrapperRef.current.getBoundingClientRect()
codeTabSelectorRef.current.style.left = `${
selectedTabsCoordinates.left - tabsWrapperCoordinates.left
}px`
codeTabSelectorRef.current.style.width = `${selectedTabsCoordinates.width}px`
codeTabSelectorRef.current.style.height = `${selectedTabsCoordinates.height}px`
},
[]
)
useEffect(() => {
if (codeTabSelectorRef?.current && tabRefs.length) {
const selectedTabElm = tabRefs.find(
(tab) => tab?.getAttribute("aria-selected") === "true"
)
if (selectedTabElm) {
changeTabSelectorCoordinates(
selectedTabElm.parentElement || selectedTabElm
)
}
}
}, [codeTabSelectorRef, tabRefs, changeTabSelectorCoordinates, selectedTab])
return (
<div
className={clsx(
"relative my-docs_1 w-full max-w-full overflow-auto",
className
)}
ref={codeTabsWrapperRef}
>
<span
className={clsx(
"xs:absolute xs:border xs:border-solid xs:border-medusa-code-border dark:xs:border-medusa-code-border-dark xs:bg-medusa-code-bg-base dark:xs:bg-medusa-code-bg-base-dark ",
"xs:transition-all xs:duration-200 xs:ease-ease xs:top-[13px] xs:z-[1] xs:rounded-full"
)}
ref={codeTabSelectorRef}
></span>
<ul
className={clsx(
"bg-medusa-code-bg-header dark:bg-medusa-code-bg-header-dark py-docs_0.75 flex !list-none rounded-t-docs_DEFAULT px-docs_1",
"border-medusa-code-border dark:border-medusa-code-border-dark border border-b-0",
"gap-docs_0.25 mb-0"
)}
>
{tabs.map((tab, index) => (
<li key={index}>
<button
className={clsx(
"text-compact-small-plus xs:border-0 py-docs_0.25 px-docs_0.75 relative z-[2] rounded-full border",
(!selectedTab || selectedTab.value !== tab.value) && [
"text-medusa-code-text-subtle dark:text-medusa-code-text-subtle-dark border-transparent",
"hover:bg-medusa-code-bg-base dark:hover:bg-medusa-code-bg-base-dark",
],
selectedTab?.value === tab.value && [
"text-medusa-code-text-base bg-medusa-code-bg-base xs:!bg-transparent",
"dark:text-medusa-code-text-base-dark dark:bg-medusa-code-bg-base-dark xs:!bg-transparent",
]
)}
ref={(tabControl) => tabRefs.push(tabControl)}
onClick={(e) => {
blockElementScrollPositionUntilNextRender(
e.target as HTMLButtonElement
)
changeSelectedTab(tab)
}}
aria-selected={selectedTab?.value === tab.value}
role="tab"
>
{tab.label}
</button>
</li>
))}
</ul>
<>
{selectedTab?.code && (
<CodeBlock
{...selectedTab?.code}
className={clsx(
"!mt-0 !rounded-t-none",
selectedTab.code.className
)}
/>
)}
{selectedTab?.codeBlock && <>{selectedTab.codeBlock}</>}
</>
</div>
)
}
@@ -0,0 +1,46 @@
"use client"
import React, { useState, useEffect, useRef, useCallback } from "react"
import copy from "copy-text-to-clipboard"
import clsx from "clsx"
import { Tooltip } from "@/components/Tooltip"
export type CopyButtonProps = {
text: string
buttonClassName?: string
tooltipClassName?: string
} & React.HTMLAttributes<HTMLDivElement>
export const CopyButton = ({
text,
buttonClassName = "",
tooltipClassName = "",
children,
}: CopyButtonProps) => {
const [isCopied, setIsCopied] = useState(false)
const copyTimeout = useRef<number>(0)
const handleCopy = useCallback(() => {
copy(text)
setIsCopied(true)
copyTimeout.current = window.setTimeout(() => {
setIsCopied(false)
}, 1000)
}, [text])
useEffect(() => () => window.clearTimeout(copyTimeout.current), [])
return (
<Tooltip
text={isCopied ? `Copied!` : `Copy to Clipboard`}
tooltipClassName={tooltipClassName}
>
<span
className={clsx("cursor-pointer", buttonClassName)}
onClick={handleCopy}
>
{children}
</span>
</Tooltip>
)
}
@@ -0,0 +1,64 @@
import React from "react"
import clsx from "clsx"
import { PlusMini } from "@medusajs/icons"
export type DetailsSummaryProps = {
title: React.ReactNode
subtitle?: string
badge?: React.ReactNode
expandable?: boolean
open?: boolean
className?: string
titleClassName?: string
} & Omit<React.HTMLAttributes<HTMLElement>, "title">
export const DetailsSummary = ({
title,
subtitle,
badge,
expandable = true,
open = false,
className,
titleClassName,
...rest
}: DetailsSummaryProps) => {
return (
<summary
className={clsx(
"py-docs_0.75 flex items-center justify-between",
expandable && "cursor-pointer",
!expandable &&
"border-medusa-border-base dark:border-medusa-border-base-dark border-y",
"no-marker",
className
)}
{...rest}
>
<span className="gap-docs_0.25 flex flex-col">
<span
className={clsx(
"text-compact-medium-plus text-medusa-fg-base dark:text-medusa-fg-base-dark",
titleClassName
)}
>
{title}
</span>
{subtitle && (
<span className="text-compact-medium text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark">
{subtitle}
</span>
)}
</span>
{(badge || expandable) && (
<span className="flex gap-docs_0.5">
{badge}
{expandable && (
<PlusMini
className={clsx("transition-transform", open && "rotate-45")}
/>
)}
</span>
)}
</summary>
)
}
@@ -0,0 +1,93 @@
"use client"
import React, { Suspense, cloneElement, useRef, useState } from "react"
import { Loading } from "@/components/Loading"
import clsx from "clsx"
import { CSSTransition } from "react-transition-group"
import { DetailsSummary } from "./Summary"
export type DetailsProps = {
openInitial?: boolean
summaryContent?: React.ReactNode
summaryElm?: React.ReactNode
} & React.HTMLAttributes<HTMLDetailsElement>
export const Details = ({
openInitial = false,
summaryContent,
summaryElm,
children,
...props
}: DetailsProps) => {
const [open, setOpen] = useState(openInitial)
const [showContent, setShowContent] = useState(openInitial)
const ref = useRef<HTMLDetailsElement>(null)
const handleToggle = () => {
if (open) {
setShowContent(false)
} else {
setOpen(true)
setShowContent(true)
}
}
return (
<details
{...props}
ref={ref}
open={open}
onClick={(event) => {
event.preventDefault()
}}
onToggle={(event) => {
// this is to avoid event propagation
// when details are nested, which is a bug
// in react. Learn more here:
// https://github.com/facebook/react/issues/22718
event.stopPropagation()
}}
className={clsx(
"border-medusa-border-base dark:border-medusa-border-base-dark border-y border-solid border-x-0",
"overflow-hidden [&>summary]:relative [&>summary]:z-[398]",
props.className
)}
>
{summaryContent && (
<DetailsSummary
onClick={handleToggle}
className="cursor-pointer"
title={summaryContent}
/>
)}
{summaryElm &&
cloneElement(summaryElm as React.ReactElement, {
open,
onClick: handleToggle,
})}
<CSSTransition
unmountOnExit
in={showContent}
timeout={150}
onEnter={(node: HTMLElement) => {
node.classList.add(
"!mb-docs_2",
"!mt-0",
"translate-y-docs_1",
"transition-transform"
)
}}
onExit={(node: HTMLElement) => {
node.classList.add("transition-transform", "!-translate-y-docs_1")
setTimeout(() => {
setOpen(false)
}, 100)
}}
>
<Suspense fallback={<Loading className="!mb-docs_2 !mt-0" />}>
{children}
</Suspense>
</CSSTransition>
</details>
)
}
@@ -0,0 +1,92 @@
"use client"
import React, { useEffect, useState } from "react"
import { request } from "@octokit/request"
import { Link } from "@/components/Link"
export type SolutionsProps = {
feedback: boolean
message?: string
}
export type GitHubSearchItem = {
url: string
html_url: string
title: string
[key: string]: unknown
}
export const Solutions = ({ feedback, message }: SolutionsProps) => {
const [possibleSolutionsQuery, setPossibleSolutionsQuery] =
useState<string>("")
const [possibleSolutions, setPossibleSolutions] = useState<
GitHubSearchItem[]
>([])
function constructQuery(searchQuery: string) {
return `${searchQuery} repo:medusajs/medusa is:closed is:issue`
}
async function searchGitHub(query: string) {
return request(`GET /search/issues`, {
q: query,
sort: "updated",
per_page: 3,
})
}
useEffect(() => {
if (!feedback) {
let query = constructQuery(
// Github does not allow queries longer than 256 characters
message ? message.substring(0, 256) : document.title
)
searchGitHub(query)
.then(async (result) => {
if (!result.data.items.length && message) {
query = constructQuery(document.title)
result = await searchGitHub(query)
}
setPossibleSolutionsQuery(query)
setPossibleSolutions(result.data.items)
})
.catch((err) => console.error(err))
} else {
setPossibleSolutionsQuery("")
setPossibleSolutions([])
}
}, [feedback, message])
return (
<>
{possibleSolutions.length > 0 && (
<div className="text-compact-large-plus font-normal">
<span className="my-docs_1 mx-0 inline-block">
If you faced a problem, here are some possible solutions from
GitHub:
</span>
<ul>
{possibleSolutions.map((solution) => (
<li key={solution.url} className="mb-docs_0.5 last:mb-0">
<Link href={solution.html_url} target="_blank" rel="noreferrer">
{solution.title}
</Link>
</li>
))}
</ul>
<span>
Explore more issues in{" "}
<a
href={`https://github.com/medusajs/medusa/issues?q=${possibleSolutionsQuery}`}
target="_blank"
rel="noreferrer"
>
the GitHub repository
</a>
</span>
</div>
)}
</>
)
}
@@ -0,0 +1,264 @@
"use client"
import React, { useRef, useState } from "react"
import { CSSTransition, SwitchTransition } from "react-transition-group"
import { Solutions } from "./Solutions"
import { ExtraData, useAnalytics } from "@/providers/Analytics"
import clsx from "clsx"
import { TextArea } from "@/components/TextArea"
import { Label } from "@/components/Label"
import { Button } from "docs-ui"
import { Details } from "@/components/Details"
import { InputText } from "@/components/Input/Text"
export type FeedbackProps = {
event: string
pathName: string
reportLink?: string
question?: string
positiveBtn?: string
negativeBtn?: string
positiveQuestion?: string
negativeQuestion?: string
submitBtn?: string
submitMessage?: string
showPossibleSolutions?: boolean
className?: string
extraData?: ExtraData
vertical?: boolean
showLongForm?: boolean
} & React.HTMLAttributes<HTMLDivElement>
export const Feedback = ({
event,
pathName,
reportLink,
question = "Was this section helpful?",
positiveBtn = "Yes",
negativeBtn = "No",
positiveQuestion = "What was most helpful?",
negativeQuestion = "What can we improve?",
submitBtn = "Submit",
submitMessage = "Thank you for helping improve our documentation!",
showPossibleSolutions = true,
className = "",
extraData = {},
vertical = false,
showLongForm = false,
}: FeedbackProps) => {
const [showForm, setShowForm] = useState(false)
const [submittedFeedback, setSubmittedFeedback] = useState(false)
const [loading, setLoading] = useState(false)
const inlineFeedbackRef = useRef<HTMLDivElement>(null)
const inlineQuestionRef = useRef<HTMLDivElement>(null)
const inlineMessageRef = useRef<HTMLDivElement>(null)
const [positiveFeedback, setPositiveFeedback] = useState(false)
const [message, setMessage] = useState("")
const [steps, setSteps] = useState("")
const [medusaVersion, setMedusaVersion] = useState("")
const [errorFix, setErrorFix] = useState("")
const [contactInfo, setContactInfo] = useState("")
const nodeRef: React.RefObject<HTMLDivElement> = submittedFeedback
? inlineMessageRef
: showForm
? inlineQuestionRef
: inlineFeedbackRef
const { loaded, track } = useAnalytics()
function handleFeedback(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
if (!loaded) {
return
}
const feedback = (e.target as Element).classList.contains("positive")
setPositiveFeedback(feedback)
setShowForm(true)
submitFeedback(e, feedback)
}
function submitFeedback(
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
feedback = false
) {
if (showForm) {
setLoading(true)
}
track(
event,
{
url: pathName,
label: document.title,
feedback:
(feedback !== null && feedback) ||
(feedback === null && positiveFeedback)
? "yes"
: "no",
message: message?.length ? message : null,
os: window.navigator.userAgent,
...extraData,
},
function () {
if (showForm) {
setLoading(false)
resetForm()
}
}
)
}
function resetForm() {
setShowForm(false)
setSubmittedFeedback(true)
}
return (
<div className={clsx("mt-docs_3", className)}>
<SwitchTransition mode="out-in">
<CSSTransition
key={
showForm
? "show_form"
: !submittedFeedback
? "feedback"
: "submitted_feedback"
}
nodeRef={nodeRef}
timeout={300}
addEndListener={(done) => {
nodeRef.current?.addEventListener("transitionend", done, false)
}}
classNames={{
enter: "animate-fadeIn animation-fill-forwards animate-fast",
exit: "animate-fadeOut animation-fill-forwards animate-fast",
}}
>
<>
{!showForm && !submittedFeedback && (
<div
className={clsx(
"flex",
!vertical && "flex-row items-center",
vertical && "flex-col justify-center gap-docs_1"
)}
ref={inlineFeedbackRef}
>
<Label className="mr-docs_1.5">{question}</Label>
<div
className={clsx("flex flex-row items-center gap-docs_0.5")}
>
<Button
onClick={handleFeedback}
className="positive w-fit"
variant="secondary"
>
{positiveBtn}
</Button>
<Button
onClick={handleFeedback}
className="w-fit"
variant="secondary"
>
{negativeBtn}
</Button>
{reportLink && (
<Button variant="secondary">
<a href={reportLink}>Report Issue</a>
</Button>
)}
</div>
</div>
)}
{showForm && !submittedFeedback && (
<div className="flex flex-col gap-docs_1" ref={inlineQuestionRef}>
<Label>
{positiveFeedback ? positiveQuestion : negativeQuestion}
</Label>
<TextArea
rows={4}
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
{showLongForm && !positiveFeedback && (
<Details summaryContent="More Details" className="mt-docs_1">
<div className="flex flex-col gap-docs_0.5">
<div className="flex flex-col gap-docs_0.5">
<Label>
Can you provide the exact steps you took before
receiving the error? For example, the commands you
ran.
</Label>
<TextArea
rows={4}
value={steps}
onChange={(e) => setSteps(e.target.value)}
placeholder="1. I ran npm dev..."
/>
</div>
<div className="flex flex-col gap-docs_0.5">
<Label>
If applicable, what version of Medusa are you using?
If a plugin is related to the error, please provide a
version of that as well.
</Label>
<TextArea
rows={4}
value={medusaVersion}
onChange={(e) => setMedusaVersion(e.target.value)}
placeholder="@medusajs/medusa: vX"
/>
</div>
<div className="flex flex-col gap-docs_0.5">
<Label>
Were you able to fix the error? If so, what steps did
you follow?
</Label>
<TextArea
rows={4}
value={errorFix}
onChange={(e) => setErrorFix(e.target.value)}
placeholder="@medusajs/medusa: vX"
/>
</div>
<div className="flex flex-col gap-docs_0.5">
<Label>
Can you provide your email or discord username? This
would allow us to contact you for further info or
assist you with your issue.
</Label>
<InputText
value={contactInfo}
onChange={(e) => setContactInfo(e.target.value)}
placeholder="user@example.com"
/>
</div>
</div>
</Details>
)}
<Button
onClick={submitFeedback}
disabled={loading}
className="w-fit"
variant="secondary"
>
{submitBtn}
</Button>
</div>
)}
{submittedFeedback && (
<div>
<div
className="text-compact-large-plus flex flex-col"
ref={inlineMessageRef}
>
<span>{submitMessage}</span>
{showPossibleSolutions && (
<Solutions message={message} feedback={positiveFeedback} />
)}
</div>
</div>
)}
</>
</CSSTransition>
</SwitchTransition>
</div>
)
}
@@ -0,0 +1,31 @@
"use client"
import React from "react"
import clsx from "clsx"
import { CopyButton } from "@/components"
export type InlineCodeProps = React.ComponentProps<"code">
export const InlineCode = (props: InlineCodeProps) => {
return (
<CopyButton
text={props.children as string}
buttonClassName={clsx(
"bg-transparent border-0 p-0 inline text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"active:[&>code]:bg-medusa-bg-subtle-pressed dark:active:[&>code]:bg-medusa-bg-subtle-pressed-dark",
"focus:[&>code]:bg-medusa-bg-subtle-pressed dark:focus:[&>code]:bg-medusa-bg-subtle-pressed-dark",
"hover:[&>code]:bg-medusa-bg-subtle-hover dark:hover:[&>code]:bg-medusa-bg-base-hover-dark"
)}
>
<code
{...props}
className={clsx(
"border-medusa-tag-neutral-border dark:border-medusa-tag-neutral-border-dark border",
"text-medusa-tag-neutral-text dark:text-medusa-tag-neutral-text-dark",
"bg-medusa-tag-neutral-bg dark:bg-medusa-tag-neutral-bg-dark font-monospace text-code-label rounded-docs_sm py-0 px-[6px]",
props.className
)}
/>
</CopyButton>
)
}
@@ -0,0 +1,31 @@
import React from "react"
import clsx from "clsx"
export type InputTextProps = {
className?: string
} & React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
>
export const InputText = (props: InputTextProps) => {
return (
<input
{...props}
className={clsx(
"bg-medusa-bg-field dark:bg-medusa-bg-field-dark shadow-button-secondary dark:shadow-button-secondary-dark",
"border-medusa-border-base dark:border-medusa-border-base-dark rounded-docs_sm border border-solid",
"px-docs_0.75 py-[9px]",
"hover:bg-medusa-bg-field-hover dark:hover:bg-medusa-bg-field-hover-dark",
"focus:border-medusa-border-interactive dark:focus:border-medusa-border-interactive-dark",
"active:border-medusa-border-interactive dark:active:border-medusa-border-interactive-dark",
"disabled:bg-medusa-bg-disabled dark:disabled:bg-medusa-bg-disabled-dark",
"disabled:border-medusa-border-base dark:disabled:border-medusa-border-base-dark",
"placeholder:text-medusa-fg-muted dark:placeholder:text-medusa-fg-muted-dark",
"disabled:placeholder:text-medusa-fg-disabled dark:disabled:placeholder:text-medusa-fg-disabled-dark",
"text-compact-medium font-base",
props.className
)}
/>
)
}
@@ -0,0 +1,23 @@
import React from "react"
import clsx from "clsx"
export type KbdProps = React.ComponentProps<"kbd">
export const Kbd = ({ children, className, ...props }: KbdProps) => {
return (
<kbd
className={clsx(
"rounded-docs_sm border-solid py-0 px-[6px]",
"inline-flex items-center justify-center",
"border-medusa-tag-neutral-border dark:border-medusa-tag-neutral-border-dark border",
"bg-medusa-tag-neutral-bg dark:bg-medusa-tag-neutral-bg-dark",
"text-medusa-tag-neutral-text dark:text-medusa-tag-neutral-text-dark",
"text-compact-x-small-plus font-monospace shadow-none",
className
)}
{...props}
>
{children}
</kbd>
)
}
@@ -0,0 +1,19 @@
import clsx from "clsx"
import React from "react"
export type LabelProps = {
className?: string
} & React.HTMLAttributes<HTMLSpanElement>
export const Label = ({ children, className }: LabelProps) => {
return (
<span
className={clsx(
"text-medusa-fg-base dark:text-medusa-fg-base-dark text-compact-medium-plus",
className
)}
>
{children}
</span>
)
}
@@ -0,0 +1,24 @@
import React from "react"
import clsx from "clsx"
export type LinkProps = {
href?: string
children?: React.ReactNode
className?: string
} & React.AllHTMLAttributes<HTMLAnchorElement>
export const Link = ({ href, children, className, ...rest }: LinkProps) => {
return (
<a
href={href || ""}
{...rest}
className={clsx(
"text-medusa-fg-interactive hover:text-medusa-fg-interactive-hover",
"dark:text-medusa-fg-interactive-dark dark:hover:text-medusa-fg-interactive-hover-dark",
className
)}
>
{children}
</a>
)
}
@@ -0,0 +1,19 @@
import React from "react"
import clsx from "clsx"
import { Spinner } from "@medusajs/icons"
import { IconProps } from "@medusajs/icons/dist/types"
export type SpinnerLoadingProps = {
iconProps?: IconProps
}
export const SpinnerLoading = ({ iconProps }: SpinnerLoadingProps) => {
return (
<span role="status">
<Spinner
{...iconProps}
className={clsx("animate-spin", iconProps?.className)}
/>
</span>
)
}
@@ -0,0 +1,43 @@
import React from "react"
import clsx from "clsx"
export type LoadingProps = {
className?: string
barClassName?: string
count?: number
}
export const Loading = ({
className,
count = 6,
barClassName,
}: LoadingProps) => {
const getLoadingBars = () => {
const bars = []
for (let i = 0; i < count; i++) {
bars.push(
<span
className={clsx(
"bg-medusa-bg-subtle-pressed dark:bg-medusa-bg-subtle-pressed-dark h-docs_1 w-full rounded-full",
barClassName
)}
key={i}
></span>
)
}
return bars
}
return (
<span
role="status"
className={clsx(
"my-docs_1 flex w-full animate-pulse flex-col gap-docs_1",
className
)}
>
{getLoadingBars()}
<span className="sr-only">Loading...</span>
</span>
)
}
@@ -0,0 +1,31 @@
import React from "react"
import clsx from "clsx"
import { Button, ButtonProps } from "@/components"
export type ModalFooterProps = {
actions?: ButtonProps[]
children?: React.ReactNode
className?: string
}
export const ModalFooter = ({
actions,
children,
className,
}: ModalFooterProps) => {
return (
<div
className={clsx(
"py-docs_1.5 pl-0 pr-docs_2",
"border-medusa-border-base dark:border-medusa-border-base-dark border-0 border-t border-solid",
"flex justify-end gap-docs_0.5",
className
)}
>
{actions?.map((action, index) => (
<Button {...action} key={index} />
))}
{children}
</div>
)
}
@@ -0,0 +1,37 @@
import React from "react"
import clsx from "clsx"
import { useModal } from "@/providers"
import { Button } from "@/components"
import { XMark } from "@medusajs/icons"
export type ModalHeaderProps = {
title?: string
}
export const ModalHeader = ({ title }: ModalHeaderProps) => {
const { closeModal } = useModal()
return (
<div
className={clsx(
"border-medusa-border-base dark:border-medusa-border-base-dark border-0 border-b border-solid py-docs_1.5 px-docs_2",
"flex items-center justify-between"
)}
>
<span
className={clsx(
"text-medusa-fg-base dark:text-medusa-fg-base-dark text-h2"
)}
>
{title}
</span>
<Button
variant="clear"
className="cursor-pointer"
onClick={() => closeModal()}
>
<XMark />
</Button>
</div>
)
}
@@ -0,0 +1,122 @@
"use client"
import clsx from "clsx"
import React, { useCallback, useEffect, useRef } from "react"
import { useModal } from "@/providers"
import { ModalHeader } from "./Header"
import { ModalFooter } from "./Footer"
import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut"
import { ButtonProps } from "@/components"
import { Ref } from "@/types"
export type ModalProps = {
className?: string
title?: string
actions?: ButtonProps[]
modalContainerClassName?: string
contentClassName?: string
onClose?: React.ReactEventHandler<HTMLDialogElement>
open?: boolean
footerContent?: React.ReactNode
passedRef?: Ref<HTMLDialogElement>
} & Omit<React.ComponentProps<"dialog">, "ref">
export const Modal = ({
className,
title,
actions,
children,
contentClassName,
modalContainerClassName,
onClose,
open = true,
footerContent,
passedRef,
...props
}: ModalProps) => {
const { closeModal } = useModal()
const ref = useRef<HTMLDialogElement | null>(null)
const setRefs = useCallback(
(node: HTMLDialogElement) => {
// Ref's from useRef needs to have the node assigned to `current`
ref.current = node
if (typeof passedRef === "function") {
passedRef(node)
} else if (passedRef && "current" in passedRef) {
passedRef.current = node
}
},
[passedRef]
)
useKeyboardShortcut({
metakey: false,
checkEditing: false,
shortcutKeys: ["escape"],
action: () => {
if (open) {
ref.current?.close()
}
},
})
const handleClick = (e: React.MouseEvent<HTMLDialogElement, MouseEvent>) => {
// close modal when the user clicks outside the content
if (e.target === ref.current) {
closeModal()
onClose?.(e)
}
}
const handleClose = (e: React.SyntheticEvent<HTMLDialogElement, Event>) => {
onClose?.(e)
closeModal()
}
useEffect(() => {
if (open) {
document.body.setAttribute("data-modal", "opened")
} else {
document.body.removeAttribute("data-modal")
}
}, [open])
return (
<dialog
{...props}
className={clsx(
"fixed top-0 left-0 flex h-screen w-screen items-center justify-center",
"bg-medusa-bg-overlay dark:bg-medusa-bg-overlay-dark z-[500]",
"hidden open:flex border-0 p-0",
className
)}
onClick={handleClick}
ref={setRefs}
onClose={handleClose}
open={open}
>
<div
className={clsx(
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark rounded-docs_sm",
"border-medusa-border-base dark:border-medusa-border-base-dark border border-solid",
"shadow-modal dark:shadow-modal-dark",
"w-[90%] md:h-auto md:w-[75%] lg:w-[560px]",
modalContainerClassName
)}
>
{title && <ModalHeader title={title} />}
<div
className={clsx(
"overflow-auto py-docs_1.5 px-docs_2",
contentClassName
)}
>
{children}
</div>
{actions && actions?.length > 0 && <ModalFooter actions={actions} />}
{footerContent && <ModalFooter>{footerContent}</ModalFooter>}
</div>
</dialog>
)
}
@@ -0,0 +1,27 @@
"use client"
import React from "react"
import { NavbarIconButton, NavbarIconButtonProps } from "../IconButton"
import { useColorMode } from "@/providers"
import { Moon, Sun } from "@medusajs/icons"
export type NavbarColorModeToggleProps = {
buttonProps?: NavbarIconButtonProps
}
export const NavbarColorModeToggle = ({
buttonProps,
}: NavbarColorModeToggleProps) => {
const { colorMode, toggleColorMode } = useColorMode()
return (
<NavbarIconButton {...buttonProps} onClick={() => toggleColorMode()}>
{colorMode === "light" && (
<Sun className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
)}
{colorMode === "dark" && (
<Moon className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
)}
</NavbarIconButton>
)
}
@@ -0,0 +1,24 @@
import React from "react"
import clsx from "clsx"
import { Button, ButtonProps } from "@/components"
export type NavbarIconButtonProps = ButtonProps
export const NavbarIconButton = ({
children,
className,
...props
}: NavbarIconButtonProps) => {
return (
<Button
className={clsx(
"[&>svg]:h-[22px] [&>svg]:w-[22px] btn-secondary-icon",
className
)}
variant="secondary"
{...props}
>
{children}
</Button>
)
}
@@ -0,0 +1,45 @@
"use client"
import React, { useMemo } from "react"
import clsx from "clsx"
import { useNavbar } from "@/providers"
import { NextLink, NextLinkProps } from "@/components"
export type NavbarLinkProps = {
href: string
label: string
className?: string
activeValuePattern?: RegExp
} & NextLinkProps
export const NavbarLink = ({
href,
label,
className,
activeValuePattern,
}: NavbarLinkProps) => {
const { activeItem } = useNavbar()
const isActive = useMemo(() => {
return activeItem
? activeValuePattern
? activeValuePattern.test(activeItem)
: href === activeItem
: false
}, [activeItem, href, activeValuePattern])
return (
<NextLink
href={href}
className={clsx(
isActive && "!text-medusa-fg-base dark:!text-medusa-fg-base-dark",
!isActive && "!text-medusa-fg-subtle dark:!text-medusa-fg-subtle-dark",
"text-compact-small-plus inline-block",
"hover:!text-medusa-fg-base dark:hover:!text-medusa-fg-base-dark",
className
)}
>
{label}
</NextLink>
)
}
@@ -0,0 +1,34 @@
"use client"
import React from "react"
import { useColorMode } from "@/providers"
import Link from "next/link"
import clsx from "clsx"
export type NavbarLogoProps = {
light: string
dark?: string
className?: string
imageClassName?: string
}
export const NavbarLogo = ({
light,
dark,
className,
imageClassName,
}: NavbarLogoProps) => {
const { colorMode } = useColorMode()
return (
<Link href={`/`} className={clsx("flex-1", className)}>
<img
src={colorMode === "light" ? light : dark || light}
alt="Medusa Logo"
height={20}
width={20}
className={clsx("align-middle", imageClassName)}
/>
</Link>
)
}
@@ -0,0 +1,39 @@
"use client"
import React from "react"
import { NavbarIconButton, NavbarIconButtonProps } from "../../IconButton"
import clsx from "clsx"
import { Sidebar, XMark } from "@medusajs/icons"
export type NavbarMobileMenuButtonProps = {
buttonProps?: NavbarIconButtonProps
mobileSidebarOpen: boolean
setMobileSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>
isLoading?: boolean
}
export const NavbarMobileMenuButton = ({
buttonProps,
mobileSidebarOpen,
setMobileSidebarOpen,
isLoading = false,
}: NavbarMobileMenuButtonProps) => {
return (
<NavbarIconButton
{...buttonProps}
className={clsx("mr-docs_1 lg:!hidden", buttonProps?.className)}
onClick={() => {
if (!isLoading) {
setMobileSidebarOpen((prevValue) => !prevValue)
}
}}
>
{!mobileSidebarOpen && (
<Sidebar className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
)}
{mobileSidebarOpen && (
<XMark className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
)}
</NavbarIconButton>
)
}
@@ -0,0 +1,54 @@
"use client"
import React from "react"
import { NavbarMobileMenuButton, NavbarMobileMenuButtonProps } from "./Button"
import { NavbarColorModeToggle } from "../ColorModeToggle"
import { NavbarSearchModalOpener } from "../SearchModalOpener"
import { useMobile } from "@/providers"
import clsx from "clsx"
import { NavbarLogo, NavbarLogoProps } from "../Logo"
export type NavbarMobileMenuProps = {
menuButton: NavbarMobileMenuButtonProps
logo: NavbarLogoProps
}
export const NavbarMobileMenu = ({
menuButton,
logo,
}: NavbarMobileMenuProps) => {
const { isMobile } = useMobile()
return (
<div className="flex w-full items-center justify-between lg:hidden">
{isMobile && (
<>
<NavbarMobileMenuButton
{...menuButton}
buttonProps={{
...(menuButton.buttonProps || {}),
className: clsx(
menuButton.buttonProps?.className,
"!border-none !bg-transparent !bg-no-image !shadow-none"
),
}}
/>
<NavbarLogo
{...logo}
className="lg:hidden"
imageClassName="mx-auto"
/>
<div className="flex">
<NavbarSearchModalOpener />
<NavbarColorModeToggle
buttonProps={{
className:
"!border-none !bg-transparent !bg-no-image !shadow-none ml-docs_1",
}}
/>
</div>
</>
)}
</div>
)
}
@@ -0,0 +1,17 @@
"use client"
import React from "react"
import { SearchModalOpener } from "@/components"
import { useMobile } from "@/providers"
export type NavbarSearchModalOpenerProps = {
isLoading?: boolean
}
export const NavbarSearchModalOpener = ({
isLoading,
}: NavbarSearchModalOpenerProps) => {
const { isMobile } = useMobile()
return <SearchModalOpener isMobile={isMobile} isLoading={isLoading} />
}
@@ -0,0 +1,67 @@
import React from "react"
import clsx from "clsx"
import { NavbarLink, NavbarLinkProps } from "./Link"
import { NavbarColorModeToggle } from "./ColorModeToggle"
import { NavbarLogo, NavbarLogoProps } from "./Logo"
import { NavbarMobileMenu } from "./MobileMenu"
import { NavbarSearchModalOpener } from "./SearchModalOpener"
import { NavbarMobileMenuButtonProps } from "./MobileMenu/Button"
export type NavbarProps = {
logo: NavbarLogoProps
items: NavbarLinkProps[]
showSearchOpener?: boolean
showColorModeToggle?: boolean
additionalActions?: React.ReactNode
mobileMenuButton: NavbarMobileMenuButtonProps
isLoading?: boolean
className?: string
}
export const Navbar = ({
logo,
items,
showSearchOpener = true,
showColorModeToggle = true,
additionalActions,
mobileMenuButton,
isLoading,
className,
}: NavbarProps) => {
return (
<nav
className={clsx(
"h-navbar sticky top-0 w-full justify-between",
"bg-docs-bg dark:bg-docs-bg-dark border-medusa-border-base dark:border-medusa-border-base-dark z-[400] border-b",
className
)}
>
<div
className={clsx(
"h-navbar max-w-xxl py-docs_0.75 sticky top-0 mx-auto flex w-full justify-between px-docs_1 lg:px-docs_3"
)}
>
<div className="hidden w-full items-center gap-docs_0.5 lg:flex lg:w-auto lg:gap-docs_1.5">
<NavbarLogo {...logo} />
{items.map((item, index) => (
<NavbarLink key={index} {...item} />
))}
</div>
<div className="hidden min-w-0 flex-1 items-center justify-end gap-docs_0.5 lg:flex">
{showSearchOpener && (
<NavbarSearchModalOpener isLoading={isLoading} />
)}
{showColorModeToggle && <NavbarColorModeToggle />}
{additionalActions}
</div>
<NavbarMobileMenu
logo={logo}
menuButton={{
...mobileMenuButton,
isLoading,
}}
/>
</div>
</nav>
)
}
@@ -0,0 +1,27 @@
import React from "react"
import NextLink from "next/link"
import type { LinkProps as NextLinkProps } from "next/link"
import clsx from "clsx"
export type LinkProps = {
href?: string
children?: React.ReactNode
className?: string
} & Partial<NextLinkProps> &
React.AllHTMLAttributes<HTMLAnchorElement>
export const Link = ({ href, children, className, ...rest }: LinkProps) => {
return (
<NextLink
href={href || ""}
{...rest}
className={clsx(
"text-medusa-fg-interactive hover:text-medusa-fg-interactive-hover",
"dark:text-medusa-fg-interactive-dark dark:hover:text-medusa-fg-interactive-hover-dark",
className
)}
>
{children}
</NextLink>
)
}
@@ -0,0 +1,89 @@
import React from "react"
import { NotificationItemProps } from "../.."
import clsx from "clsx"
import {
CheckCircleSolid,
ExclamationCircleSolid,
InformationCircleSolid,
XCircleSolid,
} from "@medusajs/icons"
import { Button } from "@/components"
export type NotificationItemLayoutDefaultProps = NotificationItemProps & {
handleClose: () => void
}
export const NotificationItemLayoutDefault: React.FC<
NotificationItemLayoutDefaultProps
> = ({
type = "info",
title = "",
text = "",
children,
isClosable = true,
handleClose,
CustomIcon,
}) => {
return (
<>
<div className={clsx("flex gap-docs_1 p-docs_1")}>
{type !== "none" && (
<div
className={clsx(
type !== "custom" && "w-docs_2 flex justify-center items-center"
)}
>
{type === "info" && (
<InformationCircleSolid className="text-medusa-fg-interactive-dark" />
)}
{type === "error" && (
<XCircleSolid className="text-medusa-tag-red-icon dark:text-medusa-tag-red-icon-dark" />
)}
{type === "warning" && (
<ExclamationCircleSolid className="text-medusa-tag-orange-icon dark:text-medusa-tag-orange-icon-dark" />
)}
{type === "success" && (
<CheckCircleSolid className="text-medusa-tag-green-icon dark:text-medusa-tag-green-icon-dark" />
)}
{type === "custom" && CustomIcon}
</div>
)}
<span
className={clsx(
"text-compact-medium-plus",
"text-medusa-fg-base dark:text-medusa-fg-base-dark"
)}
>
{title}
</span>
</div>
{(text || children) && (
<div
className={clsx(
"flex pt-0 pr-docs_1 pb-docs_1.5 pl-docs_1 gap-docs_1",
"border-0 border-b border-solid border-medusa-border-base dark:border-medusa-border-base-dark"
)}
>
<div className="w-docs_2 flex-none"></div>
<div className={clsx("flex flex-col", children && "gap-docs_1")}>
{text && (
<span
className={clsx(
"text-medium text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark"
)}
>
{text}
</span>
)}
{children}
</div>
</div>
)}
{isClosable && (
<div className={clsx("p-docs_1 flex justify-end items-center")}>
<Button onClick={handleClose}>Close</Button>
</div>
)}
</>
)
}
@@ -0,0 +1,63 @@
import clsx from "clsx"
import React, { Children, ReactElement } from "react"
import { NotificationItemLayoutDefault } from "./Layout/Default"
export type NotificationItemProps = {
layout?: "default" | "empty"
type?: "info" | "error" | "warning" | "success" | "custom" | "none"
CustomIcon?: React.ReactNode
title?: string
text?: string
className?: string
children?: ReactElement
isClosable?: boolean
placement?: "top" | "bottom"
show?: boolean
setShow?: (value: boolean) => void
onClose?: () => void
} & React.HTMLAttributes<HTMLDivElement>
export const NotificationItem = ({
className = "",
placement = "bottom",
show = true,
layout = "default",
setShow,
onClose,
children,
...rest
}: NotificationItemProps) => {
const handleClose = () => {
setShow?.(false)
onClose?.()
}
return (
<div
className={clsx(
"md:max-w-[320px] md:w-[320px] w-full bg-medusa-bg-base dark:bg-medusa-bg-base-dark rounded-docs_DEFAULT",
"shadow-flyout dark:shadow-flyout-dark max-h-[calc(100vh-90px)]",
"fixed md:right-docs_1 left-0 z-[400] md:m-docs_1",
placement === "bottom" && "md:bottom-docs_1 bottom-0",
placement === "top" && "md:top-docs_1 top-0",
"opacity-100 transition-opacity duration-200 ease-ease",
!show && "!opacity-0",
className
)}
>
{layout === "default" && (
<NotificationItemLayoutDefault {...rest} handleClose={handleClose}>
{children}
</NotificationItemLayoutDefault>
)}
{layout === "empty" &&
Children.map(children, (child) => {
if (child) {
return React.cloneElement(child, {
onClose: handleClose,
})
}
})}
</div>
)
}
@@ -0,0 +1,58 @@
import { NotificationItemType, useNotifications } from "@/providers"
import React from "react"
import { NotificationItem } from "./Item"
import { CSSTransition, TransitionGroup } from "react-transition-group"
import clsx from "clsx"
export const NotificationContainer = () => {
const { notifications, removeNotification } = useNotifications()
const handleClose = (notification: NotificationItemType) => {
notification.onClose?.()
if (notification.id) {
removeNotification(notification.id)
}
}
const renderFilteredNotifications = (
condition: (notificaiton: NotificationItemType) => boolean,
className?: string
) => {
return (
<TransitionGroup className={className}>
{notifications.filter(condition).map((notification) => (
<CSSTransition
key={notification.id}
timeout={200}
classNames={{
enter: "animate__animated animate__slideInRight animate__fastest",
exit: "animate__animated animate__slideOutRight animate__fastest",
}}
>
<NotificationItem
{...notification}
onClose={() => handleClose(notification)}
className={clsx(
notification.className,
"!relative !top-0 !bottom-0 !right-0"
)}
/>
</CSSTransition>
))}
</TransitionGroup>
)
}
return (
<>
{renderFilteredNotifications(
(notification) => notification.placement === "top",
"flex fixed flex-col gap-docs_0.5 right-0 top-0 z-[400] md:w-auto w-full max-h-[calc(100vh-57px)] overflow-y-auto"
)}
{renderFilteredNotifications(
(notification) => notification.placement !== "top",
"flex flex-col gap-docs_0.5 fixed right-0 bottom-0 z-[400] md:w-auto w-full max-h-[calc(100vh-57px)] overflow-y-auto"
)}
</>
)
}
@@ -0,0 +1,80 @@
"use client"
import React, { useRef, useState } from "react"
import clsx from "clsx"
import { Star, StarSolid } from "@medusajs/icons"
import { Button } from "@/components"
import { useAnalytics } from "@/providers"
export type RatingProps = {
event?: string
className?: string
onRating?: () => void
} & React.HTMLAttributes<HTMLDivElement>
export const Rating: React.FC<RatingProps> = ({
event = "rating",
className = "",
onRating,
}) => {
const [rating, setRating] = useState(0)
const [hoverRating, setHoverRating] = useState(0)
const starElms = useRef<HTMLElement[]>([])
const starArr = Array.from(Array(5).keys())
const { track } = useAnalytics()
const handleRating = (selectedRating: number) => {
if (rating) {
return
}
setHoverRating(0)
setRating(selectedRating)
for (let i = 0; i < selectedRating; i++) {
starElms.current[i].classList.add("animate-tada")
}
track(
event,
{
rating: selectedRating,
},
() => onRating?.()
)
}
return (
<div className={clsx("flex gap-docs_0.5", className)}>
{starArr.map((i) => {
const isSelected =
(rating !== 0 && rating - 1 >= i) ||
(hoverRating !== 0 && hoverRating - 1 >= i)
return (
<Button
variant="clear"
buttonRef={(element) => {
if (starElms.current.length - 1 < i) {
starElms.current.push(element as HTMLElement)
}
}}
key={i}
onMouseOver={() => {
if (!rating) {
setHoverRating(i + 1)
}
}}
onMouseLeave={() => {
if (!rating) {
setHoverRating(0)
}
}}
onClick={() => handleRating(i + 1)}
>
{!isSelected && <Star />}
{isSelected && (
<StarSolid className="text-medusa-tag-orange-icon dark:text-medusa-tag-orange-icon-dark" />
)}
</Button>
)
})}
</div>
)
}
@@ -0,0 +1,22 @@
"use client"
import React from "react"
import { useInstantSearch } from "react-instantsearch"
export type SearchEmptyQueryBoundaryProps = {
children: React.ReactNode
fallback: React.ReactNode
}
export const SearchEmptyQueryBoundary = ({
children,
fallback,
}: SearchEmptyQueryBoundaryProps) => {
const { indexUiState } = useInstantSearch()
if (!indexUiState.query) {
return <>{fallback}</>
}
return <>{children}</>
}
@@ -0,0 +1,20 @@
import React from "react"
import clsx from "clsx"
export type SearchHitGroupNameProps = {
name: string
}
export const SearchHitGroupName = ({ name }: SearchHitGroupNameProps) => {
return (
<span
className={clsx(
"pb-docs_0.25 flex px-docs_0.5 pt-docs_1",
"text-medusa-fg-muted dark:text-medusa-fg-muted-dark",
"text-compact-x-small-plus"
)}
>
{name}
</span>
)
}
@@ -0,0 +1,215 @@
"use client"
import React, { Fragment, useEffect, useMemo, useState } from "react"
import clsx from "clsx"
import {
Configure,
ConfigureProps,
Index,
Snippet,
useHits,
useInstantSearch,
} from "react-instantsearch"
import { SearchNoResult } from "../NoResults"
import { SearchHitGroupName } from "./GroupName"
import { useSearch } from "@/providers"
import { Link } from "@/components/Link"
export type Hierarchy = "lvl0" | "lvl1" | "lvl2" | "lvl3" | "lvl4" | "lvl5"
export type HitType = {
hierarchy: {
lvl0: string | null
lvl1: string | null
lvl2: string | null
lvl3: string | null
lvl4: string | null
lvl5: string | null
}
_tags: string[]
url: string
type?: "lvl1" | "lvl2" | "lvl3" | "lvl4" | "lvl5" | "content"
content?: string
__position: number
__queryID?: string
objectID: string
}
export type GroupedHitType = {
[k: string]: HitType[]
}
export type SearchHitWrapperProps = {
configureProps: ConfigureProps
indices: string[]
} & Omit<SearchHitsProps, "indexName" | "setNoResults">
export type IndexResults = {
[k: string]: boolean
}
export const SearchHitsWrapper = ({
configureProps,
indices,
...rest
}: SearchHitWrapperProps) => {
const { status } = useInstantSearch()
const [hasNoResults, setHashNoResults] = useState<IndexResults>({
[indices[0]]: false,
[indices[1]]: false,
})
const showNoResults = useMemo(() => {
return Object.values(hasNoResults).every((value) => value === true)
}, [hasNoResults])
const setNoResults = (index: string, value: boolean) => {
setHashNoResults((prev: IndexResults) => ({
...prev,
[index]: value,
}))
}
return (
<div className="h-full overflow-auto">
{status !== "loading" && showNoResults && <SearchNoResult />}
{indices.map((indexName, index) => (
<Index indexName={indexName} key={index}>
<SearchHits
indexName={indexName}
setNoResults={setNoResults}
{...rest}
/>
<Configure {...configureProps} />
</Index>
))}
</div>
)
}
export type SearchHitsProps = {
indexName: string
setNoResults: (index: string, value: boolean) => void
checkInternalPattern?: RegExp
}
export const SearchHits = ({
indexName,
setNoResults,
checkInternalPattern,
}: SearchHitsProps) => {
const { hits } = useHits<HitType>()
const { status } = useInstantSearch()
const { setIsOpen } = useSearch()
// group by lvl0
const grouped = useMemo(() => {
const grouped: GroupedHitType = {}
hits.forEach((hit) => {
if (hit.hierarchy.lvl0) {
if (!grouped[hit.hierarchy.lvl0]) {
grouped[hit.hierarchy.lvl0] = []
}
grouped[hit.hierarchy.lvl0].push(hit)
}
})
return grouped
}, [hits])
useEffect(() => {
if (status !== "loading" && status !== "stalled") {
setNoResults(indexName, hits.length === 0)
}
}, [hits, status])
const getLastAvailableHeirarchy = (item: HitType) => {
return (
Object.keys(item.hierarchy)
.reverse()
.find((key) => item.hierarchy[key as Hierarchy] !== null) || ""
)
}
const checkIfInternal = (url: string): boolean => {
if (!checkInternalPattern) {
return false
}
return checkInternalPattern.test(url)
}
return (
<div className="overflow-auto">
{Object.keys(grouped).map((groupName, index) => (
<Fragment key={index}>
<SearchHitGroupName name={groupName} />
{grouped[groupName].map((item, index) => (
<div
className={clsx(
"gap-docs_0.25 relative flex flex-1 flex-col p-docs_0.5",
"overflow-x-hidden text-ellipsis whitespace-nowrap break-words",
"hover:bg-medusa-bg-base-hover dark:hover:bg-medusa-bg-base-hover-dark",
"focus:bg-medusa-bg-base-hover dark:focus:bg-medusa-bg-base-hover-dark",
"last:mb-docs_1 focus:outline-none"
)}
key={index}
tabIndex={index}
data-hit
onClick={(e) => {
const target = e.target as Element
if (target.tagName.toLowerCase() === "div") {
target.querySelector("a")?.click()
}
}}
>
<span
className={clsx(
"text-compact-small-plus text-medusa-fg-base dark:text-medusa-fg-base-dark",
"max-w-full"
)}
>
<Snippet
attribute={[
"hierarchy",
item.type && item.type !== "content"
? item.type
: item.hierarchy.lvl1
? "lvl1"
: getLastAvailableHeirarchy(item),
]}
hit={item}
/>
</span>
{item.type !== "lvl1" && (
<span className="text-compact-small text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark">
<Snippet
attribute={
item.content
? "content"
: [
"hierarchy",
item.type || getLastAvailableHeirarchy(item),
]
}
hit={item}
/>
</span>
)}
<Link
href={item.url}
className="absolute top-0 left-0 h-full w-full"
target="_self"
onClick={(e) => {
if (checkIfInternal(item.url)) {
e.preventDefault()
window.location.href = item.url
setIsOpen(false)
}
}}
/>
</div>
))}
</Fragment>
))}
</div>
)
}
@@ -0,0 +1,334 @@
"use client"
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { InstantSearch, SearchBox } from "react-instantsearch"
import clsx from "clsx"
import { SearchEmptyQueryBoundary } from "../EmptyQueryBoundary"
import { SearchSuggestions, type SearchSuggestionType } from "../Suggestions"
import { AlgoliaProps, useSearch } from "@/providers"
import { checkArraySameElms } from "@/utils"
import { SearchHitsWrapper } from "../Hits"
import { findNextSibling, findPrevSibling } from "@/utils"
import { Button, Kbd, Modal, SelectBadge } from "@/components"
import { MagnifyingGlass, XMark } from "@medusajs/icons"
import { useKeyboardShortcut, type OptionType } from "@/hooks"
export type SearchModalProps = {
algolia: AlgoliaProps
isLoading?: boolean
suggestions: SearchSuggestionType[]
checkInternalPattern?: RegExp
filterOptions?: OptionType[]
}
export const SearchModal = ({
algolia,
suggestions,
isLoading = false,
checkInternalPattern,
filterOptions = [],
}: SearchModalProps) => {
const modalRef = useRef<HTMLDialogElement | null>(null)
const { isOpen, setIsOpen, defaultFilters, searchClient } = useSearch()
const [filters, setFilters] = useState<string[]>(defaultFilters)
const formattedFilters: string = useMemo(() => {
let formatted = ""
filters.forEach((filter) => {
const split = filter.split("_")
split.forEach((f) => {
if (formatted.length) {
formatted += " OR "
}
formatted += `_tags:${f}`
})
})
return formatted
}, [filters])
const searchBoxRef = useRef<HTMLFormElement>(null)
const focusSearchInput = () =>
searchBoxRef.current?.querySelector("input")?.focus()
useEffect(() => {
if (!checkArraySameElms(defaultFilters, filters)) {
setFilters(defaultFilters)
}
}, [defaultFilters])
useEffect(() => {
if (isOpen && searchBoxRef.current) {
focusSearchInput()
} else if (!isOpen) {
const focusedItem = modalRef.current?.querySelector(
":focus"
) as HTMLElement
if (
focusedItem &&
focusedItem === searchBoxRef.current?.querySelector("input")
) {
// remove focus
focusedItem.blur()
}
}
}, [isOpen])
const handleKeyAction = (e: KeyboardEvent) => {
if (!isOpen) {
return
}
e.preventDefault()
const focusedItem = modalRef.current?.querySelector(":focus") as HTMLElement
if (!focusedItem) {
// focus the first data-hit
const nextItem = modalRef.current?.querySelector(
"[data-hit]"
) as HTMLElement
nextItem?.focus()
return
}
const isHit = focusedItem.hasAttribute("data-hit")
const isInput = focusedItem.tagName.toLowerCase() === "input"
if (!isHit && !isInput) {
// ignore if focused items aren't input/data-hit
return
}
const lowerPressedKey = e.key.toLowerCase()
if (lowerPressedKey === "enter") {
if (isHit) {
// trigger click event of the focused element
focusedItem.click()
}
return
}
if (lowerPressedKey === "arrowup") {
// only hit items has action on arrow up
if (isHit) {
// find if there's a data-hit item before this one
const beforeItem = findPrevSibling(focusedItem, "[data-hit]")
if (!beforeItem) {
// focus the input
focusSearchInput()
} else {
// focus the previous item
beforeItem.focus()
}
}
} else if (lowerPressedKey === "arrowdown") {
// check if item is input or hit
if (isInput) {
// go to the first data-hit item
const nextItem = modalRef.current?.querySelector(
"[data-hit]"
) as HTMLElement
nextItem?.focus()
} else {
// handle go down for hit items
// find if there's a data-hit item after this one
const afterItem = findNextSibling(focusedItem, "[data-hit]")
if (afterItem) {
// focus the next item
afterItem.focus()
}
}
}
}
const shortcutKeys = useMemo(() => ["ArrowUp", "ArrowDown", "Enter"], [])
useKeyboardShortcut({
metakey: false,
shortcutKeys,
action: handleKeyAction,
checkEditing: false,
preventDefault: false,
isLoading,
})
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (!isOpen) {
return
}
// check if shortcut keys were pressed
const lowerPressedKey = e.key.toLowerCase()
const pressedShortcut = [...shortcutKeys, "Escape"].some(
(s) => s.toLowerCase() === lowerPressedKey
)
if (pressedShortcut) {
return
}
const focusedItem = modalRef.current?.querySelector(
":focus"
) as HTMLElement
const searchInput = searchBoxRef.current?.querySelector(
"input"
) as HTMLInputElement
if (searchInput && focusedItem !== searchInput) {
searchInput.focus()
}
},
[shortcutKeys, isOpen]
)
useEffect(() => {
window.addEventListener("keydown", handleKeyDown)
return () => {
window.removeEventListener("keydown", handleKeyDown)
}
}, [handleKeyDown])
return (
<Modal
contentClassName={clsx(
"!p-0 overflow-hidden relative h-full",
"rounded-none md:rounded-docs_lg flex flex-col justify-between"
)}
modalContainerClassName="w-screen h-screen !rounded-none md:!rounded-docs_lg"
open={isOpen}
onClose={() => setIsOpen(false)}
passedRef={modalRef}
>
<InstantSearch
indexName={algolia.mainIndexName}
searchClient={searchClient}
>
<div
className={clsx("bg-medusa-bg-base dark:bg-medusa-bg-base-dark flex")}
>
<SearchBox
classNames={{
root: clsx(
"h-[56px] w-full md:rounded-t-docs_xl relative border-0 border-solid",
"border-b border-medusa-border-base dark:border-medusa-border-base-dark",
"bg-transparent"
),
form: clsx("h-full md:rounded-t-docs_xl bg-transparent"),
input: clsx(
"w-full h-full pl-docs_3 text-medusa-fg-base dark:text-medusa-fg-base-dark",
"placeholder:text-medusa-fg-muted dark:placeholder:text-medusa-fg-muted-dark",
"md:rounded-t-docs_xl text-compact-medium bg-transparent",
"appearance-none search-cancel:hidden border-0 active:outline-none focus:outline-none"
),
submit: clsx("absolute top-[18px] left-docs_1 btn-clear p-0"),
reset: clsx(
"absolute top-docs_0.75 right-docs_1 hover:bg-medusa-bg-base-hover dark:hover:bg-medusa-bg-base-hover-dark",
"p-[5px] md:rounded-docs_DEFAULT btn-clear"
),
loadingIndicator: clsx("absolute top-[18px] right-docs_1"),
}}
submitIconComponent={() => (
<MagnifyingGlass className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
)}
resetIconComponent={() => (
<XMark className="hidden md:block text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark" />
)}
placeholder="Find something..."
autoFocus
formRef={searchBoxRef}
/>
<Button
variant="clear"
className={clsx(
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark block md:hidden",
"border-0 border-solid",
"border-medusa-border-base dark:border-medusa-border-base-dark border-b",
"pr-docs_1"
)}
onClick={() => setIsOpen(false)}
>
<XMark className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
</Button>
</div>
<div className="mx-docs_0.5 h-[calc(100%-120px)] md:h-[332px] md:flex-initial lg:max-h-[332px] lg:min-h-[332px]">
<SearchEmptyQueryBoundary
fallback={<SearchSuggestions suggestions={suggestions} />}
>
<SearchHitsWrapper
configureProps={{
filters: formattedFilters,
attributesToSnippet: [
"content",
"hierarchy.lvl1",
"hierarchy.lvl2",
],
attributesToHighlight: [
"content",
"hierarchy.lvl1",
"hierarchy.lvl2",
],
}}
indices={algolia.indices}
checkInternalPattern={checkInternalPattern}
/>
</SearchEmptyQueryBoundary>
</div>
</InstantSearch>
<div
className={clsx(
"py-docs_0.75 flex items-center justify-between px-docs_1",
"border-0 border-solid",
"border-medusa-border-base dark:border-medusa-border-base-dark border-t",
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark"
)}
>
{filterOptions.length && (
<SelectBadge
multiple
options={filterOptions}
value={filters}
setSelected={(value) =>
setFilters(Array.isArray(value) ? [...value] : [value])
}
addSelected={(value) => setFilters((prev) => [...prev, value])}
removeSelected={(value) =>
setFilters((prev) => prev.filter((v) => v !== value))
}
showClearButton={false}
placeholder="Filters"
handleAddAll={(isAllSelected: boolean) => {
if (isAllSelected) {
setFilters(defaultFilters)
} else {
setFilters(filterOptions.map((option) => option.value))
}
}}
/>
)}
<div className="hidden items-center gap-docs_1 md:flex">
<div className="flex items-center gap-docs_0.5">
<span
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"text-compact-x-small"
)}
>
Navigation
</span>
<span className="gap-docs_0.25 flex">
<Kbd>↑</Kbd>
<Kbd>↓</Kbd>
</span>
</div>
<div className="flex items-center gap-docs_0.5">
<span
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"text-compact-x-small"
)}
>
Open Result
</span>
<Kbd>↵</Kbd>
</div>
</div>
</div>
</Modal>
)
}
@@ -0,0 +1,90 @@
"use client"
import React, { MouseEvent, useMemo } from "react"
import clsx from "clsx"
import { useSearch } from "@/providers"
import { Button, InputText, Kbd } from "@/components"
import { MagnifyingGlass } from "@medusajs/icons"
import { useKeyboardShortcut } from "@/hooks"
export type SearchModalOpenerProps = {
isLoading?: boolean
isMobile?: boolean
}
export const SearchModalOpener = ({
isLoading = false,
isMobile = false,
}: SearchModalOpenerProps) => {
const { setIsOpen } = useSearch()
const isApple = useMemo(() => {
return typeof navigator !== "undefined"
? navigator.userAgent.toLowerCase().indexOf("mac") !== 0
: true
}, [])
useKeyboardShortcut({
shortcutKeys: ["k"],
action: () => setIsOpen((prev) => !prev),
isLoading,
})
const handleOpen = (
e:
| MouseEvent<HTMLDivElement, globalThis.MouseEvent>
| MouseEvent<HTMLInputElement, globalThis.MouseEvent>
| MouseEvent<HTMLButtonElement, globalThis.MouseEvent>
) => {
if (isLoading) {
return
}
e.preventDefault()
if ("blur" in e.target && typeof e.target.blur === "function") {
e.target.blur()
}
setIsOpen(true)
}
return (
<>
{isMobile && (
<Button variant="clear" onClick={handleOpen}>
<MagnifyingGlass className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
</Button>
)}
{!isMobile && (
<div
className={clsx("relative w-min hover:cursor-pointer")}
onClick={handleOpen}
>
<MagnifyingGlass
className={clsx(
"absolute left-docs_0.5 top-[5px]",
"text-medusa-fg-muted dark:text-medusa-fg-muted-dark"
)}
/>
<InputText
type="search"
className={clsx(
"placeholder:text-compact-small",
"!py-[5px] !pl-[36px] !pr-docs_0.5",
"cursor-pointer select-none"
)}
placeholder="Find something"
onClick={handleOpen}
onFocus={(e) => e.target.blur()}
tabIndex={-1}
/>
<span
className={clsx(
"gap-docs_0.25 flex",
"absolute right-docs_0.5 top-[5px]"
)}
>
<Kbd>{isApple ? "⌘" : "Ctrl"}</Kbd>
<Kbd>K</Kbd>
</span>
</div>
)}
</>
)
}
@@ -0,0 +1,19 @@
import { ExclamationCircleSolid } from "@medusajs/icons"
import clsx from "clsx"
import React from "react"
export const SearchNoResult = () => {
return (
<div
className={clsx(
"flex h-full w-full flex-col items-center justify-center gap-docs_1",
"text-medusa-fg-muted dark:text-medusa-fg-muted-dark"
)}
>
<ExclamationCircleSolid />
<span className="text-compact-small">
No results found. Try changing selected filters.
</span>
</div>
)
}
@@ -0,0 +1,56 @@
"use client"
import React from "react"
import clsx from "clsx"
import { useInstantSearch } from "react-instantsearch"
import { SearchHitGroupName } from "../Hits/GroupName"
export type SearchSuggestionType = {
title: string
items: string[]
}
export type SearchSuggestionsProps = {
suggestions: SearchSuggestionType[]
}
export const SearchSuggestions = ({ suggestions }: SearchSuggestionsProps) => {
const { setIndexUiState } = useInstantSearch()
return (
<div className="h-full overflow-auto">
{suggestions.map((suggestion, index) => (
<React.Fragment key={index}>
<SearchHitGroupName name={suggestion.title} />
{suggestion.items.map((item, itemIndex) => (
<div
className={clsx(
"flex items-center justify-between",
"cursor-pointer rounded-docs_sm p-docs_0.5",
"hover:bg-medusa-bg-base-hover dark:hover:bg-medusa-bg-base-hover-dark",
"focus:bg-medusa-bg-base-hover dark:focus:bg-medusa-bg-base-hover-dark",
"focus:outline-none last:mb-docs_1"
)}
onClick={() =>
setIndexUiState({
query: item,
})
}
key={itemIndex}
tabIndex={itemIndex}
data-hit
>
<span
className={clsx(
"text-medusa-fg-base dark:text-medusa-fg-base-dark",
"text-compact-small"
)}
>
{item}
</span>
</div>
))}
</React.Fragment>
))}
</div>
)
}
@@ -0,0 +1,6 @@
export * from "./EmptyQueryBoundary"
export * from "./Hits"
export * from "./Modal"
export * from "./ModalOpener"
export * from "./NoResults"
export * from "./Suggestions"
@@ -0,0 +1,136 @@
"use client"
import React, { useCallback, useRef, useState } from "react"
import { useSelect } from "@/hooks"
import clsx from "clsx"
import { SelectDropdown, SelectProps } from ".."
export const SelectBadge = ({
value,
options,
setSelected,
addSelected,
removeSelected,
multiple,
className,
addAll = multiple,
handleAddAll,
...props
}: SelectProps) => {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
const { isValueSelected, isAllSelected, handleChange, handleSelectAll } =
useSelect({
value,
options,
multiple,
setSelected,
removeSelected,
addSelected,
handleAddAll,
})
const getSelectedText = useCallback(() => {
let str = ""
const selectedOptions = options.filter((option) =>
value.includes(option.value)
)
if (isAllSelected) {
str = "All Areas"
} else {
if (
(!Array.isArray(value) && !value) ||
(Array.isArray(value) && !value.length)
) {
str = "None selected"
} else {
str = selectedOptions[0].label
}
}
return (
<>
<span
className={clsx(
"text-medusa-fg-base dark:text-medusa-fg-base-dark",
"text-compact-x-small-plus",
"inline-block max-w-[60px] overflow-hidden text-ellipsis"
)}
>
{str}
</span>
{!isAllSelected && selectedOptions.length > 1 && (
<span
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"text-compact-x-small"
)}
>
{" "}
+ {selectedOptions.length}
</span>
)}
</>
)
}, [isAllSelected, options, value])
return (
<div className={clsx("relative", className)}>
<div
className={clsx(
"border-medusa-border-base dark:border-medusa-border-base-dark rounded-sm border border-solid",
"hover:bg-medusa-bg-subtle-hover dark:hover:bg-medusa-bg-subtle-hover-dark",
"py-docs_0.25 h-fit cursor-pointer px-docs_0.5",
"flex items-center gap-[6px] whitespace-nowrap",
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
!open && "bg-medusa-bg-subtle dark:bg-medusa-bg-subtle-dark",
open &&
"bg-medusa-bg-subtle-hover dark:bg-medusa-bg-subtle-hover-dark",
className
)}
ref={ref}
onClick={(e) => {
if (!dropdownRef.current?.contains(e.target as Element)) {
setOpen((prev) => !prev)
}
}}
>
<span
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"text-compact-x-small"
)}
>
Show results from:{" "}
</span>
{getSelectedText()}
</div>
<input
type="hidden"
name={props.name}
value={Array.isArray(value) ? value.join(",") : value}
/>
<SelectDropdown
options={options}
open={open}
setOpen={setOpen}
addAll={addAll}
multiple={multiple}
isAllSelected={isAllSelected}
isValueSelected={isValueSelected}
handleSelectAll={handleSelectAll}
handleChange={handleChange}
parentRef={ref}
passedRef={dropdownRef}
className={clsx(
"!top-[unset] !bottom-full",
open && "!-translate-y-docs_0.5"
)}
/>
</div>
)
}
export default SelectBadge
@@ -0,0 +1,148 @@
"use client"
import React, { useCallback, useEffect, useRef } from "react"
import clsx from "clsx"
import { OptionType } from "@/hooks"
import { Ref } from "@/types"
import { CheckMini, EllipseMiniSolid } from "@medusajs/icons"
export type SelectDropdownProps = {
options: OptionType[]
open: boolean
setOpen: React.Dispatch<React.SetStateAction<boolean>>
addAll?: boolean
multiple?: boolean
isAllSelected: boolean
isValueSelected: (val: string) => boolean
handleSelectAll: () => void
handleChange?: (selectedValue: string, wasSelected: boolean) => void
parentRef?: React.RefObject<HTMLDivElement>
className?: string
passedRef?: Ref<HTMLDivElement>
}
export const SelectDropdown = ({
open,
setOpen,
options,
addAll,
multiple = false,
isAllSelected,
isValueSelected,
handleSelectAll,
handleChange: handleSelectChange,
parentRef,
className,
passedRef,
}: SelectDropdownProps) => {
const ref = useRef<HTMLDivElement | null>(null)
const setRefs = useCallback(
(node: HTMLDivElement) => {
// Ref's from useRef needs to have the node assigned to `current`
ref.current = node
if (typeof passedRef === "function") {
passedRef(node)
} else if (passedRef && "current" in passedRef) {
passedRef.current = node
}
},
[passedRef]
)
const handleChange = (clickedValue: string, wasSelected: boolean) => {
handleSelectChange?.(clickedValue, wasSelected)
if (!multiple) {
setOpen(false)
}
}
const handleOutsideClick = useCallback(
(e: MouseEvent) => {
if (
open &&
!ref.current?.contains(e.target as Element) &&
!parentRef?.current?.contains(e.target as Element)
) {
setOpen(false)
}
},
[open, parentRef, setOpen]
)
useEffect(() => {
document.body.addEventListener("click", handleOutsideClick)
return () => {
document.body.removeEventListener("click", handleOutsideClick)
}
}, [handleOutsideClick])
const getSelectOption = (option: OptionType, index: number) => {
const isSelected = option.isAllOption
? isAllSelected
: isValueSelected(option.value)
return (
<li
key={index}
className={clsx(
"pr-docs_0.75 relative rounded-docs_sm py-docs_0.5 pl-docs_2.5",
"hover:bg-medusa-bg-base-hover dark:hover:bg-medusa-bg-base-hover-dark",
"[&>svg]:left-docs_0.75 cursor-pointer [&>svg]:absolute [&>svg]:top-docs_0.5",
!isSelected && "text-compact-small",
isSelected && "text-compact-small-plus"
)}
onClick={() => {
if (option.isAllOption) {
handleSelectAll()
} else {
handleChange(option.value, isSelected)
}
}}
>
{isSelected && (
<>
{multiple && (
<CheckMini className="text-medusa-fg-base dark:text-medusa-fg-base-dark" />
)}
{!multiple && (
<EllipseMiniSolid className="text-medusa-fg-base dark:text-medusa-fg-base-dark" />
)}
</>
)}
{option.label}
</li>
)
}
return (
<div
className={clsx(
"absolute top-full left-0 w-full",
"z-10 h-0 translate-y-0 overflow-hidden transition-transform",
open && "h-auto translate-y-docs_0.5 !overflow-visible",
className
)}
ref={setRefs}
>
<ul
className={clsx(
"p-docs_0.25 mb-0 w-full overflow-auto rounded-docs_DEFAULT",
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark text-medusa-fg-base dark:text-medusa-fg-base-dark",
"shadow-flyout dark:shadow-flyout-dark list-none"
)}
>
{addAll &&
getSelectOption(
{
value: "all",
label: "All Areas",
isAllOption: true,
},
-1
)}
{options.map(getSelectOption)}
</ul>
</div>
)
}
@@ -0,0 +1,125 @@
"use client"
import React, { useRef, useState } from "react"
import clsx from "clsx"
import { useSelect } from "@/hooks"
import { SelectDropdown, SelectProps } from ".."
import { Badge } from "docs-ui"
import { ChevronUpDown, XMarkMini } from "@medusajs/icons"
export const SelectInput = ({
value,
options,
setSelected,
addSelected,
removeSelected,
multiple,
className,
addAll = multiple,
handleAddAll,
showClearButton = true,
...props
}: SelectProps) => {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
const {
isValueSelected,
hasSelectedValue,
hasSelectedValues,
selectedValues,
isAllSelected,
handleChange,
handleSelectAll,
} = useSelect({
value,
options,
multiple,
setSelected,
removeSelected,
addSelected,
handleAddAll,
})
return (
<div
className={clsx(
"px-docs_0.75 relative py-[9px]",
"border-medusa-border-base dark:border-medusa-border-base-dark rounded-docs_sm border border-solid",
"bg-medusa-bg-field dark:bg-medusa-bg-field-dark shadow-button-neutral dark:shadow-button-neutral-dark",
"hover:bg-medusa-bg-field-hover dark:hover:bg-medusa-bg-field-hover-dark",
"active:shadow-active dark:active:shadow-active-dark",
"focus:shadow-active dark:focus:shadow-active-dark",
"text-medusa-fg-base dark:text-medusa-fg-base-dark text-compact-medium",
"disabled:bg-medusa-bg-disabled dark:disabled:bg-medusa-bg-disabled-dark",
"disabled:text-medusa-fg-disabled dark:disabled:text-medusa-fg-disabled-dark",
"flex items-center gap-docs_0.5",
!hasSelectedValues &&
"placeholder:text-medusa-fg-muted dark:placeholder:text-medusa-fg-muted-dark",
hasSelectedValues &&
"placeholder:text-medusa-fg-base dark:placeholder:text-medusa-fg-base-dark",
className
)}
ref={ref}
onClick={(e) => {
if (!dropdownRef.current?.contains(e.target as Element)) {
setOpen((prev) => !prev)
}
}}
>
{hasSelectedValues && (
<Badge
variant="neutral"
className={clsx("flex", showClearButton && "flex-1")}
>
<span
className={clsx(
"text-compact-medium-plus inline-block",
showClearButton && "mr-docs_0.125"
)}
>
{(value as string[]).length}
</span>
{showClearButton && (
<XMarkMini
className="text-medusa-tag-neutral-icon dark:text-medusa-tag-neutral-icon-dark"
onClick={(e) => {
e.stopPropagation()
setSelected?.([])
}}
/>
)}
</Badge>
)}
<span
className={clsx(
"inline-block flex-1 select-none overflow-ellipsis whitespace-nowrap break-words",
hasSelectedValues && "max-w-1/3"
)}
>
{!multiple && hasSelectedValue && selectedValues.length
? selectedValues[0].label
: props.placeholder}
</span>
<ChevronUpDown className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
<input
type="hidden"
name={props.name}
value={Array.isArray(value) ? value.join(",") : value}
/>
<SelectDropdown
options={options}
open={open}
setOpen={setOpen}
addAll={addAll}
multiple={multiple}
isAllSelected={isAllSelected}
isValueSelected={isValueSelected}
handleSelectAll={handleSelectAll}
handleChange={handleChange}
parentRef={ref}
passedRef={dropdownRef}
/>
</div>
)
}
@@ -0,0 +1,13 @@
import type { OptionType, SelectOptions } from "@/hooks"
export type SelectProps = {
options: OptionType[]
multiple?: boolean
addAll?: boolean
showClearButton?: boolean
} & SelectOptions &
React.ComponentProps<"input">
export * from "./Badge"
export * from "./Dropdown"
export * from "./Input"
@@ -0,0 +1,126 @@
"use client"
import React, { useEffect, useMemo, useRef, useState } from "react"
import type { SidebarItemType } from "@/providers"
import { useSidebar } from "@/providers"
import clsx from "clsx"
import Link from "next/link"
import { checkSidebarItemVisibility } from "@/utils"
import { Loading } from "@/components"
export type SidebarItemProps = {
item: SidebarItemType
nested?: boolean
expandItems?: boolean
} & React.AllHTMLAttributes<HTMLLIElement>
export const SidebarItem = ({
item,
nested = false,
expandItems = false,
className,
}: SidebarItemProps) => {
const [showLoading, setShowLoading] = useState(false)
const { isItemActive, setMobileSidebarOpen: setSidebarOpen } = useSidebar()
const active = useMemo(() => {
return isItemActive(item, nested)
}, [isItemActive, item, nested])
const collapsed = !expandItems && !isItemActive(item, true)
const ref = useRef<HTMLLIElement>(null)
useEffect(() => {
if (active && ref.current && window.innerWidth >= 1025) {
if (
!checkSidebarItemVisibility(ref.current, {
topMargin: 57,
})
) {
// scroll to element
ref.current.scrollIntoView({
block: "center",
})
}
}
if (active) {
setShowLoading(true)
}
}, [active])
const classNames = useMemo(
() =>
clsx(
"flex items-center justify-between gap-docs_0.5 rounded-docs_sm px-docs_0.5 py-[6px] hover:no-underline",
!item.children &&
"text-compact-small-plus text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
item.children &&
"text-compact-x-small-plus text-medusa-fg-muted dark:text-medusa-fg-muted-dark uppercase",
item.path !== undefined &&
active && [
"!text-medusa-fg-base bg-medusa-bg-base-pressed",
"dark:!text-medusa-fg-base-dark dark:bg-medusa-bg-base-pressed-dark",
],
item.path !== undefined &&
active &&
"border border-medusa-border-base dark:border-medusa-border-base-dark",
item.path !== undefined &&
!active &&
"hover:bg-medusa-bg-base-hover dark:hover:bg-medusa-bg-base-hover-dark border-transparent"
),
[item.children, active, item.path]
)
return (
<li
className={clsx(
item.children && !collapsed && "my-docs_1.5",
!item.children && !nested && active && "mt-docs_1.5",
!expandItems &&
((item.children && !collapsed) ||
(!item.children && !nested && active)) &&
"-translate-y-docs_1 transition-transform",
className
)}
ref={ref}
>
{item.path === undefined && (
<span className={classNames}>
<span>{item.title}</span>
{item.additionalElms}
</span>
)}
{item.path !== undefined && (
<Link
href={item.isPathHref ? item.path : `#${item.path}`}
className={classNames}
scroll={true}
onClick={() => {
if (window.innerWidth < 1025) {
setSidebarOpen(false)
}
}}
replace
shallow
>
<span>{item.title}</span>
{item.additionalElms}
</Link>
)}
{item.children && (
<ul
className={clsx("ease-ease overflow-hidden", collapsed && "m-0 h-0")}
>
{showLoading && !item.loaded && (
<Loading
count={3}
className="!mb-0 !px-docs_0.5"
barClassName="h-[20px]"
/>
)}
{item.children?.map((childItem, index) => (
<SidebarItem item={childItem} key={index} nested={true} />
))}
</ul>
)}
</li>
)
}
@@ -0,0 +1,63 @@
"use client"
import React from "react"
import { useSidebar } from "@/providers"
import clsx from "clsx"
import { Loading } from "@/components"
import { SidebarItem } from "./Item"
export type SidebarProps = {
className?: string
expandItems?: boolean
}
export const Sidebar = ({
className = "",
expandItems = false,
}: SidebarProps) => {
const { items, mobileSidebarOpen, desktopSidebarOpen } = useSidebar()
return (
<aside
className={clsx(
"clip bg-docs-bg dark:bg-docs-bg-dark w-ref-sidebar block",
"border-medusa-border-base dark:border-medusa-border-base-dark border-0 border-r border-solid",
"fixed -left-full top-[57px] h-screen transition-[left] lg:relative lg:left-0 lg:top-auto lg:h-auto",
"lg:w-sidebar z-[100] w-full lg:z-0",
mobileSidebarOpen && "!left-0",
!desktopSidebarOpen && "!absolute !-left-full",
className
)}
style={{
animationFillMode: "forwards",
}}
>
<ul
className={clsx(
"sticky top-[57px] h-screen max-h-screen w-full list-none overflow-auto p-0",
"px-docs_1.5 pb-[57px] pt-docs_1.5"
)}
id="sidebar"
>
<div className="mb-docs_1.5 lg:hidden">
{!items.mobile.length && <Loading className="px-0" />}
{items.mobile.map((item, index) => (
<SidebarItem item={item} key={index} expandItems={expandItems} />
))}
</div>
<div className="mb-docs_1.5">
{!items.top.length && <Loading className="px-0" />}
{items.top.map((item, index) => (
<SidebarItem item={item} key={index} expandItems={expandItems} />
))}
</div>
<div className="mb-docs_1.5">
{!items.bottom.length && <Loading className="px-0" />}
{items.bottom.map((item, index) => (
<SidebarItem item={item} key={index} expandItems={expandItems} />
))}
</div>
</ul>
</aside>
)
}
@@ -0,0 +1,30 @@
import React from "react"
import clsx from "clsx"
export type TextAreaProps = {
className?: string
} & React.DetailedHTMLProps<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
HTMLTextAreaElement
>
export const TextArea = (props: TextAreaProps) => {
return (
<textarea
{...props}
className={clsx(
"bg-medusa-bg-field dark:bg-medusa-bg-field-dark shadow-button-secondary dark:shadow-button-secondary-dark",
"border-medusa-border-base dark:border-medusa-border-base-dark rounded-docs_sm border border-solid",
"pt-docs_0.4 px-docs_0.75 text-medium font-base pb-[9px]",
"hover:bg-medusa-bg-field-hover dark:hover:bg-medusa-bg-field-hover-dark",
"focus:border-medusa-border-interactive dark:focus:border-medusa-border-interactive-dark",
"active:border-medusa-border-interactive dark:active:border-medusa-border-interactive-dark",
"disabled:bg-medusa-bg-disabled dark:disabled:bg-medusa-bg-disabled-dark",
"disabled:border-medusa-border-base dark:disabled:border-medusa-border-base-dark",
"placeholder:text-medusa-fg-muted dark:placeholder:text-medusa-fg-muted-dark",
"disabled:placeholder:text-medusa-fg-disabled dark:disabled:placeholder:text-medusa-fg-disabled-dark",
props.className
)}
/>
)
}
@@ -0,0 +1,64 @@
"use client"
import React, { useEffect, useState } from "react"
import { Tooltip as ReactTooltip } from "react-tooltip"
import type { ITooltip } from "react-tooltip"
import clsx from "clsx"
import "react-tooltip/dist/react-tooltip.css"
import uuid from "react-uuid"
export type TooltipProps = {
text?: string
tooltipClassName?: string
html?: string
tooltipChildren?: React.ReactNode
} & React.HTMLAttributes<HTMLSpanElement> &
ITooltip
export const Tooltip = ({
text = "",
tooltipClassName = "",
children,
html = "",
tooltipChildren,
...tooltipProps
}: TooltipProps) => {
const [elementId, setElementId] = useState<string>("")
useEffect(() => {
if (!elementId) {
setElementId(uuid())
}
}, [elementId])
return (
<>
<span
id={elementId}
data-tooltip-content={text}
data-tooltip-html={html}
data-tooltip-id={elementId}
>
{children}
</span>
<ReactTooltip
anchorId={elementId}
// anchorSelect={elementId ? `#${elementId}` : undefined}
className={clsx(
"!border-medusa-border-base dark:!border-medusa-border-base-dark !border !border-solid",
"!text-compact-x-small-plus !shadow-tooltip dark:!shadow-tooltip-dark !rounded-docs_DEFAULT",
"!py-docs_0.4 !z-[399] hidden !px-docs_1 lg:block",
"!bg-medusa-bg-base dark:!bg-medusa-bg-base-dark",
"!text-medusa-fg-subtle dark:!text-medusa-fg-subtle-dark",
tooltipClassName
)}
wrapper="span"
noArrow={true}
positionStrategy={"fixed"}
{...tooltipProps}
>
{tooltipChildren}
</ReactTooltip>
</>
)
}
@@ -0,0 +1,42 @@
export * from "./Badge"
export * from "./Bordered"
export * from "./BorderedIcon"
export * from "./Button"
export * from "./Card"
export * from "./CodeBlock"
export * from "./CodeMdx"
export * from "./CodeTabs"
export * from "./CopyButton"
export * from "./Details"
export * from "./Details/Summary"
export * from "./Feedback"
export * from "./Feedback/Solutions"
export * from "./InlineCode"
export * from "./Input/Text"
export * from "./Kbd"
export * from "./Label"
export * from "./Link"
export * from "./Loading"
export * from "./Loading/Spinner"
export * from "./Modal"
export * from "./Modal/Header"
export * from "./Modal/Footer"
export * from "./Navbar"
export * from "./Navbar/ColorModeToggle"
export * from "./Navbar/IconButton"
export * from "./Navbar/Link"
export * from "./Navbar/Logo"
export * from "./Navbar/MobileMenu"
export * from "./Navbar/MobileMenu/Button"
export * from "./Navbar/SearchModalOpener"
export { Link as NextLink, type LinkProps as NextLinkProps } from "./NextLink"
export * from "./Notification"
export * from "./Notification/Item"
export * from "./Notification/Item/Layout/Default"
export * from "./Rating"
export * from "./Search"
export * from "./Select"
export * from "./Sidebar"
export * from "./Sidebar/Item"
export * from "./TextArea"
export * from "./Tooltip"
+1
View File
@@ -0,0 +1 @@
export const GITHUB_ISSUES_PREFIX = `https://github.com/medusajs/medusa/issues/new?assignees=&labels=type%3A+docs&template=docs.yml`
+4
View File
@@ -0,0 +1,4 @@
export * from "./use-keyboard-shortcut"
export * from "./use-scroll-utils"
export * from "./use-select"
export * from "./use-tabs"
@@ -0,0 +1,72 @@
"use client"
import { useCallback, useEffect } from "react"
export type useKeyboardShortcutOptions = {
metakey?: boolean
shortcutKeys: string[]
action: (e: KeyboardEvent) => void
checkEditing?: boolean
preventDefault?: boolean
isLoading?: boolean
}
export const useKeyboardShortcut = ({
metakey = true,
shortcutKeys,
action,
checkEditing = true,
preventDefault = true,
isLoading = false,
}: useKeyboardShortcutOptions) => {
function isEditingContent(event: KeyboardEvent) {
const element = event.target as HTMLElement
const tagName = element.tagName
return (
element.isContentEditable ||
tagName === "INPUT" ||
tagName === "SELECT" ||
tagName === "TEXTAREA"
)
}
const checkKeysPressed = useCallback(
(pressedKey: string) => {
const lowerPressedKey = pressedKey.toLowerCase()
return shortcutKeys.some(
(value) => lowerPressedKey === value.toLowerCase()
)
},
[shortcutKeys]
)
const sidebarShortcut = useCallback(
(e: KeyboardEvent) => {
// the event is triggered when an input
// autocompletes, and in that case
// e.key will be empty
if (isLoading || !e.key) {
return
}
if (
(!metakey || e.metaKey || e.ctrlKey) &&
checkKeysPressed(e.key) &&
(!checkEditing || !isEditingContent(e))
) {
if (preventDefault) {
e.preventDefault()
}
action(e)
}
},
[isLoading, metakey, checkKeysPressed, checkEditing, action, preventDefault]
)
useEffect(() => {
window.addEventListener("keydown", sidebarShortcut)
return () => {
window.removeEventListener("keydown", sidebarShortcut)
}
}, [sidebarShortcut])
}
@@ -0,0 +1,331 @@
"use client"
/* Copied from Docusaurus to maintain scroll position on re-renders. Useful when content of the page is updated dynamically */
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {
useCallback,
useContext,
useEffect,
useLayoutEffect,
useMemo,
useRef,
type ReactNode,
} from "react"
type EventFunc = (...args: never[]) => unknown
export function useEvent<T extends EventFunc>(callback: T): T {
const ref = useRef<T>(callback)
useLayoutEffect(() => {
ref.current = callback
}, [callback])
// @ts-expect-error: TS is right that this callback may be a supertype of T,
// but good enough for our use
return useCallback<T>((...args) => ref.current(...args), [])
}
/**
* Gets `value` from the last render.
*/
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>()
useLayoutEffect(() => {
ref.current = value
})
return ref.current
}
type ScrollController = {
/** A boolean ref tracking whether scroll events are enabled. */
scrollEventsEnabledRef: React.MutableRefObject<boolean>
/** Enable scroll events in `useScrollPosition`. */
enableScrollEvents: () => void
/** Disable scroll events in `useScrollPosition`. */
disableScrollEvents: () => void
}
function useScrollControllerContextValue(): ScrollController {
const scrollEventsEnabledRef = useRef(true)
return useMemo(
() => ({
scrollEventsEnabledRef,
enableScrollEvents: () => {
scrollEventsEnabledRef.current = true
},
disableScrollEvents: () => {
scrollEventsEnabledRef.current = false
},
}),
[]
)
}
const ScrollMonitorContext = React.createContext<ScrollController | undefined>(
undefined
)
export function ScrollControllerProvider({
children,
}: {
children: ReactNode
}): JSX.Element {
const value = useScrollControllerContextValue()
return (
<ScrollMonitorContext.Provider value={value}>
{children}
</ScrollMonitorContext.Provider>
)
}
/**
* We need a way to update the scroll position while ignoring scroll events
* so as not to toggle Navbar/BackToTop visibility.
*
* This API permits to temporarily disable/ignore scroll events. Motivated by
* https://github.com/facebook/docusaurus/pull/5618
*/
export function useScrollController(): ScrollController {
const context = useContext(ScrollMonitorContext)
if (context == null) {
throw new Error(
`useScrollController must be used by elements in ScrollControllerProvider`
)
}
return context
}
type ScrollPosition = { scrollX: number; scrollY: number }
const getScrollPosition = (): ScrollPosition | null => ({
scrollX: window.pageXOffset,
scrollY: window.pageYOffset,
})
/**
* This hook fires an effect when the scroll position changes. The effect will
* be provided with the before/after scroll positions. Note that the effect may
* not be always run: if scrolling is disabled through `useScrollController`, it
* will be a no-op.
*
* @see {@link useScrollController}
*/
export function useScrollPosition(
effect: (
position: ScrollPosition,
lastPosition: ScrollPosition | null
) => void,
deps: unknown[] = []
): void {
const { scrollEventsEnabledRef } = useScrollController()
const lastPositionRef = useRef<ScrollPosition | null>(getScrollPosition())
const dynamicEffect = useEvent(effect)
useEffect(() => {
const handleScroll = () => {
if (!scrollEventsEnabledRef.current) {
return
}
const currentPosition = getScrollPosition()!
dynamicEffect(currentPosition, lastPositionRef.current)
lastPositionRef.current = currentPosition
}
const opts: AddEventListenerOptions & EventListenerOptions = {
passive: true,
}
handleScroll()
window.addEventListener("scroll", handleScroll, opts)
return () => window.removeEventListener("scroll", handleScroll, opts)
}, [dynamicEffect, scrollEventsEnabledRef, ...deps])
}
type UseScrollPositionSaver = {
/** Measure the top of an element, and store the details. */
save: (elem: HTMLElement) => void
/**
* Restore the page position to keep the stored element's position from
* the top of the viewport, and remove the stored details.
*/
restore: () => { restored: boolean }
}
function useScrollPositionSaver(): UseScrollPositionSaver {
const lastElementRef = useRef<{ elem: HTMLElement | null; top: number }>({
elem: null,
top: 0,
})
const save = useCallback((elem: HTMLElement) => {
lastElementRef.current = {
elem,
top: elem.getBoundingClientRect().top,
}
}, [])
const restore = useCallback(() => {
const {
current: { elem, top },
} = lastElementRef
if (!elem) {
return { restored: false }
}
const newTop = elem.getBoundingClientRect().top
const heightDiff = newTop - top
if (heightDiff) {
window.scrollBy({ left: 0, top: heightDiff })
}
lastElementRef.current = { elem: null, top: 0 }
return { restored: heightDiff !== 0 }
}, [])
return useMemo(() => ({ save, restore }), [restore, save])
}
/**
* This hook permits to "block" the scroll position of a DOM element.
* The idea is that we should be able to update DOM content above this element
* but the screen position of this element should not change.
*
* Feature motivated by the Tabs groups: clicking on a tab may affect tabs of
* the same group upper in the tree, yet to avoid a bad UX, the clicked tab must
* remain under the user mouse.
*
* @see https://github.com/facebook/docusaurus/pull/5618
*/
export function useScrollPositionBlocker(): {
/**
* Takes an element, and keeps its screen position no matter what's getting
* rendered above it, until the next render.
*/
blockElementScrollPositionUntilNextRender: (el: HTMLElement) => void
} {
const scrollController = useScrollController()
const scrollPositionSaver = useScrollPositionSaver()
const nextLayoutEffectCallbackRef = useRef<(() => void) | undefined>(
undefined
)
const blockElementScrollPositionUntilNextRender = useCallback(
(el: HTMLElement) => {
scrollPositionSaver.save(el)
scrollController.disableScrollEvents()
nextLayoutEffectCallbackRef.current = () => {
const { restored } = scrollPositionSaver.restore()
nextLayoutEffectCallbackRef.current = undefined
// Restoring the former scroll position will trigger a scroll event. We
// need to wait for next scroll event to happen before enabling the
// scrollController events again.
if (restored) {
const handleScrollRestoreEvent = () => {
scrollController.enableScrollEvents()
window.removeEventListener("scroll", handleScrollRestoreEvent)
}
window.addEventListener("scroll", handleScrollRestoreEvent)
} else {
scrollController.enableScrollEvents()
}
}
},
[scrollController, scrollPositionSaver]
)
useLayoutEffect(() => {
// Queuing permits to restore scroll position after all useLayoutEffect
// have run, and yet preserve the sync nature of the scroll restoration
// See https://github.com/facebook/docusaurus/issues/8625
queueMicrotask(() => nextLayoutEffectCallbackRef.current?.())
})
return {
blockElementScrollPositionUntilNextRender,
}
}
type CancelScrollTop = () => void
function smoothScrollNative(top: number): CancelScrollTop {
window.scrollTo({ top, behavior: "smooth" })
return () => {
// Nothing to cancel, it's natively cancelled if user tries to scroll down
}
}
function smoothScrollPolyfill(top: number): CancelScrollTop {
let raf: number | null = null
const isUpScroll = document.documentElement.scrollTop > top
function rafRecursion() {
const currentScroll = document.documentElement.scrollTop
if (
(isUpScroll && currentScroll > top) ||
(!isUpScroll && currentScroll < top)
) {
raf = requestAnimationFrame(rafRecursion)
window.scrollTo(0, Math.floor((currentScroll - top) * 0.85) + top)
}
}
rafRecursion()
// Break the recursion. Prevents the user from "fighting" against that
// recursion producing a weird UX
return () => raf && cancelAnimationFrame(raf)
}
/**
* A "smart polyfill" of `window.scrollTo({ top, behavior: "smooth" })`.
* This currently always uses a polyfilled implementation unless
* `scroll-behavior: smooth` has been set in CSS, because native support
* detection for scroll behavior seems unreliable.
*
* This hook does not do anything by itself: it returns a start and a stop
* handle. You can execute either handle at any time.
*/
export function useSmoothScrollTo(): {
/**
* Start the scroll.
*
* @param top The final scroll top position.
*/
startScroll: (top: number) => void
/**
* A cancel function, because the non-native smooth scroll-top
* implementation must be interrupted if user scrolls down. If there's no
* existing animation or the scroll is using native behavior, this is a no-op.
*/
cancelScroll: CancelScrollTop
} {
const cancelRef = useRef<CancelScrollTop | null>(null)
// Not all have support for smooth scrolling (particularly Safari mobile iOS)
// TODO proper detection is currently unreliable!
// see https://github.com/wessberg/scroll-behavior-polyfill/issues/16
// For now, we only use native scroll behavior if smooth is already set,
// because otherwise the polyfill produces a weird UX when both CSS and JS try
// to scroll a page, and they cancel each other.
const supportsNativeSmoothScrolling =
getComputedStyle(document.documentElement).scrollBehavior === "smooth"
return {
startScroll: (top: number) => {
cancelRef.current = supportsNativeSmoothScrolling
? smoothScrollNative(top)
: smoothScrollPolyfill(top)
},
cancelScroll: () => cancelRef.current?.(),
}
}
@@ -0,0 +1,91 @@
import { useCallback, useMemo } from "react"
export type OptionType = {
value: string
label: string
index?: string
isAllOption?: boolean
}
export type SelectOptions = {
value: string | string[]
multiple?: boolean
options: OptionType[]
setSelected?: (value: string | string[]) => void
addSelected?: (value: string) => void
removeSelected?: (value: string) => void
handleAddAll?: (isAllSelected: boolean) => void
}
export const useSelect = ({
value,
options,
multiple = false,
setSelected,
addSelected,
removeSelected,
handleAddAll,
}: SelectOptions) => {
const isValueSelected = useCallback(
(val: string) => {
return (
(typeof value === "string" && val === value) ||
(Array.isArray(value) && value.includes(val))
)
},
[value]
)
// checks if there are multiple selected values
const hasSelectedValues = useMemo(() => {
return multiple && Array.isArray(value) && value.length > 0
}, [value, multiple])
// checks if there are any selected values,
// whether multiple or one
const hasSelectedValue = useMemo(() => {
return hasSelectedValues || (typeof value === "string" && value.length)
}, [hasSelectedValues, value])
const selectedValues: OptionType[] = useMemo(() => {
if (typeof value === "string") {
const selectedValue = options.find((option) => option.value === value)
return selectedValue ? [selectedValue] : []
} else if (Array.isArray(value)) {
return options.filter((option) => value.includes(option.value))
}
return []
}, [options, value])
const isAllSelected = useMemo(() => {
return Array.isArray(value) && value.length === options.length
}, [options, value])
const handleChange = (selectedValue: string, wasSelected: boolean) => {
if (multiple) {
wasSelected
? removeSelected?.(selectedValue)
: addSelected?.(selectedValue)
} else {
setSelected?.(selectedValue)
}
}
const handleSelectAll = () => {
if (handleAddAll) {
handleAddAll(isAllSelected)
} else {
setSelected?.(options.map((option) => option.value))
}
}
return {
isValueSelected,
hasSelectedValue,
hasSelectedValues,
selectedValues,
isAllSelected,
handleChange,
handleSelectAll,
}
}
@@ -0,0 +1,92 @@
"use client"
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react"
export type BaseTabType = {
label: string
value: string
}
export type EventData = {
storageValue: string
}
export type TabProps<T> = {
tabs: T[]
group?: string
}
export function useTabs<T extends BaseTabType>({ tabs, group }: TabProps<T>) {
const [selectedTab, setSelectedTab] = useState<T | null>(null)
const storageKey = useMemo(() => `tab_${group}`, [group])
const eventKey = useMemo(() => `tab_${group}_changed`, [group])
const scrollPosition = useRef<number>(0)
const changeSelectedTab = (tab: T) => {
scrollPosition.current = window.scrollY
setSelectedTab(tab)
localStorage.setItem(storageKey, tab.value)
window.dispatchEvent(
new CustomEvent<EventData>(eventKey, {
detail: {
storageValue: tab.value,
},
})
)
}
const findTabItem = useCallback(
(val: string) => {
const lowerVal = val.toLowerCase()
return tabs.find((t) => t.value.toLowerCase() === lowerVal)
},
[tabs]
)
const handleStorageChange = useCallback(
(e: CustomEvent<EventData>) => {
if (e.detail.storageValue !== selectedTab?.value) {
// check if tab exists
const tab = findTabItem(e.detail.storageValue)
if (tab) {
setSelectedTab(tab)
}
}
},
[selectedTab, findTabItem]
) as EventListener
useEffect(() => {
if (!selectedTab) {
const storedSelectedTabValue = localStorage.getItem(storageKey)
setSelectedTab(
storedSelectedTabValue
? findTabItem(storedSelectedTabValue) || tabs[0]
: tabs[0]
)
}
}, [selectedTab, storageKey, tabs, findTabItem])
useEffect(() => {
window.addEventListener(eventKey, handleStorageChange)
return () => {
window.removeEventListener(eventKey, handleStorageChange)
}
}, [handleStorageChange, eventKey])
useLayoutEffect(() => {
if (scrollPosition.current && window.scrollY !== scrollPosition.current) {
window.scrollTo(0, scrollPosition.current)
}
}, [selectedTab])
return { selectedTab, changeSelectedTab }
}
+7
View File
@@ -0,0 +1,7 @@
export * from "./constants"
export * from "./components"
export * from "./hooks"
export * from "./providers"
export * from "./types"
export * from "./utils"
View File
Whitespace-only changes.
@@ -0,0 +1,112 @@
"use client"
import React, { createContext, useContext, useEffect, useState } from "react"
import { Analytics, AnalyticsBrowser } from "@segment/analytics-next"
export type ExtraData = {
section?: string
[key: string]: any
}
export type AnalyticsContextType = {
loaded: boolean
analytics: Analytics | null
track: (
event: string,
options?: Record<string, any>,
callback?: () => void
) => void
}
const AnalyticsContext = createContext<AnalyticsContextType | null>(null)
export type AnalyticsProviderProps = {
writeKey?: string
children?: React.ReactNode
}
const LOCAL_STORAGE_KEY = "ajs_anonymous_id"
export const AnalyticsProvider = ({
writeKey = "temp",
children,
}: AnalyticsProviderProps) => {
// loaded is used to ensure that a connection has been made to segment
// even if it failed. This is to ensure that the connection isn't
// continuously retried
const [loaded, setLoaded] = useState<boolean>(false)
const [analytics, setAnalytics] = useState<Analytics | null>(null)
const analyticsBrowser = new AnalyticsBrowser()
const init = () => {
if (!loaded) {
analyticsBrowser
.load(
{ writeKey },
{
initialPageview: true,
user: {
localStorage: {
key: LOCAL_STORAGE_KEY,
},
},
}
)
.then((instance) => {
setAnalytics(instance[0])
})
.catch((e) =>
console.error(`Could not connect to Segment. Error: ${e}`)
)
.finally(() => setLoaded(true))
}
}
const track = async (
event: string,
options?: Record<string, any>,
callback?: () => void
) => {
if (analytics) {
void analytics.track(
event,
{
...options,
uuid: analytics.user().anonymousId(),
},
callback
)
} else if (callback) {
console.warn(
"Segment is either not installed or not configured. Simulating success..."
)
callback()
}
}
useEffect(() => {
init()
}, [])
return (
<AnalyticsContext.Provider
value={{
analytics,
track,
loaded,
}}
>
{children}
</AnalyticsContext.Provider>
)
}
export const useAnalytics = () => {
const context = useContext(AnalyticsContext)
if (!context) {
throw new Error("useAnalytics must be used within a AnalyticsProvider")
}
return context
}
@@ -0,0 +1,74 @@
"use client"
import React, { createContext, useContext, useEffect, useState } from "react"
export type ColorMode = "light" | "dark"
export type ColorModeContextType = {
colorMode: ColorMode
setColorMode: (value: ColorMode) => void
toggleColorMode: () => void
}
const ColorModeContext = createContext<ColorModeContextType | null>(null)
export type ColorModeProviderProps = {
children: React.ReactNode
}
export const ColorModeProvider = ({ children }: ColorModeProviderProps) => {
const [colorMode, setColorMode] = useState<ColorMode>("light")
const [loaded, setLoaded] = useState(false)
const toggleColorMode = () =>
setColorMode(colorMode === "light" ? "dark" : "light")
useEffect(() => {
if (loaded) {
return
}
const theme = localStorage.getItem("theme")
if (theme && (theme === "light" || theme === "dark")) {
setColorMode(theme)
setLoaded(true)
}
}, [])
useEffect(() => {
document.querySelector("html")?.setAttribute("data-theme", colorMode)
}, [colorMode])
useEffect(() => {
if (!loaded) {
return
}
const theme = localStorage.getItem("theme")
if (theme !== colorMode) {
localStorage.setItem("theme", colorMode)
}
}, [loaded, colorMode])
return (
<ColorModeContext.Provider
value={{
colorMode,
setColorMode,
toggleColorMode,
}}
>
{children}
</ColorModeContext.Provider>
)
}
export const useColorMode = (): ColorModeContextType => {
const context = useContext(ColorModeContext)
if (!context) {
throw new Error("useColorMode must be used inside a ColorModeProvider")
}
return context
}
@@ -0,0 +1,63 @@
"use client"
import React, {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react"
export type MobileContextType = {
isMobile?: boolean
}
const MobileContext = createContext<MobileContextType | null>(null)
export type MobileProviderProps = {
children: React.ReactNode
}
export const MobileProvider = ({ children }: MobileProviderProps) => {
const [isMobile, setIsMobile] = useState(false)
const handleResize = useCallback(() => {
if (window.innerWidth < 1025 && !isMobile) {
setIsMobile(true)
} else if (window.innerWidth >= 1025 && isMobile) {
setIsMobile(false)
}
}, [isMobile])
useEffect(() => {
window.addEventListener("resize", handleResize)
return () => {
window.removeEventListener("resize", handleResize)
}
}, [handleResize])
useEffect(() => {
handleResize()
}, [])
return (
<MobileContext.Provider
value={{
isMobile,
}}
>
{children}
</MobileContext.Provider>
)
}
export const useMobile = () => {
const context = useContext(MobileContext)
if (!context) {
throw new Error("useMobile must be used within a MobileProvider")
}
return context
}
@@ -0,0 +1,53 @@
"use client"
import React, { useContext, useState } from "react"
import { createContext } from "react"
import { Modal, type ModalProps } from "@/components"
export type ModalContextType = {
modalProps: ModalProps | null
setModalProps: (value: ModalProps | null) => void
closeModal: () => void
}
const ModalContext = createContext<ModalContextType | null>(null)
export type ModalProviderProps = {
children?: React.ReactNode
}
export const ModalProvider = ({ children }: ModalProviderProps) => {
const [modalProps, setModalProps] = useState<ModalProps | null>(null)
const closeModal = () => {
setModalProps(null)
}
return (
<ModalContext.Provider
value={{
modalProps,
setModalProps,
closeModal,
}}
>
{children}
{modalProps && (
<>
<div className="bg-medusa-bg-overlay dark:bg-medusa-bg-overlay-dark fixed top-0 left-0 z-[499] h-screen w-screen"></div>
<Modal {...modalProps} onClose={closeModal} />
</>
)}
</ModalContext.Provider>
)
}
export const useModal = () => {
const context = useContext(ModalContext)
if (!context) {
throw new Error("useModal must be used within a ModalProvider")
}
return context
}
@@ -0,0 +1,55 @@
"use client"
import React, { createContext, useContext, useState, useEffect } from "react"
import { usePathname } from "next/navigation"
export type NavbarContextType = {
activeItem: string | null
setActiveItem: (value: string) => void
}
const NavbarContext = createContext<NavbarContextType | null>(null)
export type NavbarProviderProps = {
children: React.ReactNode
basePath?: string
}
export const NavbarProvider = ({
children,
basePath = "",
}: NavbarProviderProps) => {
const [activeItem, setActiveItem] = useState<string | null>(null)
const pathname = usePathname()
const assemblePathName = (path: string) =>
`${basePath}/${path.charAt(0) === "/" ? path.substring(1) : path}`
useEffect(() => {
const newPath = assemblePathName(pathname)
if (activeItem !== newPath) {
setActiveItem(newPath)
}
}, [pathname, activeItem])
return (
<NavbarContext.Provider
value={{
activeItem,
setActiveItem,
}}
>
{children}
</NavbarContext.Provider>
)
}
export const useNavbar = (): NavbarContextType => {
const context = useContext(NavbarContext)
if (!context) {
throw new Error("useNavbar must be used inside a NavbarProvider")
}
return context
}
@@ -0,0 +1,135 @@
"use client"
import React, { createContext, useContext, useReducer } from "react"
import { NotificationItemProps, NotificationContainer } from "@/components"
import uuid from "react-uuid"
export type NotificationItemType = {
id?: string
} & NotificationItemProps
export type NotificationContextType = {
notifications: NotificationItemType[]
addNotification: (value: NotificationItemType) => void
generateId: () => string
removeNotification: (id: string) => void
updateNotification: (
id: string,
updatedData: Partial<Omit<NotificationItemType, "id">>
) => void
}
export enum NotificationReducerActionTypes {
ADD = "add",
REMOVE = "remove",
UPDATE = "update",
}
export type NotificationReducerAction =
| {
type: NotificationReducerActionTypes.ADD
notification: NotificationItemType
}
| {
type: NotificationReducerActionTypes.REMOVE
id: string
}
| {
type: NotificationReducerActionTypes.UPDATE
id: string
updatedData: Partial<Omit<NotificationItemType, "id">>
}
const notificationReducer = (
state: NotificationItemType[],
action: NotificationReducerAction
) => {
switch (action.type) {
case NotificationReducerActionTypes.ADD:
return [...state, action.notification]
case NotificationReducerActionTypes.REMOVE:
return state.filter((notification) => notification.id !== action.id)
case NotificationReducerActionTypes.UPDATE:
return state.map((notification) => {
if (notification.id === action.id) {
return {
...notification,
...action.updatedData,
}
}
return notification
})
default:
return state
}
}
const NotificationContext = createContext<NotificationContextType | null>(null)
export type NotificationProviderProps = {
children?: React.ReactNode
}
export const NotificationProvider = ({
children,
}: NotificationProviderProps) => {
const [notifications, dispatch] = useReducer(notificationReducer, [])
const generateId = () => uuid()
const addNotification = (notification: NotificationItemType) => {
if (!notification.id) {
notification.id = generateId()
}
dispatch({
type: NotificationReducerActionTypes.ADD,
notification,
})
}
const updateNotification = (
id: string,
updatedData: Partial<Omit<NotificationItemType, "id">>
) => {
dispatch({
type: NotificationReducerActionTypes.UPDATE,
id,
updatedData,
})
}
const removeNotification = (id: string) => {
dispatch({
type: NotificationReducerActionTypes.REMOVE,
id,
})
}
return (
<NotificationContext.Provider
value={{
notifications,
addNotification,
generateId,
removeNotification,
updateNotification,
}}
>
{children}
<NotificationContainer />
</NotificationContext.Provider>
)
}
export const useNotifications = (): NotificationContextType => {
const context = useContext(NotificationContext)
if (!context) {
throw new Error(
"useNotifications must be used within a NotificationProvider"
)
}
return context
}
@@ -0,0 +1,39 @@
"use client"
import React, { createContext, useContext, useState } from "react"
export type PageLoadingContextType = {
isLoading: boolean
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>
}
const PageLoadingContext = createContext<PageLoadingContextType | null>(null)
export type PageLoadingProviderProps = {
children?: React.ReactNode
}
export const PageLoadingProvider = ({ children }: PageLoadingProviderProps) => {
const [isLoading, setIsLoading] = useState(true)
return (
<PageLoadingContext.Provider
value={{
isLoading,
setIsLoading,
}}
>
{children}
</PageLoadingContext.Provider>
)
}
export const usePageLoading = (): PageLoadingContextType => {
const context = useContext(PageLoadingContext)
if (!context) {
throw new Error("usePageLoading must be used inside a PageLoadingProvider")
}
return context
}
@@ -0,0 +1,108 @@
"use client"
import React, {
createContext,
useContext,
useEffect,
useState,
useMemo,
} from "react"
import { SearchModal, SearchModalProps } from "@/components"
import { checkArraySameElms } from "../../utils"
import algoliasearch, { SearchClient } from "algoliasearch/lite"
export type SearchContextType = {
isOpen: boolean
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>
defaultFilters: string[]
setDefaultFilters: (value: string[]) => void
searchClient: SearchClient
}
const SearchContext = createContext<SearchContextType | null>(null)
export type AlgoliaProps = {
appId: string
apiKey: string
mainIndexName: string
indices: string[]
}
export type SearchProviderProps = {
children: React.ReactNode
initialDefaultFilters?: string[]
algolia: AlgoliaProps
searchProps: Omit<SearchModalProps, "algolia">
}
export const SearchProvider = ({
children,
initialDefaultFilters = [],
searchProps,
algolia,
}: SearchProviderProps) => {
const [isOpen, setIsOpen] = useState(false)
const [defaultFilters, setDefaultFilters] = useState<string[]>(
initialDefaultFilters
)
const searchClient: SearchClient = useMemo(() => {
const algoliaClient = algoliasearch(algolia.appId, algolia.apiKey)
return {
...algoliaClient,
async search(requests) {
if (requests.every(({ params }) => !params?.query)) {
return Promise.resolve({
results: requests.map(() => ({
hits: [],
nbHits: 0,
nbPages: 0,
page: 0,
processingTimeMS: 0,
hitsPerPage: 0,
exhaustiveNbHits: false,
query: "",
params: "",
})),
})
}
return algoliaClient.search(requests)
},
}
}, [algolia.appId, algolia.apiKey])
useEffect(() => {
if (
initialDefaultFilters.length &&
!checkArraySameElms(defaultFilters, initialDefaultFilters)
) {
setDefaultFilters(initialDefaultFilters)
}
}, [initialDefaultFilters])
return (
<SearchContext.Provider
value={{
isOpen,
setIsOpen,
defaultFilters,
setDefaultFilters,
searchClient,
}}
>
{children}
<SearchModal {...searchProps} algolia={algolia} />
</SearchContext.Provider>
)
}
export const useSearch = (): SearchContextType => {
const context = useContext(SearchContext)
if (!context) {
throw new Error("useSearch must be used inside a SearchProvider")
}
return context
}
@@ -0,0 +1,318 @@
"use client"
import React, {
ReactNode,
createContext,
useCallback,
useContext,
useEffect,
useReducer,
useState,
} from "react"
import { usePathname } from "next/navigation"
export enum SidebarItemSections {
TOP = "top",
BOTTOM = "bottom",
MOBILE = "mobile",
}
export type SidebarItemType = {
path?: string
title: string
additionalElms?: React.ReactNode
children?: SidebarItemType[]
loaded?: boolean
isPathHref?: boolean
}
export type SidebarSectionItemsType = {
[k in SidebarItemSections]: SidebarItemType[]
}
export type SidebarContextType = {
items: SidebarSectionItemsType
activePath: string | null
getActiveItem: () => SidebarItemType | undefined
setActivePath: (path: string | null) => void
isItemActive: (item: SidebarItemType, checkChildren?: boolean) => boolean
addItems: (
item: SidebarItemType[],
options?: {
section?: SidebarItemSections
parent?: {
path: string
changeLoaded?: boolean
}
indexPosition?: number
ignoreExisting?: boolean
}
) => void
findItemInSection: (
section: SidebarItemType[],
item: Partial<SidebarItemType>,
checkChildren?: boolean
) => SidebarItemType | undefined
mobileSidebarOpen: boolean
setMobileSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>
isSidebarEmpty: () => boolean
desktopSidebarOpen: boolean
setDesktopSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>
}
export const SidebarContext = createContext<SidebarContextType | null>(null)
export type ActionOptionsType = {
section?: SidebarItemSections
parent?: {
path: string
changeLoaded?: boolean
}
indexPosition?: number
ignoreExisting?: boolean
}
export type ActionType = {
type: "add" | "update"
items: SidebarItemType[]
options?: ActionOptionsType
}
export const reducer = (
state: SidebarSectionItemsType,
{ type, items, options }: ActionType
) => {
const {
section = SidebarItemSections.TOP,
parent,
indexPosition,
} = options || {}
switch (type) {
case "add":
return {
...state,
[section]:
indexPosition !== undefined
? [
...state[section].slice(0, indexPosition),
...items,
...state[section].slice(indexPosition),
]
: [...state[section], ...items],
}
case "update":
// find item index
return {
...state,
[section]: state[section].map((i) => {
if (i.path && parent?.path && i.path === parent?.path) {
return {
...i,
children: [...(i.children || []), ...items],
loaded: parent.changeLoaded ? true : i.loaded,
}
}
return i
}),
}
default:
return state
}
}
export type SidebarProviderProps = {
children?: ReactNode
isLoading?: boolean
setIsLoading?: React.Dispatch<React.SetStateAction<boolean>>
initialItems?: SidebarSectionItemsType
shouldHandleHashChange?: boolean
shouldHandlePathChange?: boolean
}
export const SidebarProvider = ({
children,
isLoading = false,
setIsLoading,
initialItems,
shouldHandleHashChange = false,
shouldHandlePathChange = false,
}: SidebarProviderProps) => {
const [items, dispatch] = useReducer(reducer, {
top: initialItems?.top || [],
bottom: initialItems?.bottom || [],
mobile: initialItems?.mobile || [],
})
const [activePath, setActivePath] = useState<string | null>("")
const [mobileSidebarOpen, setMobileSidebarOpen] = useState<boolean>(false)
const [desktopSidebarOpen, setDesktopSidebarOpen] = useState(true)
const pathname = usePathname()
const findItemInSection = useCallback(
(
section: SidebarItemType[],
item: Partial<SidebarItemType>,
checkChildren = true
): SidebarItemType | undefined => {
return section.find((i) => {
if (!item.path) {
return !i.path && i.title === item.title
} else {
return (
i.path === item.path ||
(checkChildren && i.children && findItemInSection(i.children, item))
)
}
})
},
[]
)
const getActiveItem = useCallback(() => {
if (activePath === null) {
return undefined
}
return (
findItemInSection(items.mobile, { path: activePath }) ||
findItemInSection(items.top, { path: activePath }) ||
findItemInSection(items.bottom, { path: activePath })
)
}, [activePath, items, findItemInSection])
const addItems = (
newItems: SidebarItemType[],
options?: {
section?: SidebarItemSections
parent?: {
path: string
changeLoaded?: boolean
}
indexPosition?: number
ignoreExisting?: boolean
}
) => {
const {
section = SidebarItemSections.TOP,
parent,
ignoreExisting = false,
} = options || {}
if (!ignoreExisting) {
const selectedSection =
section === SidebarItemSections.BOTTOM ? items.bottom : items.top
newItems = newItems.filter(
(item) => !findItemInSection(selectedSection, item)
)
}
if (!newItems.length) {
return
}
dispatch({
type: parent ? "update" : "add",
items: newItems,
options,
})
}
const isItemActive = useCallback(
(item: SidebarItemType, checkChildren = false): boolean => {
return (
item.path === activePath ||
(checkChildren && activePath?.split("_")[0] === item.path)
)
},
[activePath]
)
const isSidebarEmpty = useCallback((): boolean => {
return Object.values(items).every(
(sectionItems) => sectionItems.length === 0
)
}, [items])
const init = () => {
const currentPath = location.hash.replace("#", "")
if (currentPath) {
setActivePath(currentPath)
}
}
// this is mainly triggered by Algolia
const handleHashChange = useCallback(() => {
const currentPath = location.hash.replace("#", "")
if (currentPath !== activePath) {
setActivePath(currentPath)
}
}, [activePath])
useEffect(() => {
if (!shouldHandleHashChange) {
return
}
init()
const handleScroll = () => {
if (window.scrollY === 0) {
setActivePath("")
// can't use next router as it doesn't support
// changing url without scrolling
history.replaceState({}, "", location.pathname)
}
}
window.addEventListener("scroll", handleScroll)
window.addEventListener("hashchange", handleHashChange)
return () => {
window.removeEventListener("scroll", handleScroll)
window.removeEventListener("hashchange", handleHashChange)
}
}, [handleHashChange, shouldHandleHashChange])
useEffect(() => {
if (isLoading && items.top.length && items.bottom.length) {
setIsLoading?.(false)
}
}, [items, isLoading, setIsLoading])
useEffect(() => {
if (shouldHandlePathChange && pathname !== activePath) {
setActivePath(pathname)
}
}, [shouldHandlePathChange, pathname])
return (
<SidebarContext.Provider
value={{
items,
addItems,
activePath,
setActivePath,
isItemActive,
findItemInSection,
mobileSidebarOpen,
setMobileSidebarOpen,
isSidebarEmpty,
getActiveItem,
desktopSidebarOpen,
setDesktopSidebarOpen,
}}
>
{children}
</SidebarContext.Provider>
)
}
export const useSidebar = (): SidebarContextType => {
const context = useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used inside a SidebarProvider")
}
return context
}
@@ -0,0 +1,9 @@
export * from "./Analytics"
export * from "./ColorMode"
export * from "./Mobile"
export * from "./Modal"
export * from "./Navbar"
export * from "./Notification"
export * from "./PageLoading"
export * from "./Search"
export * from "./Sidebar"
+3
View File
@@ -0,0 +1,3 @@
export type Ref<T> =
| React.MutableRefObject<T | null>
| ((instance: T | null) => void)
@@ -0,0 +1,10 @@
export function checkArraySameElms(
arr1: Array<any>,
arr2: Array<any>
): boolean {
if (arr1.length !== arr2.length) {
return false
}
return arr1.every((value, index) => value === arr2[index])
}
@@ -0,0 +1,3 @@
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
}
@@ -0,0 +1,21 @@
export function checkSidebarItemVisibility(
item: Element,
options?: {
topMargin?: number
}
) {
const sidebar = document.getElementById("sidebar")
if (!sidebar) {
return false
}
const { topMargin = 0 } = options || {}
const sidebarBoundingRect = sidebar.getBoundingClientRect()
const activeItemBoundingRect = item.getBoundingClientRect()
return (
activeItemBoundingRect.top >= topMargin &&
activeItemBoundingRect.top - sidebarBoundingRect.height + topMargin < 0 &&
activeItemBoundingRect.bottom > 0
)
}
@@ -0,0 +1,29 @@
export function findPrevSibling(
element: HTMLElement,
selector: string
): HTMLElement | null {
let prevElement = element.previousElementSibling
while (prevElement !== null) {
if (prevElement.matches(selector)) {
return prevElement as HTMLElement
}
prevElement = prevElement.previousElementSibling
}
return null
}
export function findNextSibling(
element: HTMLElement,
selector: string
): HTMLElement | null {
let nextElement = element.nextElementSibling
while (nextElement !== null) {
if (nextElement.matches(selector)) {
return nextElement as HTMLElement
}
nextElement = nextElement.nextElementSibling
}
return null
}
@@ -0,0 +1,7 @@
import { GITHUB_ISSUES_PREFIX } from "../constants"
export function formatReportLink(title: string, sectionTitle: string) {
return `${GITHUB_ISSUES_PREFIX}&title=${encodeURI(
title
)}%3A%20Issue%20in%20${encodeURI(sectionTitle)}`
}
+5
View File
@@ -0,0 +1,5 @@
export * from "./array-same-elms"
export * from "./capitalize"
export * from "./check-sidebar-item-visibility"
export * from "./dom-utils"
export * from "./format-report-link"
+7
View File
@@ -0,0 +1,7 @@
import coreConfig from "tailwind/modified.tailwind.config"
/** @type {import('tailwindcss').Config} */
module.exports = {
...coreConfig,
content: ["./src/**/*.{js,jsx,ts,tsx}"],
}
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./dist/cjs",
"module": "CommonJS"
},
"exclude": [
"tailwind.config.js",
"postcss.config.js"
]
}
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./dist/esm",
"module": "ES2020"
},
"exclude": [
"tailwind.config.js",
"postcss.config.js"
]
}
+29
View File
@@ -0,0 +1,29 @@
{
"extends": "tsconfig/base.json",
"compilerOptions": {
"paths": {
"docs-ui": ["./src/index.ts"],
"@/*": [
"./src/*"
]
},
"outDir": "dist",
"tsBuildInfoFile": "./dist/.tsbuildinfo-client",
"noEmit": false,
"jsx": "react",
"lib": ["dom", "ES2015"],
"module": "ESNext",
"target": "es6",
"declaration": true,
"declarationMap": true,
"typeRoots": [
"./node_modules/@types",
"./types"
]
},
"include": [
"src/**/*",
"tailwind.config.js",
"postcss.config.js"
]
}
+134
View File
@@ -0,0 +1,134 @@
module.exports = {
root: true,
parser: "@babel/eslint-parser",
parserOptions: {
requireConfigFile: false,
ecmaFeatures: {
experimentalDecorators: true,
jsx: true,
},
project: true,
},
plugins: ["prettier"],
extends: [
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
],
settings: {
react: {
version: "detect",
},
},
rules: {
curly: ["error", "all"],
"new-cap": "off",
"require-jsdoc": "off",
"no-unused-expressions": "off",
"no-unused-vars": "off",
camelcase: "off",
"no-invalid-this": "off",
"max-len": [
"error",
{
code: 80,
ignoreStrings: true,
ignoreRegExpLiterals: true,
ignoreComments: true,
ignoreTrailingComments: true,
ignoreUrls: true,
ignoreTemplateLiterals: true,
},
],
semi: ["error", "never"],
quotes: [
"error",
"double",
{
allowTemplateLiterals: true,
},
],
"comma-dangle": [
"error",
{
arrays: "always-multiline",
objects: "always-multiline",
imports: "always-multiline",
exports: "always-multiline",
functions: "never",
},
],
"object-curly-spacing": ["error", "always"],
"arrow-parens": ["error", "always"],
"linebreak-style": 0,
"no-confusing-arrow": [
"error",
{
allowParens: false,
},
],
"space-before-function-paren": [
"error",
{
anonymous: "always",
named: "never",
asyncArrow: "always",
},
],
"space-infix-ops": "error",
"eol-last": ["error", "always"],
"no-console": [
"error",
{
allow: ["error", "warn"],
},
],
"react/prop-types": [
2,
{ "ignore": ["className"] }
],
},
env: {
es6: true,
node: true,
jest: true,
browser: true,
},
ignorePatterns: [
"eslint-config-docs",
".eslintrc.js",
"dist"
],
overrides: [
{
files: ["*.ts", "*.tsx", "*.js", "*.jsx"],
plugins: ["@typescript-eslint/eslint-plugin"],
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: {
project: "./tsconfig.json"
},
rules: {
"valid-jsdoc": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/keyword-spacing": "error",
"@typescript-eslint/space-before-function-paren": [
"error",
{
anonymous: "always",
named: "never",
asyncArrow: "always",
},
],
"@typescript-eslint/space-infix-ops": "error",
},
},
],
}
+112
View File
@@ -0,0 +1,112 @@
module.exports = {
parser: "@babel/eslint-parser",
parserOptions: {
requireConfigFile: false,
ecmaFeatures: {
experimentalDecorators: true,
},
},
settings: {
react: {
version: "detect",
},
},
env: {
es6: true,
node: true,
},
plugins: ["prettier", "markdown"],
ignorePatterns: [
"**/content/references/**",
"**/content/**/events-list.md"
],
rules: {
"no-undef": "off",
"no-unused-expressions": "off",
"no-unused-vars": "off",
"no-unused-labels": "off",
"no-console": "off",
curly: ["error", "all"],
"new-cap": "off",
"require-jsdoc": "off",
camelcase: "off",
"no-invalid-this": "off",
"max-len": [
"error",
{
code: 64,
},
],
semi: ["error", "never"],
quotes: [
"error",
"double",
{
allowTemplateLiterals: true,
},
],
"comma-dangle": [
"error",
{
arrays: "always-multiline",
objects: "always-multiline",
imports: "always-multiline",
exports: "always-multiline",
functions: "never",
},
],
"object-curly-spacing": ["error", "always"],
"arrow-parens": ["error", "always"],
"linebreak-style": 0,
"no-confusing-arrow": [
"error",
{
allowParens: false,
},
],
"space-before-function-paren": [
"error",
{
anonymous: "always",
named: "never",
asyncArrow: "always",
},
],
"space-infix-ops": "off",
"eol-last": ["error", "always"],
"react/prop-types": "off",
"react/jsx-no-undef": "off"
},
overrides: [
{
files: ["**/*.md", "**/*.mdx"],
processor: "markdown/markdown",
},
{
files: [
"**/*.md/*.js",
"**/*.mdx/*.js",
"**/*.md/*.jsx",
"**/*.mdx/*.jsx",
],
},
{
files: [
"**/*.md/*.ts",
"**/*.mdx/*.ts",
"**/*.md/*.tsx",
"**/*.mdx/*.tsx",
],
plugins: ["@typescript-eslint/eslint-plugin"],
extends: ["plugin:@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
rules: {
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-var-requires": "off",
},
},
],
}
@@ -0,0 +1,14 @@
module.exports = {
extends: [
"plugin:@docusaurus/recommended",
"google",
"./base.js",
],
ignorePatterns: [
"**/node_modules/**",
"**/.docusaurus/*",
"**/build/*",
".eslintrc.js",
"babel.config.js"
]
}
+21
View File
@@ -0,0 +1,21 @@
module.exports = {
extends: [
"next/core-web-vitals",
"google",
"./base.js"
],
"rules": {
"react/react-in-jsx-scope": "off",
"@typescript-eslint/prefer-ts-expect-error": "off"
},
ignorePatterns: [
"next.config.js",
"spec",
"node_modules",
"public",
".eslintrc.js"
],
parserOptions: {
project: true,
},
}
@@ -0,0 +1,24 @@
{
"name": "eslint-config-docs",
"license": "MIT",
"version": "0.0.0",
"private": true,
"main": "base.js",
"dependencies": {
"@babel/eslint-parser": "^7.22.15",
"@docusaurus/eslint-plugin": "latest",
"@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0",
"eslint-config-google": "^0.14.0",
"eslint-config-next": "latest",
"eslint-config-prettier": "^8.7.0",
"eslint-config-turbo": "^1.10.12",
"eslint-plugin-markdown": "^3.0.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"next": "latest"
},
"devDependencies": {
"eslint": "^8.36.0"
}
}
+9
View File
@@ -0,0 +1,9 @@
const path = require("path")
module.exports = {
extends: ["docs"],
parserOptions: {
project: true,
tsconfigRootDir: path.join(__dirname, "..", ".."),
}
}
@@ -0,0 +1,786 @@
import plugin from "tailwindcss/plugin"
/** @type {import('tailwindcss').Config} */
module.exports = {
presets: [require("@medusajs/ui-preset")],
darkMode: ["class", `[data-theme="dark"]`], // hooks into docusaurus' dark mode settings
// TODO: remove anything that can be replaced by the
// UI preset
theme: {
extend: {
colors: {
/* docs colors */
medusa: {
bg: {
subtle: {
DEFAULT: "#F9FAFB",
dark: "#18181A",
hover: {
DEFAULT: "#F3F4F6",
dark: "#1B1B1F",
},
pressed: {
DEFAULT: "#E5E7EB",
dark: "#27282D",
},
},
base: {
DEFAULT: "#FFFFFF",
dark: "#1B1B1F",
hover: {
DEFAULT: "#F9FAFB",
dark: "#27282D",
},
pressed: {
DEFAULT: "#F3F4F6",
dark: "#2E3035",
},
},
component: {
DEFAULT: "#F1F3F5",
dark: "#27282D",
},
"switch-off": {
DEFAULT: "#E5E7EB",
dark: "#35373C",
hover: {
DEFAULT: "#E5E7EB",
dark: "#464B50",
},
},
interactive: {
DEFAULT: "#3B82F6",
dark: "#60A5FA",
},
overlay: {
DEFAULT: "rgba(3, 7, 18, 0.4)",
dark: "rgba(24, 24, 26, 0.7)",
},
disabled: {
DEFAULT: "#F3F4F6",
dark: "#27282D",
},
highlight: {
DEFAULT: "#EFF6FF",
dark: "#172554",
hover: {
DEFAULT: "#DBEAFE",
dark: "#1E3A8A",
},
},
field: {
DEFAULT: "#F9FAFB",
dark: "#27282D",
hover: {
DEFAULT: "#F3F4F6",
dark: "#2E3035",
},
},
},
fg: {
base: {
DEFAULT: "#030712",
dark: "#EDEEF0",
},
subtle: {
DEFAULT: "#4B5563",
dark: "#ADB1B8",
},
muted: {
DEFAULT: "#9CA3AF",
dark: "#696E77",
},
disabled: {
DEFAULT: "#D1D5DB",
dark: "#3C3F44",
},
on: {
color: {
DEFAULT: "#FFFFFF",
dark: "#FFFFFF",
},
inverted: {
DEFAULT: "#FFFFFF",
dark: "#18181A",
},
},
interactive: {
DEFAULT: "#3B82F6",
dark: "#60A5FA",
hover: {
DEFAULT: "#2563EB",
dark: "#3B82F6",
},
},
error: {
DEFAULT: "#E11D48",
dark: "#FB7185",
},
},
border: {
base: {
DEFAULT: "#E5E7EB",
dark: "#2E3035",
},
strong: {
DEFAULT: "#D1D5DB",
dark: "#35373C",
},
loud: {
DEFAULT: "#030712",
dark: "#EDEEF0",
},
interactive: {
DEFAULT: "#3B82F6",
dark: "#60A5FA",
},
error: {
DEFAULT: "#E11D48",
dark: "#F43F5E",
},
danger: {
DEFAULT: "#BE123C",
dark: "#BE123C",
},
transparent: {
DEFAULT: "rgba(3, 7, 18, 0)",
dark: "rgba(238, 238, 238, 0)",
},
},
button: {
inverted: {
DEFAULT: "#030712",
dark: "#EDEEF0",
hover: {
DEFAULT: "#1F2937",
dark: "#FFFFFF",
},
pressed: {
DEFAULT: "#374151",
dark: "#EDEEF0",
},
},
neutral: {
DEFAULT: "#FFFFFF",
dark: "#27282D",
hover: {
DEFAULT: "#F9FAFB",
dark: "#35373C",
},
pressed: {
DEFAULT: "#F3F4F6",
dark: "#3C3F44",
},
},
danger: {
DEFAULT: "#E11D48",
dark: "#9F1239",
hover: {
DEFAULT: "#E11D48",
dark: "#BE123C",
},
pressed: {
DEFAULT: "#BE123C",
dark: "#E11D48",
},
},
transparent: {
DEFAULT: "rgba(255, 255, 255, 0)",
dark: "rgba(0, 0, 0, 0)",
hover: {
DEFAULT: "#F9FAFB",
dark: "#27282D",
},
pressed: {
DEFAULT: "#F3F4F6",
dark: "#2E3035",
},
},
disabled: {
DEFAULT: "#ECEEF0",
dark: "#28282C",
},
},
tag: {
neutral: {
bg: {
DEFAULT: "#F3F4F6",
dark: "#2E3035",
hover: {
DEFAULT: "#E5E7EB",
dark: "#35373C",
},
},
text: {
DEFAULT: "#4B5563",
dark: "#ADB1B8",
},
icon: {
DEFAULT: "#6B7280",
dark: "#7D828A",
},
border: {
DEFAULT: "#E5E7EB",
dark: "#3C3F44",
},
},
purple: {
bg: {
DEFAULT: "#EDE9FE",
dark: "#2E1064",
hover: {
DEFAULT: "#DDD6FE",
dark: "#4C1D95",
},
},
text: {
DEFAULT: "#6D28D9",
dark: "#8B5CF6",
},
icon: {
DEFAULT: "#7C3AED",
dark: "#6D28D9",
},
border: {
DEFAULT: "#DDD6FE",
dark: "#4C1D95",
},
},
blue: {
bg: {
DEFAULT: "#DBEAFE",
dark: "#172554",
hover: {
DEFAULT: "#BFDBFE",
dark: "#1E2A8A",
},
},
text: {
DEFAULT: "#1D4ED8",
dark: "#3B82F6",
},
icon: {
DEFAULT: "#2563EB",
dark: "#1D4ED8",
},
border: {
DEFAULT: "#BFDBFE",
dark: "#1E3A8A",
},
},
green: {
bg: {
DEFAULT: "#D1FAE5",
dark: "#022C22",
hover: {
DEFAULT: "#A7F3D0",
dark: "#064E3B",
},
},
text: {
DEFAULT: "#047857",
dark: "#10B981",
},
icon: {
DEFAULT: "#059669",
dark: "#047857",
},
border: {
DEFAULT: "#A7F3D0",
dark: "#064E3B",
},
},
orange: {
bg: {
DEFAULT: "#FEF4C7",
dark: "#451A03",
hover: {
DEFAULT: "#FDE68A",
dark: "#78350F",
},
},
text: {
DEFAULT: "#B45309",
dark: "#F59E0B",
},
icon: {
DEFAULT: "#D97706",
dark: "#B45309",
},
border: {
DEFAULT: "#FDE68A",
dark: "#78350F",
},
},
red: {
bg: {
DEFAULT: "#FFE4E6",
dark: "#4C0519",
hover: {
DEFAULT: "#FECDD3",
dark: "#881337",
},
},
text: {
DEFAULT: "#BE123C",
dark: "#FB7185",
},
icon: {
DEFAULT: "#E11D48",
dark: "#F43F5E",
},
border: {
DEFAULT: "#FECDD3",
dark: "#881337",
},
},
},
code: {
text: {
base: {
DEFAULT: "#F9FAFB",
dark: "#EDEEF0",
},
subtle: {
DEFAULT: "#9CA3AF",
dark: "#696E77",
},
highlight: "#102A4C",
},
icon: {
DEFAULT: "#6B7280",
dark: "#464B50",
},
bg: {
base: {
DEFAULT: "#111827",
dark: "#1B1B1F",
// Need this one for color-fade
transparent: {
DEFAULT: "#11182700",
dark: "#1B1B1F00",
},
},
header: {
DEFAULT: "#1F2937",
dark: "#18181A",
},
},
border: {
DEFAULT: "#374151",
dark: "#2E3035",
},
},
},
/* docs defaults */
docs: {
bg: {
DEFAULT: "#FFFFFF",
dark: "#18181A",
surface: {
DEFAULT: "#F9FAFB",
dark: "#1B1B1F",
},
},
},
},
boxShadow: {
"card-rest":
"0px 2px 4px 0px rgba(3, 7, 18, 0.04), 0px 1px 2px -1px rgba(3, 7, 18, 0.08), 0px 0px 0px 1px rgba(3, 7, 18, 0.08)",
"card-rest-dark":
"0px 2px 4px 0px rgba(0, 0, 0, 0.40), 0px 1px 2px -1px rgba(255, 255, 255, 0.16), 0px 0px 0px 1px rgba(255, 255, 255, 0.10)",
"card-hover":
"0px 2px 8px 0px rgba(3, 7, 18, 0.10), 0px 1px 2px -1px rgba(3, 7, 18, 0.08), 0px 0px 0px 1px rgba(3, 7, 18, 0.08)",
"card-hover-dark":
"0px 2px 8px 0px rgba(0, 0, 0, 0.40), 0px 1px 2px -1px rgba(255, 255, 255, 0.16), 0px 0px 0px 1px rgba(255, 255, 255, 0.10)",
tooltip:
"0px 4px 8px 0px rgba(3, 7, 18, 0.08), 0px 0px 0px 1px rgba(3, 7, 18, 0.08)",
"tooltip-dark":
"0px 4px 8px 0px rgba(0, 0, 0, 0.32), 0px 0px 0px 1px rgba(255, 255, 255, 0.10)",
flyout:
"0px 8px 16px 0px rgba(3, 7, 18, 0.08), 0px 0px 0px 1px rgba(3, 7, 18, 0.08)",
"flyout-dark":
"0px 8px 16px 0px rgba(0, 0, 0, 0.32), 0px 0px 0px 1px rgba(255, 255, 255, 0.10)",
modal:
"0px 2px 24px 0px rgba(3, 7, 18, 0.08), 0px 16px 32px 0px rgba(3, 7, 18, 0.08), 0px 0px 0px 1px rgba(3, 7, 18, 0.08), 0px 0px 0px 2px rgba(229, 231, 235, 0.40) inset, 0px 0px 0px 1px #FFF inset",
"modal-dark":
"0px 2px 24px 0px rgba(0, 0, 0, 0.32), 0px 16px 32px 0px rgba(0, 0, 0, 0.32), 0px 0px 0px 1px rgba(255, 255, 255, 0.10)",
"navbar-dark": "0px 1px 0px 0px #2E2E32",
"button-colored":
"0px 0.5px 0px 0px rgba(3, 7, 18, 0.16), 0px 0.25px 0px 0px rgba(3, 7, 18, 0.16), 0px 1.75px 0px 0px rgba(255, 255, 255, 0.16) inset",
"button-colored-dark":
"0px 0.5px 0px 0px rgba(0, 0, 0, 0.60), 0px 0.25px 0px 0px rgba(0, 0, 0, 0.60)",
"button-colored-focused":
"0px 0px 0px 3px rgba(59, 130, 246, 0.60), 0px 0px 0px 1px #FFF, 0px 0.5px 0px 0px rgba(3, 7, 18, 0.20), 0px 0.25px 0px 0px rgba(3, 7, 18, 0.20), 0px 1.75px 0px 0px rgba(255, 255, 255, 0.16) inset",
"button-colored-focused-dark":
"0px 0px 0px 3px rgba(96, 165, 250, 0.80), 0px 0px 0px 1px #1B1B1F, 0px 0.5px 0px 0px rgba(0, 0, 0, 0.60), 0px 0.25px 0px 0px rgba(0, 0, 0, 0.60)",
"button-neutral":
"0px 0.5px 0px 0px rgba(3, 7, 18, 0.16), 0px 0.25px 0px 0px rgba(3, 7, 18, 0.16)",
"button-neutral-dark":
"0px 0.5px 0px 0px rgba(0, 0, 0, 0.60), 0px 0.25px 0px 0px rgba(0, 0, 0, 0.60), 0px 1.5px 0px 0px rgba(255, 255, 255, 0.10) inset",
"button-neutral-focused":
"0px 0px 0px 3px rgba(59, 130, 246, 0.60), 0px 0px 0px 1px #FFF, 0px 0.5px 0px 0px rgba(3, 7, 18, 0.16), 0px 0.25px 0px 0px rgba(3, 7, 18, 0.16)",
"button-neutral-focused-dark":
"0px 0px 0px 3px rgba(96, 165, 250, 0.80), 0px 0px 0px 1px #1B1B1F, 0px 0.5px 0px 0px rgba(0, 0, 0, 0.60), 0px 0.25px 0px 0px rgba(0, 0, 0, 0.60), 0px 1.5px 0px 0px rgba(255, 255, 255, 0.10) inset",
"button-secondary":
"0px 1px 1px 0px rgba(3, 7, 18, 0.06), 0px -1px 0px 0px rgba(3, 7, 18, 0.08) inset",
"button-secondary-dark":
"0px 1px 1px 0px rgba(3, 7, 18, 0.06), 0px -1px 0px 0px rgba(3, 7, 18, 0.08) inset", // TODO change
"button-secondary-focus":
"0px 0px 0px 3px rgba(59, 130, 246, 0.60), 0px 0px 0px 1px #FFF, 0px 1px 1px 0px rgba(3, 7, 18, 0.06), 0px -1px 0px 0px rgba(3, 7, 18, 0.08) inset",
"button-secondary-focus-dark":
"0px 0px 0px 3px rgba(59, 130, 246, 0.60), 0px 0px 0px 1px #FFF, 0px 1px 1px 0px rgba(3, 7, 18, 0.06), 0px -1px 0px 0px rgba(3, 7, 18, 0.08) inset", // TODO change
// TODO remove if not used
active: "0px 0px 0px 3px #E1F0FF",
"active-dark": "0px 0px 0px 3px #2C2250",
navbar: "0px 1px 0px 0px #E6E8EB",
},
borderRadius: {
xxs: "2px",
xs: "4px",
sm: "6px",
DEFAULT: "8px",
md: "8px",
lg: "12px",
xl: "16px",
},
lineHeight: {
DEFAULT: "24px",
},
backgroundImage: ({ theme }) => ({
"button-neutral":
"linear-gradient(180deg, rgba(3, 7, 18, 0.00) 0%, rgba(3, 7, 18, 0.03) 100%)",
"button-neutral-dark":
"linear-gradient(180deg, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0.00) 100%)",
"no-image": "none",
"button-inverted":
"linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.00) 100%)",
"button-inverted-dark":
"linear-gradient(180deg, rgba(0, 0, 0, 0.00) 0%, rgba(0, 0, 0, 0.12) 100%)",
"button-danger":
"linear-gradient(180deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.00) 100%)",
"button-danger-dark":
"linear-gradient(180deg, rgba(255, 255, 255, 0.10) 0%, rgba(255, 255, 255, 0.00) 100%)",
"button-danger-hover":
"linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.00) 100%)",
"button-danger-hover-dark":
"linear-gradient(180deg, rgba(255, 255, 255, 0.14) 0%, rgba(255, 255, 255, 0.00) 100%)",
"button-danger-pressed":
"linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, rgba(255, 255, 255, 0.16) 100%)",
"button-danger-pressed-dark":
"linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, rgba(255, 255, 255, 0.14) 100%)",
"code-fade": `linear-gradient(90deg, ${theme(
"colors.medusa.code.bg.base.transparent.DEFAULT"
)}, ${theme("colors.medusa.code.bg.base.DEFAULT")} 24px)`,
"code-fade-dark": `linear-gradient(90deg, ${theme(
"colors.medusa.code.bg.base.transparent.dark"
)}, ${theme("colors.medusa.code.bg.base.dark")} 24px)`,
fade: "linear-gradient(to top, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0))",
"fade-dark":
"linear-gradient(to top, rgba(27, 27, 31, 1), rgba(27, 27, 31, 0))",
}),
screens: {
xs: "576px",
lg: "1025px",
xl: "1419px",
xxl: "1440px",
},
transitionTimingFunction: {
ease: "ease",
},
width: {
sidebar: "321px",
"sidebar-hidden": "0px",
"main-content": "1140px",
"main-content-hidden-sidebar": "1440px",
"ref-sidebar": "280px",
"ref-main": "calc(100% - 280px)",
"ref-content": "calc(100% - 484px)",
},
height: {
navbar: "57px",
},
maxWidth: {
"main-content": "1140px",
"main-content-hidden-sidebar": "1440px",
xl: "1419px",
xxl: "1440px",
},
minWidth: {
xl: "1419px",
},
fontSize: {
h1: [
"32px",
{
lineHeight: "44px",
fontWeight: "500",
},
],
h2: [
"24px",
{
lineHeight: "32px",
fontWeight: "500",
},
],
h3: [
"18px",
{
lineHeight: "28px",
fontWeight: "500",
},
],
h4: [
"16px",
{
lineHeight: "20px",
fontWeight: "500",
},
],
"compact-large-plus": [
"16px",
{
lineHeight: "20px",
fontWeight: "500",
},
],
"compact-large": [
"16px",
{
lineHeight: "20px",
fontWeight: "400",
},
],
"compact-medium-plus": [
"14px",
{
lineHeight: "20px",
fontWeight: "500",
},
],
"compact-medium": [
"14px",
{
lineHeight: "20px",
fontWeight: "400",
},
],
"compact-small-plus": [
"13px",
{
lineHeight: "20px",
fontWeight: "500",
},
],
"compact-small": [
"13px",
{
lineHeight: "20px",
fontWeight: "400",
},
],
"compact-x-small-plus": [
"12px",
{
lineHeight: "20px",
fontWeight: "500",
},
],
"compact-x-small": [
"12px",
{
lineHeight: "20px",
fontWeight: "400",
},
],
"x-large-plus": [
"18px",
{
lineHeight: "32px",
fontWeight: "500",
},
],
"x-large": [
"18px",
{
lineHeight: "32px",
fontWeight: "400",
},
],
"large-plus": [
"16px",
{
lineHeight: "28px",
fontWeight: "500",
},
],
large: [
"16px",
{
lineHeight: "28px",
fontWeight: "400",
},
],
"medium-plus": [
"14px",
{
lineHeight: "24px",
fontWeight: "500",
},
],
medium: [
"14px",
{
lineHeight: "24px",
fontWeight: "400",
},
],
"code-label": [
"13px",
{
lineHeight: "20px",
fontWeight: "400",
},
],
"code-body": [
"13px",
{
lineHeight: "24px",
fontWeight: "400",
},
],
},
keyframes: {
fadeIn: {
from: { opacity: 0 },
to: { opacity: 1 },
},
fadeOut: {
from: { opacity: 1 },
to: { opacity: 0 },
},
tada: {
from: {
transform: "scale3d(1, 1, 1)",
},
"10%, 20%": {
transform: "scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg)",
},
"30%, 50%, 70%, 90%": {
transform: "scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg)",
},
"40%, 60%, 80%": {
transform: "scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg)",
},
to: {
transform: "scale3d(1, 1, 1)",
},
},
},
animation: {
fadeIn: "fadeIn 500ms",
fadeOut: "fadeOut 500ms",
tada: "tada 1s",
},
},
fontFamily: {
base: [
"var(--font-inter)",
"Inter",
"BlinkMacSystemFont",
"-apple-system",
"Segoe UI",
"Roboto",
"Oxygen",
"Ubuntu",
"Cantarell",
"Fira Sans",
"Droid Sans",
"Helvetica Neue",
"Helvetica",
"Arial",
"sans-serif",
],
monospace: [
"var(--font-roboto-mono)",
"Roboto Mono",
"SFMono-Regular",
"Menlo",
"Monaco",
"Consolas",
"Liberation Mono",
"Courier New",
"monospace",
],
},
spacing: {
px: "1px",
0: "0px",
0.125: "2px",
0.25: "4px",
0.4: "7px",
0.5: "8px",
0.75: "12px",
1: "16px",
1.5: "24px",
2: "32px",
2.5: "40px",
3: "48px",
4: "64px",
5: "80px",
6: "96px",
7: "112px",
8: "128px",
},
backgroundImage: {
"large-card": "url('/img/squares-bg-light.svg')",
"large-card-dark": "url('/img/squares-bg.svg')",
"large-card-fade":
"linear-gradient(transparent, url('/img/squares-bg-light.svg'))",
"large-card-fade-dark":
"linear-gradient(transparent, url('/img/squares-bg.svg'))",
"large-card-fade-hover": "linear-gradient(transparent, #1C1C1F)",
"large-card-fade-hover-dark": "linear-gradient(transparent, #232326)",
"announcement-bg": "url('/img/announcement-bg.svg')",
"card-highlighted": "url('/img/small-squares-bg-light.svg')",
"card-highlighted-dark": "url('/img/small-squares-bg.svg')",
"search-hit": "url('/img/search-hit-light.svg')",
"search-hit-dark": "url('/img/search-hit.svg')",
"search-arrow": "url('/img/search-hit-arrow-light.svg')",
"search-arrow-dark": "url('/img/search-hit-arrow.svg')",
"search-no-result": "url('/img/search-no-result-light.svg')",
"search-no-result-dark": "url('/img/search-no-result.svg')",
"magnifying-glass": "url('/img/magnifying-glass.svg')",
"magnifying-glass-dark": "url('/img/magnifying-glass-dark.svg')",
toc: "url('/img/side-menu-light.svg')",
"toc-dark": "url('/img/side-menu.svg')",
},
},
plugins: [
plugin(({ addVariant, addUtilities, addComponents, theme }) => {
addVariant("search-cancel", "&::-webkit-search-cancel-button")
addUtilities({
".animation-fill-forwards": {
animationFillMode: "forwards",
},
".animate-fast": {
animationDuration: "300ms",
},
})
addComponents({
".btn-secondary-icon": {
padding: "4px !important",
},
"btn-clear": {
backgroundColor: "transparent",
boxShadow: theme("shadow.none"),
borderWidth: 0,
borderColor: "transparent",
outlineColor: "transparent",
cursor: "pointer",
},
})
}),
],
}
@@ -0,0 +1,33 @@
// This config file includes the base configurations with the modified configs for UI docs
import coreConfig from "./base.tailwind.config"
// modify core spacing to have "docs" prefix
const modifiedSpacing = {}
Object.entries(coreConfig.theme.spacing).forEach(([key, value]) => {
modifiedSpacing[`docs_${key}`] = value
})
// modify core border radius to have "docs" prefix
const modifiedRadius = {}
Object.entries(coreConfig.theme.extend.borderRadius).forEach(([key, value]) => {
modifiedRadius[`docs_${key}`] = value
})
/** @type {import('tailwindcss').Config} */
module.exports = {
...coreConfig,
theme: {
...coreConfig.theme,
extend: {
...coreConfig.theme.extend,
borderRadius: {
...coreConfig.theme.extend.borderRadius,
...modifiedRadius,
},
},
spacing: {
...coreConfig.theme.spacing,
...modifiedSpacing,
},
},
}
+24
View File
@@ -0,0 +1,24 @@
{
"name": "tailwind",
"version": "0.0.0",
"private": true,
"license": "MIT",
"main": "tailwind.config.js",
"devDependencies": {
"eslint-config-docs": "*"
},
"publishConfig": {
"access": "public"
},
"files": [
"tailwind.config.js",
"postcss.config.js"
],
"dependencies": {
"@medusajs/ui-preset": "^1.0.0",
"tailwindcss-animate": "^1.0.7"
},
"peerDependencies": {
"docs-ui": "*"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+12
View File
@@ -0,0 +1,12 @@
import path from "path"
import coreConfig from "./modified.tailwind.config"
// Get two levels up from require.resolve("@medusajs/ui")
const root = path.join(require.resolve("docs-ui"), "../..")
const uiPath = path.join(root, "**/*.{js,ts,jsx,tsx,mdx}")
/** @type {import('tailwindcss').Config} */
module.exports = {
...coreConfig,
content: [uiPath],
}
+29
View File
@@ -0,0 +1,29 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"downlevelIteration": true
},
"exclude": [
"node_modules",
"public"
]
}
+15
View File
@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Docusaurus",
"extends": "@tsconfig/docusaurus/tsconfig.json",
"compilerOptions": {
"jsx": "react",
"lib": [
"es2022",
"dom",
"dom.iterable"
],
"allowJs": true,
"module": "Node16"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Next.js",
"extends": "./base.json",
"compilerOptions": {
"plugins": [
{
"name": "next"
}
],
"jsx": "react-jsx"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "tsconfig",
"version": "0.0.0",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@tsconfig/docusaurus": "^1.0.7"
}
}