New features: - Multi-profile management (create, switch, rename, delete) - Knowledge browser with document viewer - MCP server settings screen - Skills hub with marketplace search fallback - Context usage tracking and display Improvements: - eslint added and auto-fixed (69 issues resolved) - Settings dialog restructured (Agent, Smart Routing, Voice, Display sections) - Navigation updated with Profiles tab across desktop/mobile - Security contact updated to GitHub advisories + X DM - .gitignore hardened (.runtime/, internal dev docs) - Version bumped to 1.0.0 Build: clean | TypeScript: 0 errors | Tests: 4/4 passing
332 lines
10 KiB
TypeScript
332 lines
10 KiB
TypeScript
'use client'
|
|
|
|
import { Autocomplete as AutocompletePrimitive } from '@base-ui/react/autocomplete'
|
|
import { HugeiconsIcon } from '@hugeicons/react'
|
|
import { ArrowUpDownIcon, Cancel01Icon } from '@hugeicons/core-free-icons'
|
|
|
|
import { cn } from '@/lib/utils'
|
|
import { Input } from '@/components/ui/input'
|
|
import {
|
|
ScrollAreaRoot,
|
|
ScrollAreaScrollbar,
|
|
ScrollAreaThumb,
|
|
ScrollAreaViewport,
|
|
} from '@/components/ui/scroll-area'
|
|
|
|
const Autocomplete = AutocompletePrimitive.Root
|
|
|
|
function AutocompleteInput({
|
|
className,
|
|
showTrigger = false,
|
|
showClear = false,
|
|
startAddon,
|
|
size,
|
|
...props
|
|
}: Omit<AutocompletePrimitive.Input.Props, 'size'> & {
|
|
showTrigger?: boolean
|
|
showClear?: boolean
|
|
startAddon?: React.ReactNode
|
|
size?: 'sm' | 'default' | 'lg' | number
|
|
ref?: React.Ref<HTMLInputElement>
|
|
}) {
|
|
const sizeValue = size ?? 'default'
|
|
|
|
return (
|
|
<div
|
|
className="relative not-has-[>*.w-full]:w-fit w-full has-disabled:opacity-64"
|
|
style={{ color: 'var(--theme-text)' }}
|
|
>
|
|
{startAddon && (
|
|
<div
|
|
aria-hidden="true"
|
|
className="[&_svg]:-mx-0.5 pointer-events-none absolute inset-y-0 start-px z-10 flex items-center ps-[calc(--spacing(3)-1px)] opacity-80 has-[+[data-size=sm]]:ps-[calc(--spacing(2.5)-1px)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4"
|
|
data-slot="autocomplete-start-addon"
|
|
>
|
|
{startAddon}
|
|
</div>
|
|
)}
|
|
<AutocompletePrimitive.Input
|
|
className={cn(
|
|
startAddon &&
|
|
'data-[size=sm]:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(7.5)-1px)] *:data-[slot=autocomplete-input]:ps-[calc(--spacing(8.5)-1px)] sm:data-[size=sm]:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(7)-1px)] sm:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(8)-1px)]',
|
|
sizeValue === 'sm'
|
|
? 'has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-6.5'
|
|
: 'has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-7',
|
|
className,
|
|
)}
|
|
data-slot="autocomplete-input"
|
|
render={<Input nativeInput size={sizeValue} />}
|
|
{...props}
|
|
/>
|
|
{showTrigger && (
|
|
<AutocompleteTrigger
|
|
className={cn(
|
|
"-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-colors pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
sizeValue === 'sm' ? 'end-0' : 'end-0.5',
|
|
)}
|
|
>
|
|
<HugeiconsIcon icon={ArrowUpDownIcon} size={20} strokeWidth={1.5} />
|
|
</AutocompleteTrigger>
|
|
)}
|
|
{showClear && (
|
|
<AutocompleteClear
|
|
className={cn(
|
|
"-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-colors pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
sizeValue === 'sm' ? 'end-0' : 'end-0.5',
|
|
)}
|
|
>
|
|
<HugeiconsIcon icon={Cancel01Icon} size={20} strokeWidth={1.5} />
|
|
</AutocompleteClear>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function AutocompletePopup({
|
|
className,
|
|
children,
|
|
side = 'bottom',
|
|
sideOffset = 4,
|
|
alignOffset,
|
|
align = 'start',
|
|
...props
|
|
}: AutocompletePrimitive.Popup.Props & {
|
|
align?: AutocompletePrimitive.Positioner.Props['align']
|
|
sideOffset?: AutocompletePrimitive.Positioner.Props['sideOffset']
|
|
alignOffset?: AutocompletePrimitive.Positioner.Props['alignOffset']
|
|
side?: AutocompletePrimitive.Positioner.Props['side']
|
|
}) {
|
|
return (
|
|
<AutocompletePrimitive.Portal>
|
|
<AutocompletePrimitive.Positioner
|
|
align={align}
|
|
alignOffset={alignOffset}
|
|
className="z-50 select-none"
|
|
data-slot="autocomplete-positioner"
|
|
side={side}
|
|
sideOffset={sideOffset}
|
|
>
|
|
<span
|
|
className={cn(
|
|
'relative flex max-h-full min-w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) rounded-lg transition-[scale,opacity]',
|
|
className,
|
|
)}
|
|
style={{
|
|
background: 'var(--theme-card)',
|
|
color: 'var(--theme-text)',
|
|
border: '1px solid var(--theme-border)',
|
|
boxShadow: 'var(--theme-shadow-2)',
|
|
}}
|
|
>
|
|
<AutocompletePrimitive.Popup
|
|
className="flex max-h-[min(var(--available-height),23rem)] flex-1 flex-col"
|
|
data-slot="autocomplete-popup"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</AutocompletePrimitive.Popup>
|
|
</span>
|
|
</AutocompletePrimitive.Positioner>
|
|
</AutocompletePrimitive.Portal>
|
|
)
|
|
}
|
|
|
|
function AutocompleteItem({
|
|
className,
|
|
children,
|
|
...props
|
|
}: AutocompletePrimitive.Item.Props) {
|
|
return (
|
|
<AutocompletePrimitive.Item
|
|
className={cn(
|
|
'flex min-h-8 cursor-default select-none items-center rounded-sm px-2 py-1 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-[var(--theme-card2)] data-disabled:opacity-64 sm:min-h-7 sm:text-sm',
|
|
className,
|
|
)}
|
|
data-slot="autocomplete-item"
|
|
style={{ color: 'var(--theme-text)' }}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</AutocompletePrimitive.Item>
|
|
)
|
|
}
|
|
|
|
function AutocompleteSeparator({
|
|
className,
|
|
...props
|
|
}: AutocompletePrimitive.Separator.Props) {
|
|
return (
|
|
<AutocompletePrimitive.Separator
|
|
className={cn('mx-2 my-1 h-px bg-border last:hidden', className)}
|
|
data-slot="autocomplete-separator"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function AutocompleteGroup({
|
|
className,
|
|
...props
|
|
}: AutocompletePrimitive.Group.Props) {
|
|
return (
|
|
<AutocompletePrimitive.Group
|
|
className={cn('[[role=group]+&]:mt-1.5', className)}
|
|
data-slot="autocomplete-group"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function AutocompleteGroupLabel({
|
|
className,
|
|
...props
|
|
}: AutocompletePrimitive.GroupLabel.Props) {
|
|
return (
|
|
<AutocompletePrimitive.GroupLabel
|
|
className={cn('px-2 py-1.5 font-medium text-xs', className)}
|
|
data-slot="autocomplete-group-label"
|
|
style={{ color: 'var(--theme-muted)' }}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function AutocompleteEmpty({
|
|
className,
|
|
...props
|
|
}: AutocompletePrimitive.Empty.Props) {
|
|
return (
|
|
<AutocompletePrimitive.Empty
|
|
className={cn(
|
|
'not-empty:p-2 text-center text-base sm:text-sm',
|
|
className,
|
|
)}
|
|
data-slot="autocomplete-empty"
|
|
style={{ color: 'var(--theme-muted)' }}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function AutocompleteRow({
|
|
className,
|
|
...props
|
|
}: AutocompletePrimitive.Row.Props) {
|
|
return (
|
|
<AutocompletePrimitive.Row
|
|
className={className}
|
|
data-slot="autocomplete-row"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) {
|
|
return (
|
|
<AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} />
|
|
)
|
|
}
|
|
|
|
function AutocompleteList({
|
|
className,
|
|
...props
|
|
}: AutocompletePrimitive.List.Props) {
|
|
return (
|
|
<ScrollAreaRoot>
|
|
<ScrollAreaViewport>
|
|
<AutocompletePrimitive.List
|
|
className={cn(
|
|
'not-empty:scroll-py-1 not-empty:p-1 in-data-has-overflow-y:pe-3',
|
|
className,
|
|
)}
|
|
data-slot="autocomplete-list"
|
|
{...props}
|
|
/>
|
|
</ScrollAreaViewport>
|
|
<ScrollAreaScrollbar orientation="vertical">
|
|
<ScrollAreaThumb />
|
|
</ScrollAreaScrollbar>
|
|
</ScrollAreaRoot>
|
|
)
|
|
}
|
|
|
|
function AutocompleteClear({
|
|
className,
|
|
...props
|
|
}: AutocompletePrimitive.Clear.Props) {
|
|
return (
|
|
<AutocompletePrimitive.Clear
|
|
className={cn(
|
|
"-translate-y-1/2 absolute end-0.5 top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-[color,background-color,box-shadow,opacity] pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
className,
|
|
)}
|
|
data-slot="autocomplete-clear"
|
|
{...props}
|
|
>
|
|
<HugeiconsIcon icon={Cancel01Icon} size={20} strokeWidth={1.5} />
|
|
</AutocompletePrimitive.Clear>
|
|
)
|
|
}
|
|
|
|
function AutocompleteStatus({
|
|
className,
|
|
...props
|
|
}: AutocompletePrimitive.Status.Props) {
|
|
return (
|
|
<AutocompletePrimitive.Status
|
|
className={cn(
|
|
'px-3 py-2 font-medium text-xs empty:m-0 empty:p-0',
|
|
className,
|
|
)}
|
|
data-slot="autocomplete-status"
|
|
style={{ color: 'var(--theme-muted)' }}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function AutocompleteCollection({
|
|
...props
|
|
}: AutocompletePrimitive.Collection.Props) {
|
|
return (
|
|
<AutocompletePrimitive.Collection
|
|
data-slot="autocomplete-collection"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function AutocompleteTrigger({
|
|
className,
|
|
...props
|
|
}: AutocompletePrimitive.Trigger.Props) {
|
|
return (
|
|
<AutocompletePrimitive.Trigger
|
|
className={className}
|
|
data-slot="autocomplete-trigger"
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const useAutocompleteFilter = AutocompletePrimitive.useFilter
|
|
|
|
export {
|
|
Autocomplete,
|
|
AutocompleteInput,
|
|
AutocompleteTrigger,
|
|
AutocompletePopup,
|
|
AutocompleteItem,
|
|
AutocompleteSeparator,
|
|
AutocompleteGroup,
|
|
AutocompleteGroupLabel,
|
|
AutocompleteEmpty,
|
|
AutocompleteValue,
|
|
AutocompleteList,
|
|
AutocompleteClear,
|
|
AutocompleteStatus,
|
|
AutocompleteRow,
|
|
AutocompleteCollection,
|
|
useAutocompleteFilter,
|
|
}
|