mirror of
https://github.com/logseq/logseq.git
synced 2026-05-18 09:52:22 +00:00
* enhance(plugin): call apis with the sdk ns * enhance(plugin): types * enhance(api): get value from the computed style * enhance(api): types * enhance(plugin): types * enhance(plugin): types * fix: lint * fix(apis): incorrect shortcut command registion for block editing mode #10392 * fix(api): types * enhance(apis): support register shortcuts with multi binding vals * fix(plugins): normalize command key to make the internal keyword legal * chore(plugin): build libs core * chore(plugin): bump version * enhance(apis): normalize apis cljs data * chore(plugin): update libs user sdk * chore(plugin): CHANGELOG.md * fix: typo * feat(ui): add package * Update .gitignore * feat(ui): set up shui infrastructure * feat(ui): add storybook macro * enhance(ui): storybook themes * feat(ui): adapt ui button to classic * enhance(ui): shui story * feat(ui): shui toaster * enhance(ui): shui toaster * feat(ui): imperative API for shui toaster * enhance(shui): update API for shui toaster * enhance(shui): update hooks for shui toaster * enhance(shui): remove debug * feat(ui): story for the shui toaster * feat(ui): story * feat(ui): story docs * feat(ui): more variants for the shui toaster * feat(ui): story * fix(ux): support querying plugins with right space chars * feat(ui): add shui `Alert` component * enhance(ui): shui demo * feat(ui): add logseq UI readme * enhance(ui): default shui theme * feat(ui): add shui `Badge` component & demo * fix(ui): outline theme for shui button * feat(ui): custom icon for the toaster item * feat(ui): add shui dropdown & demo * feat(ui): WIP shui form related components * feat(ui): WIP shui form-related components * feat(ui): WIP shui form * feat(ui): WIP shui form state for validation * fix(ui): missing rounded for ui button * feat(ui): add yup for shui form as default validation resolver * enhance(ui): simplify validation schema input for the shui form * fix(ui): accent ring color for input * feat(ui): add shui switch * feat(ui): add shui checkbox & switch * feat(ui): add shui radio group * fix(ui): missing file * feat(ui): add Textarea component * feat(ui): add shui card & skeleton * feat(ui): add shui context menu component & demo * fix(ui): accent color for the context menu item * feat(ui): add shui select component & demo * enhance(ui): ui css priority * feat(ui): add shui calendar & ui details * feat(ui): add shui popover * feat(ui): add date picker & demo * feat(ui): add shui dialog * feat(ui): WIP add shui dialog * feat(ui): WIP shui dialog as modal * feat(ui): WIP imperative APIs for the shui modal * feat(ui): imperative APIs for the shui modal/alert * feat(ui): support imperative API alert!/confirm! return promise * feat(ui): simplify shui components resources * feat(ui): response layout for the demo ui page * feat(ui): simplify colors * feat(ui): simplify colors * feat(ui): simplify colors * refactor(ui): WIP Adapt to the new button component * refactor(ui): polish new button & colors * fix(ui): the new theme color for the plugin settings nav item link * fix(ui): blockquote colors * enhance(ui): more custom colors for shui button * feat(ui): WIP make logseq green as a theme color * enhance(ui): polish logseq classical theme color * fix(ui): theme details of all pages * enhance(ui): polish logseq theme color for dark mode * fix(ui): missing table style * refactor(ui): simplify the all shui buttons & shortcuts for the cmdk component * fix(ui): missing file * refactor(ui): clear up stuff * fix(ui): theme color related issues * enhance(ui): polish button style * enhance(ui): polish the keymap setting pane * fix(ui): hint button from the cmdk pane footer * fix(ui): logseq colors for the storybook * enhance(ui): stories for the shui components * fix(ui): active color for the old toggle component * enhance(ui): keep the constant size of the settings pane * fix(ui): polish search input for the plugins pane * enhance(ui): polish number list bullet colors * feat(ui): add shui tooltip component * chore: build ui * chore(ui): clean up resources * fix: lint * fix: lint * fix: lint * fix(ui): alignment of the keymap title from the settings pane * fix: tests * fix(ui): close button for the classic notification tip * fix(ui): polish toaster viewport * enhance(ui): polish the ghost button colors * enhance(ui): demos for tips * fix(ui): accent colors for the rc-datepicker * fix(ui): accent color for the menu item * refactor(ui): remove unless code for the accent colors * enhance(ui): polish pdf viewer background color for the accent color mode * fix: lint * fix: lint * fix: lint * enhance(ui): support button with the custom href link * enhance(ui): polish aside setting items * enhance(ui): polish accent color for buttons * enhance(ui): polish all pages --------- Co-authored-by: Gabriel Horner <97210743+logseq-cldwalker@users.noreply.github.com>
162 lines
5.1 KiB
TypeScript
162 lines
5.1 KiB
TypeScript
import * as React from 'react'
|
|
import { cn } from '@/lib/utils'
|
|
import { Check, ChevronsUpDown, X } from 'lucide-react'
|
|
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
} from '@/components/ui/command'
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from '@/components/ui/popover'
|
|
|
|
export type OptionType = Record<'value' | 'label', string>
|
|
|
|
interface MultiSelectProps {
|
|
options: Record<'value' | 'label', string>[]
|
|
selected: Record<'value' | 'label', string>[]
|
|
onChange: React.Dispatch<
|
|
React.SetStateAction<Record<'value' | 'label', string>[]>
|
|
>
|
|
className?: string
|
|
placeholder?: string
|
|
}
|
|
|
|
const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>(
|
|
({ options, selected, onChange, className, ...props }, ref) => {
|
|
const [open, setOpen] = React.useState(false)
|
|
const [query, setQuery] = React.useState<string>('')
|
|
|
|
const handleUnselect = (item: Record<'value' | 'label', string>) => {
|
|
onChange(selected.filter((i) => i.value !== item.value))
|
|
}
|
|
|
|
// on delete key press, remove last selected item
|
|
React.useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === 'Backspace' && query === '' && selected.length > 0) {
|
|
onChange(selected.filter((_, index) => index !== selected.length - 1))
|
|
}
|
|
|
|
// close on escape
|
|
if (e.key === 'Escape') {
|
|
setOpen(false)
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', handleKeyDown)
|
|
|
|
return () => {
|
|
document.removeEventListener('keydown', handleKeyDown)
|
|
}
|
|
}, [onChange, query, selected])
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild className={className}>
|
|
<Button
|
|
ref={ref}
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={open}
|
|
className={`group w-full justify-between ${
|
|
selected.length > 1 ? 'h-full' : 'h-9'
|
|
}`}
|
|
onClick={() => setOpen(!open)}
|
|
>
|
|
<div className="flex flex-wrap items-center gap-1">
|
|
{selected.map((item) => (
|
|
<Badge
|
|
variant="outline"
|
|
key={item.value}
|
|
className="flex items-center gap-1 group-hover:bg-background"
|
|
>
|
|
{item.label}
|
|
{open && (
|
|
<Button
|
|
asChild
|
|
variant="outline"
|
|
size="icon"
|
|
className={`border-none duration-300 ${
|
|
open ? 'opacity-100 ease-in' : 'opacity-0 ease-out'
|
|
}`}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
handleUnselect(item)
|
|
}
|
|
}}
|
|
onMouseDown={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
}}
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
handleUnselect(item)
|
|
}}
|
|
>
|
|
<X className="h-3 w-3 text-muted-foreground hover:text-foreground"/>
|
|
</Button>
|
|
)}
|
|
</Badge>
|
|
))}
|
|
{selected.length === 0 && (
|
|
<span>{props.placeholder ?? 'Select ...'}</span>
|
|
)}
|
|
</div>
|
|
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50"/>
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-full p-0">
|
|
<Command className={className}>
|
|
<CommandInput
|
|
onValueChange={(item) => {
|
|
console.log('dd', item)
|
|
setQuery(item)
|
|
}}
|
|
placeholder="Search ..."
|
|
/>
|
|
<CommandEmpty>No item found.</CommandEmpty>
|
|
<CommandGroup className="max-h-64 overflow-auto">
|
|
{options.map((option) => (
|
|
<CommandItem
|
|
key={option.value}
|
|
onSelect={() => {
|
|
onChange(
|
|
selected.some((item) => item.value === option.value)
|
|
? selected.filter((item) => item.value !== option.value)
|
|
: [...selected, option]
|
|
)
|
|
setOpen(true)
|
|
}}
|
|
>
|
|
<Check
|
|
className={cn(
|
|
'mr-2 h-4 w-4',
|
|
selected.some((item) => item.value === option.value)
|
|
? 'opacity-100'
|
|
: 'opacity-0'
|
|
)}
|
|
/>
|
|
{option.label}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
}
|
|
)
|
|
|
|
MultiSelect.displayName = 'MultiSelect'
|
|
|
|
export { MultiSelect }
|