snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
All checks were successful
Release / Create Version PR or Publish Stable Release (push) Has been skipped
Release / Publish Prerelease to NPM (push) Has been skipped
Monitor Registries / Monitor registry health (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 22:44:43 +05:00
commit 1416db152c
5727 changed files with 630238 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
import { useAtom } from "jotai"
import { atomWithStorage } from "jotai/utils"
import { type ColorFormat } from "@/lib/colors"
import { useMounted } from "@/hooks/use-mounted"
type Config = {
format: ColorFormat
lastCopied: string
}
const colorsAtom = atomWithStorage<Config>("colors", {
format: "hsl",
lastCopied: "",
})
export function useColors() {
const [colors, setColors] = useAtom(colorsAtom)
const mounted = useMounted()
return {
isLoading: !mounted,
format: colors.format,
lastCopied: colors.lastCopied,
setFormat: (format: ColorFormat) => setColors({ ...colors, format }),
setLastCopied: (lastCopied: string) => setColors({ ...colors, lastCopied }),
}
}

View File

@@ -0,0 +1,18 @@
import { useAtom } from "jotai"
import { atomWithStorage } from "jotai/utils"
type Config = {
style: "new-york-v4"
packageManager: "npm" | "yarn" | "pnpm" | "bun"
installationType: "cli" | "manual"
}
const configAtom = atomWithStorage<Config>("config", {
style: "new-york-v4",
packageManager: "pnpm",
installationType: "cli",
})
export function useConfig() {
return useAtom(configAtom)
}

View File

@@ -0,0 +1,80 @@
"use client"
import * as React from "react"
function legacyCopyToClipboard(value: string) {
const textArea = document.createElement("textarea")
textArea.value = value
textArea.setAttribute("readonly", "")
textArea.style.position = "fixed"
textArea.style.opacity = "0"
textArea.style.pointerEvents = "none"
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
textArea.setSelectionRange(0, value.length)
let hasCopied = false
try {
hasCopied = document.execCommand("copy")
} catch {
hasCopied = false
}
document.body.removeChild(textArea)
return hasCopied
}
export function useCopyToClipboard({
timeout = 2000,
onCopy,
}: {
timeout?: number
onCopy?: () => void
} = {}) {
const [isCopied, setIsCopied] = React.useState(false)
const copyToClipboard = async (value: string) => {
if (typeof window === "undefined") {
return false
}
if (!value) {
return false
}
let hasCopied = false
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(value)
hasCopied = true
} catch {
hasCopied = legacyCopyToClipboard(value)
}
} else {
hasCopied = legacyCopyToClipboard(value)
}
if (!hasCopied) {
return false
}
setIsCopied(true)
if (onCopy) {
onCopy()
}
if (timeout !== 0) {
setTimeout(() => {
setIsCopied(false)
}, timeout)
}
return true
}
return { isCopied, copyToClipboard }
}

View File

@@ -0,0 +1,19 @@
import * as React from "react"
import type { ChatStatus } from "ai"
export function useDelayedStatus(status: ChatStatus, delay: number) {
const [delayedStatus, setDelayedStatus] = React.useState(status)
React.useEffect(() => {
const timeout = window.setTimeout(
() => {
setDelayedStatus(status)
},
status === "submitted" ? delay : 0
)
return () => window.clearTimeout(timeout)
}, [delay, status])
return delayedStatus
}

View File

@@ -0,0 +1,163 @@
"use client"
import * as React from "react"
type Layout = "fixed" | "full"
interface LayoutProviderProps {
children: React.ReactNode
defaultLayout?: Layout
forcedLayout?: Layout
storageKey?: string
attribute?: string | string[]
value?: Record<string, string>
}
interface LayoutProviderState {
layout: Layout
setLayout: (layout: Layout | ((prev: Layout) => Layout)) => void
forcedLayout?: Layout
}
const isServer = typeof window === "undefined"
const LayoutContext = React.createContext<LayoutProviderState | undefined>(
undefined
)
const saveToLS = (storageKey: string, value: string) => {
try {
localStorage.setItem(storageKey, value)
} catch {
// Unsupported
}
}
const useLayout = () => {
const context = React.useContext(LayoutContext)
if (context === undefined) {
throw new Error("useLayout must be used within a LayoutProvider")
}
return context
}
const Layout = ({
forcedLayout,
storageKey = "layout",
defaultLayout = "full",
attribute = "class",
value,
children,
}: LayoutProviderProps) => {
const [layout, setLayoutState] = React.useState<Layout>(() => {
if (isServer) return defaultLayout
try {
const saved = localStorage.getItem(storageKey)
if (saved === "fixed" || saved === "full") {
return saved
}
return defaultLayout
} catch {
return defaultLayout
}
})
const attrs = React.useMemo(
() => (!value ? ["layout-fixed", "layout-full"] : Object.values(value)),
[value]
)
const applyLayout = React.useCallback(
(layout: Layout) => {
if (!layout) return
const name = value ? value[layout] : `layout-${layout}`
const d = document.documentElement
const handleAttribute = (attr: string) => {
if (attr === "class") {
d.classList.remove(...attrs)
if (name) d.classList.add(name)
} else if (attr.startsWith("data-")) {
if (name) {
d.setAttribute(attr, name)
} else {
d.removeAttribute(attr)
}
}
}
if (Array.isArray(attribute)) attribute.forEach(handleAttribute)
else handleAttribute(attribute)
},
[attrs, attribute, value]
)
const setLayout = React.useCallback(
(value: Layout | ((prev: Layout) => Layout)) => {
if (typeof value === "function") {
setLayoutState((prevLayout) => {
const newLayout = value(prevLayout)
saveToLS(storageKey, newLayout)
return newLayout
})
} else {
setLayoutState(value)
saveToLS(storageKey, value)
}
},
[storageKey]
)
// localStorage event handling
React.useEffect(() => {
const handleStorage = (e: StorageEvent) => {
if (e.key !== storageKey) return
if (!e.newValue) {
setLayout(defaultLayout)
} else if (e.newValue === "fixed" || e.newValue === "full") {
setLayoutState(e.newValue)
}
}
window.addEventListener("storage", handleStorage)
return () => window.removeEventListener("storage", handleStorage)
}, [setLayout, storageKey, defaultLayout])
// Apply layout on mount and when it changes
React.useEffect(() => {
const currentLayout = forcedLayout ?? layout
applyLayout(currentLayout)
}, [forcedLayout, layout, applyLayout])
// Prevent layout changes during hydration
const [isHydrated, setIsHydrated] = React.useState(false)
React.useEffect(() => {
setIsHydrated(true)
}, [])
const providerValue = React.useMemo(
() => ({
layout: isHydrated ? layout : defaultLayout,
setLayout,
forcedLayout,
}),
[layout, setLayout, forcedLayout, isHydrated, defaultLayout]
)
return (
<LayoutContext.Provider value={providerValue}>
{children}
</LayoutContext.Provider>
)
}
const LayoutProvider = (props: LayoutProviderProps) => {
const context = React.useContext(LayoutContext)
// Ignore nested context providers, just passthrough children
if (context) return <>{props.children}</>
return <Layout {...props} />
}
export { useLayout, LayoutProvider }

View File

@@ -0,0 +1,19 @@
import * as React from "react"
export function useMediaQuery(query: string) {
const [value, setValue] = React.useState(false)
React.useEffect(() => {
function onChange(event: MediaQueryListEvent) {
setValue(event.matches)
}
const result = matchMedia(query)
result.addEventListener("change", onChange)
setValue(result.matches)
return () => result.removeEventListener("change", onChange)
}, [query])
return value
}

View File

@@ -0,0 +1,28 @@
import * as React from "react"
import { useTheme } from "next-themes"
export const META_THEME_COLORS = {
light: "#ffffff",
dark: "#0a0a0a",
}
export function useMetaColor() {
const { resolvedTheme } = useTheme()
const metaColor = React.useMemo(() => {
return resolvedTheme !== "dark"
? META_THEME_COLORS.light
: META_THEME_COLORS.dark
}, [resolvedTheme])
const setMetaColor = React.useCallback((color: string) => {
document
.querySelector('meta[name="theme-color"]')
?.setAttribute("content", color)
}, [])
return {
metaColor,
setMetaColor,
}
}

View File

@@ -0,0 +1,17 @@
import * as React from "react"
export function useIsMobile(mobileBreakpoint = 768) {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${mobileBreakpoint - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < mobileBreakpoint)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < mobileBreakpoint)
return () => mql.removeEventListener("change", onChange)
}, [mobileBreakpoint])
return !!isMobile
}

View File

@@ -0,0 +1,11 @@
import * as React from "react"
export function useMounted() {
const [mounted, setMounted] = React.useState(false)
React.useEffect(() => {
setMounted(true)
}, [])
return mounted
}

View File

@@ -0,0 +1,20 @@
import * as React from "react"
export const useMutationObserver = (
ref: React.RefObject<HTMLElement | null>,
callback: MutationCallback,
options: MutationObserverInit = {
attributes: true,
characterData: true,
childList: true,
subtree: true,
}
) => {
React.useEffect(() => {
if (ref.current) {
const observer = new MutationObserver(callback)
observer.observe(ref.current, options)
return () => observer.disconnect()
}
}, [ref, callback, options])
}

View File

@@ -0,0 +1,71 @@
import { debounce, parseAsInteger, useQueryState } from "nuqs"
import { useMounted } from "@/hooks/use-mounted"
import globalRegistries from "@/registry/directory.json"
const PAGE_SIZE = 10
const normalizeQuery = (query: string) =>
query.toLowerCase().replaceAll(" ", "").replaceAll("@", "")
function finderFn<T extends (typeof globalRegistries)[0]>(
registry: T,
query: string
) {
const normalizedName = normalizeQuery(registry.name)
const normalizedDecription = normalizeQuery(registry.description)
const normalizedQuery = normalizeQuery(query)
return (
normalizedName.includes(normalizedQuery) ||
normalizedDecription.includes(normalizedQuery)
)
}
const searchDirectory = (query: string | null) => {
if (!query) return globalRegistries
return globalRegistries.filter((registry) => finderFn(registry, query))
}
export function useSearchRegistry() {
const mounted = useMounted()
const [query, setQuery] = useQueryState("q", {
defaultValue: "",
limitUrlUpdates: debounce(250),
})
const [page, setPage] = useQueryState("page", {
...parseAsInteger,
defaultValue: 1,
history: "push",
})
const currentQuery = mounted ? query : ""
const currentPageValue = mounted ? page : 1
const registries = searchDirectory(currentQuery)
const totalPages = Math.ceil(registries.length / PAGE_SIZE)
// Clamp page to valid range.
const currentPage = Math.max(1, Math.min(currentPageValue, totalPages))
const paginatedRegistries = registries.slice(
(currentPage - 1) * PAGE_SIZE,
currentPage * PAGE_SIZE
)
return {
isLoading: !mounted,
query: currentQuery,
setQuery: (value: string | null) => {
setQuery(value)
setPage(null)
},
registries,
paginatedRegistries,
page: currentPage,
totalPages,
setPage,
}
}