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:
1 parent
19c5d5ba36
commit
fa7c94b4cc
3209 files changed
+32188
-31018
No files matched your search
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
"docs"
|
||||
],
|
||||
parserOptions: {
|
||||
project: true,
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
export const GITHUB_ISSUES_PREFIX = `https://github.com/medusajs/medusa/issues/new?assignees=&labels=type%3A+docs&template=docs.yml`
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from "./constants"
|
||||
|
||||
export * from "./components"
|
||||
export * from "./hooks"
|
||||
export * from "./providers"
|
||||
export * from "./types"
|
||||
export * from "./utils"
|
||||
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"
|
||||
@@ -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)}`
|
||||
}
|
||||
@@ -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"
|
||||
@@ -0,0 +1,7 @@
|
||||
import coreConfig from "tailwind/modified.tailwind.config"
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
...coreConfig,
|
||||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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": "*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -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],
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"display": "Next.js",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "tsconfig",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tsconfig/docusaurus": "^1.0.7"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user