Files
shadcn-ui/apps/v4/app/(create)/components/item-picker.tsx
shadcn 86d9b00084 chore: update deps (#9022)
* feat: init

* fix

* fix

* fix

* feat

* feat

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: implement icons

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: update init command

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: dialog

* feat

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: add registry:base item type

* feat: rename frame to canva

* fix

* feat

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fi

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: add all colors

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: add outfit font

* fix

* fix

* fix

* fix

* fix

* chore: changeset

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix
2025-12-12 21:01:44 +04:00

198 lines
6.2 KiB
TypeScript

"use client"
import * as React from "react"
import Script from "next/script"
import { Search01Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { useQueryStates } from "nuqs"
import { type RegistryItem } from "shadcn/schema"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Combobox,
ComboboxCollection,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
ComboboxTrigger,
ComboboxValue,
} from "@/registry/new-york-v4/ui/combobox"
import { designSystemSearchParams } from "@/app/(create)/lib/search-params"
import { groupItemsByType } from "@/app/(create)/lib/utils"
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"
const cachedGroupedItems = React.cache(
(items: Pick<RegistryItem, "name" | "title" | "type">[]) => {
return groupItemsByType(items)
}
)
export function ItemPicker({
items,
}: {
items: Pick<RegistryItem, "name" | "title" | "type">[]
}) {
const [open, setOpen] = React.useState(false)
const [params, setParams] = useQueryStates(designSystemSearchParams, {
history: "push",
shallow: true,
})
const groupedItems = React.useMemo(() => cachedGroupedItems(items), [items])
const currentItem = React.useMemo(
() => items.find((item) => item.name === params.item) ?? null,
[items, params.item]
)
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if ((e.key === "k" || e.key === "p") && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [])
const handleSelect = React.useCallback(
(item: Pick<RegistryItem, "name" | "title" | "type">) => {
setParams({ item: item.name })
setOpen(false)
},
[setParams]
)
const comboboxValue = React.useMemo(() => {
return currentItem ?? null
}, [currentItem])
return (
<Combobox
autoHighlight
items={groupedItems}
value={comboboxValue}
onValueChange={(value) => {
if (value) {
handleSelect(value)
}
}}
open={open}
onOpenChange={setOpen}
itemToStringValue={(item) => {
if (!item) {
return ""
}
// Handle both groups and items.
if ("items" in item) {
return item.title ?? ""
}
return item.title ?? item.name ?? ""
}}
>
<ComboboxTrigger
render={
<Button
variant="outline"
aria-label="Select item"
size="sm"
className="data-popup-open:bg-muted dark:data-popup-open:bg-muted/50 bg-muted/50 sm:bg-background md:dark:bg-background border-foreground/10 dark:bg-muted/50 h-[calc(--spacing(13.5))] flex-1 touch-manipulation justify-between gap-2 rounded-xl pr-4! pl-2.5 text-left shadow-none select-none *:data-[slot=combobox-trigger-icon]:hidden sm:h-8 sm:max-w-56 sm:rounded-lg sm:pr-2! xl:max-w-md"
/>
}
>
<ComboboxValue>
{(value) => (
<>
<div className="flex flex-col justify-start text-left sm:hidden">
<div className="text-muted-foreground text-xs font-normal">
Preview
</div>
<div className="text-foreground text-sm font-medium">
{value?.title || "Not Found"}
</div>
</div>
<div className="text-foreground hidden flex-1 text-sm sm:flex">
{value?.title || "Not Found"}
</div>
</>
)}
</ComboboxValue>
<HugeiconsIcon icon={Search01Icon} />
</ComboboxTrigger>
<ComboboxContent
className="ring-foreground/10 min-w-[calc(var(--available-width)---spacing(4))] translate-x-2 animate-none rounded-xl border-0 ring-1 data-open:animate-none sm:min-w-[calc(var(--anchor-width)+--spacing(7))] sm:translate-x-0"
side="bottom"
align="center"
>
<ComboboxInput
showTrigger={false}
placeholder="Search"
className="bg-muted h-8 rounded-lg shadow-none has-focus-visible:border-inherit! has-focus-visible:ring-0! pointer-coarse:hidden"
/>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList className="no-scrollbar scroll-my-1 pb-1">
{(group) => (
<ComboboxGroup key={group.type} items={group.items}>
<ComboboxLabel>{group.title}</ComboboxLabel>
<ComboboxCollection>
{(item) => (
<ComboboxItem
key={item.name}
value={item}
className="group/combobox-item rounded-lg pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base"
>
{item.title}
<span className="text-muted-foreground ml-auto text-xs opacity-0 group-data-[selected=true]/combobox-item:opacity-100">
{group.title}
</span>
</ComboboxItem>
)}
</ComboboxCollection>
</ComboboxGroup>
)}
</ComboboxList>
</ComboboxContent>
<div
data-open={open}
className="fixed inset-0 z-50 hidden bg-transparent data-[open=true]:block"
onClick={() => setOpen(false)}
/>
</Combobox>
)
}
export function ItemPickerScript() {
return (
<Script
id="design-system-listener"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `
(function() {
// Forward Cmd/Ctrl + K and Cmd/Ctrl + P
document.addEventListener('keydown', function(e) {
if ((e.key === 'k' || e.key === 'p') && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
if (window.parent && window.parent !== window) {
window.parent.postMessage({
type: '${CMD_K_FORWARD_TYPE}',
key: e.key
}, '*');
}
}
});
})();
`,
}}
/>
)
}