mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
F: Improve shadcn Select integration
This commit is contained in:
parent
95e66fb836
commit
c1d84dc490
|
@ -3,6 +3,7 @@ import clsx from 'clsx';
|
|||
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/icons';
|
||||
import { useWindowSize } from '@/hooks/use-window-size';
|
||||
import { useAppLayoutStore } from '@/stores/app-layout';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
import { urls } from '../urls';
|
||||
|
||||
|
@ -16,6 +17,7 @@ export function Navigation() {
|
|||
const { push } = useConceptNavigation();
|
||||
const size = useWindowSize();
|
||||
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
||||
const activeDialog = useDialogsStore(state => state.active);
|
||||
|
||||
const navigateHome = (event: React.MouseEvent<Element>) =>
|
||||
push({ path: urls.home, newTab: event.ctrlKey || event.metaKey });
|
||||
|
@ -27,7 +29,7 @@ export function Navigation() {
|
|||
push({ path: urls.create_schema, newTab: event.ctrlKey || event.metaKey });
|
||||
|
||||
return (
|
||||
<nav className='z-navigation sticky top-0 left-0 right-0 select-none bg-prim-100'>
|
||||
<nav className='z-navigation sticky top-0 left-0 right-0 select-none bg-prim-100' inert={activeDialog !== null}>
|
||||
<ToggleNavigation />
|
||||
<div
|
||||
className={clsx(
|
||||
|
|
|
@ -20,8 +20,10 @@ function SelectTrigger({
|
|||
className,
|
||||
size = 'default',
|
||||
children,
|
||||
noBorder,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
noBorder?: boolean;
|
||||
size?: 'sm' | 'default';
|
||||
}) {
|
||||
return (
|
||||
|
@ -29,7 +31,16 @@ function SelectTrigger({
|
|||
data-slot='select-trigger'
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"bg-input border cursor-pointer data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 aria-invalid:border-destructive disabled:cursor-auto flex w-fit items-center justify-between gap-2 px-3 py-2 whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
'data-[size=default]:h-9 data-[size=sm]:h-8',
|
||||
'flex items-center justify-between gap-2 px-3 py-2',
|
||||
'bg-input cursor-pointer disabled:cursor-auto disabled:opacity-50',
|
||||
'data-[placeholder]:text-muted-foreground',
|
||||
'whitespace-nowrap',
|
||||
'outline-none focus-visible:ring-[2px] focus-visible:border-ring focus-visible:ring-ring aria-invalid:ring-destructive',
|
||||
'*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2',
|
||||
"[&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
!noBorder && 'border aria-invalid:border-destructive',
|
||||
noBorder && 'rounded-md',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
@ -53,7 +64,11 @@ function SelectContent({
|
|||
<SelectPrimitive.Content
|
||||
data-slot='select-content'
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto border shadow-md',
|
||||
'z-topmost relative max-h-(--radix-select-content-available-height) min-w-32',
|
||||
'bg-popover text-sm text-popover-foreground',
|
||||
'border shadow-md',
|
||||
'overflow-x-hidden overflow-y-auto',
|
||||
'cc-select-popover',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className
|
||||
|
@ -92,7 +107,12 @@ function SelectItem({ className, children, ...props }: React.ComponentProps<type
|
|||
<SelectPrimitive.Item
|
||||
data-slot='select-item'
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
'relative',
|
||||
'flex py-1 pr-8 pl-2 items-center gap-2',
|
||||
'cursor-default rounded-sm select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'outline-none focus:bg-accent focus:text-accent-foreground',
|
||||
'*:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
|
||||
"[&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { SelectSingle } from '@/components/input';
|
||||
import { type Styling } from '@/components/props';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
|
||||
import { type ILibraryItem } from '../backend/types';
|
||||
import { matchLibraryItem } from '../models/library-api';
|
||||
|
||||
interface SelectLibraryItemProps extends Styling {
|
||||
id?: string;
|
||||
value: ILibraryItem | null;
|
||||
onChange: (newValue: ILibraryItem | null) => void;
|
||||
|
||||
|
@ -18,33 +16,30 @@ interface SelectLibraryItemProps extends Styling {
|
|||
}
|
||||
|
||||
export function SelectLibraryItem({
|
||||
className,
|
||||
id,
|
||||
items,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Выберите схему',
|
||||
...restProps
|
||||
}: SelectLibraryItemProps) {
|
||||
const options =
|
||||
items?.map(cst => ({
|
||||
value: cst.id,
|
||||
label: `${cst.alias}: ${cst.title}`
|
||||
})) ?? [];
|
||||
|
||||
function filter(option: { value: string | undefined; label: string }, query: string) {
|
||||
const item = items?.find(item => item.id === Number(option.value));
|
||||
return !item ? false : matchLibraryItem(item, query);
|
||||
function handleSelect(newValue: string) {
|
||||
const newItem = items?.find(item => item.id === Number(newValue)) ?? null;
|
||||
onChange(newItem);
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectSingle
|
||||
className={clsx('text-ellipsis', className)}
|
||||
options={options}
|
||||
value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : null}
|
||||
onChange={data => onChange(items?.find(cst => cst.id === data?.value) ?? null)}
|
||||
filterOption={filter}
|
||||
placeholder={placeholder}
|
||||
{...restProps}
|
||||
/>
|
||||
<Select onValueChange={handleSelect} defaultValue={value ? String(value.id) : undefined}>
|
||||
<SelectTrigger id={id} {...restProps}>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className='max-w-80'>
|
||||
{items?.map(item => (
|
||||
<SelectItem key={`${id ?? 'default'}-item-select-${item.id}`} value={String(item.id)}>
|
||||
{`${item.alias}: ${item.title}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,15 @@ interface SelectVersionProps extends Styling {
|
|||
noBorder?: boolean;
|
||||
}
|
||||
|
||||
export function SelectVersion({ id, className, items, value, placeholder, onChange }: SelectVersionProps) {
|
||||
export function SelectVersion({
|
||||
id,
|
||||
className,
|
||||
items,
|
||||
value,
|
||||
placeholder,
|
||||
onChange,
|
||||
...restProps
|
||||
}: SelectVersionProps) {
|
||||
function handleSelect(newValue: string) {
|
||||
if (newValue === 'latest') {
|
||||
onChange(newValue);
|
||||
|
@ -26,10 +34,9 @@ export function SelectVersion({ id, className, items, value, placeholder, onChan
|
|||
onChange(Number(newValue));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Select onValueChange={handleSelect} defaultValue={String(value)}>
|
||||
<SelectTrigger id={id} className={cn('min-w-48', className)}>
|
||||
<SelectTrigger id={id} className={cn('min-w-48', className)} {...restProps}>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
|
@ -231,8 +231,9 @@ export function PickSubstitutions({
|
|||
return (
|
||||
<div className={clsx('flex flex-col', className)} {...restProps}>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='grow flex flex-col basis-1/2 gap-1 border-x border-t clr-input rounded-t-md'>
|
||||
<div className='w-60 grow flex flex-col basis-1/2 gap-1 border-x border-t clr-input rounded-t-md'>
|
||||
<SelectLibraryItem
|
||||
id='substitute-left-schema'
|
||||
noBorder
|
||||
placeholder='Выберите аргумент'
|
||||
items={allowSelfSubstitution ? schemas : schemas.filter(item => item.id !== rightArgument?.id)}
|
||||
|
@ -262,8 +263,9 @@ export function PickSubstitutions({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className='grow basis-1/2 flex flex-col gap-1 border-x border-t clr-input rounded-t-md'>
|
||||
<div className='w-60 grow basis-1/2 flex flex-col gap-1 border-x border-t clr-input rounded-t-md'>
|
||||
<SelectLibraryItem
|
||||
id='substitute-right-schema'
|
||||
noBorder
|
||||
placeholder='Выберите аргумент'
|
||||
items={allowSelfSubstitution ? schemas : schemas.filter(item => item.id !== leftArgument?.id)}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { SelectSingle } from '@/components/input';
|
||||
import { type Styling } from '@/components/props';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { CstType } from '../backend/types';
|
||||
import { labelCstType } from '../labels';
|
||||
|
||||
const SelectorCstType = Object.values(CstType).map(typeStr => ({
|
||||
value: typeStr as CstType,
|
||||
label: labelCstType(typeStr as CstType)
|
||||
}));
|
||||
import { IconCstType } from './icon-cst-type';
|
||||
|
||||
interface SelectCstTypeProps extends Styling {
|
||||
id?: string;
|
||||
|
@ -16,16 +14,20 @@ interface SelectCstTypeProps extends Styling {
|
|||
onChange: (newValue: CstType) => void;
|
||||
}
|
||||
|
||||
export function SelectCstType({ value, onChange, disabled = false, ...restProps }: SelectCstTypeProps) {
|
||||
export function SelectCstType({ id, value, onChange, className, disabled = false, ...restProps }: SelectCstTypeProps) {
|
||||
return (
|
||||
<SelectSingle
|
||||
id='dlg_cst_type'
|
||||
placeholder='Выберите тип'
|
||||
options={SelectorCstType}
|
||||
value={{ value: value, label: labelCstType(value) }}
|
||||
onChange={data => onChange(data?.value ?? CstType.BASE)}
|
||||
isDisabled={disabled}
|
||||
{...restProps}
|
||||
/>
|
||||
<Select onValueChange={onChange} defaultValue={value} disabled={disabled}>
|
||||
<SelectTrigger id={id} className={cn('w-66', className)} {...restProps}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.values(CstType).map(typeStr => (
|
||||
<SelectItem key={`csttype-${typeStr}`} value={typeStr}>
|
||||
<IconCstType value={typeStr as CstType} />
|
||||
<span>{labelCstType(typeStr as CstType)}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -45,7 +45,6 @@ export function FormCreateCst({ schema }: FormCreateCstProps) {
|
|||
<div className='flex items-center self-center gap-3'>
|
||||
<SelectCstType
|
||||
id='dlg_cst_type' //
|
||||
className='w-64'
|
||||
value={cst_type}
|
||||
onChange={handleTypeChange}
|
||||
/>
|
||||
|
|
|
@ -56,8 +56,7 @@ export function DlgRenameCst() {
|
|||
helpTopic={HelpTopic.CC_CONSTITUENTA}
|
||||
>
|
||||
<SelectCstType
|
||||
id='dlg_cst_type'
|
||||
className='w-64'
|
||||
id='dlg_cst_type' //
|
||||
value={cst_type}
|
||||
onChange={handleChangeType}
|
||||
disabled={target.is_inherited}
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
import { HelpTopic } from '@/features/help';
|
||||
import { BadgeHelp } from '@/features/help/components';
|
||||
|
||||
import { SelectSingle } from '@/components/input';
|
||||
|
||||
import { mapLabelColoring } from '../../../labels';
|
||||
import { type GraphColoring, useTermGraphStore } from '../../../stores/term-graph';
|
||||
|
||||
import { SchemasGuide } from './schemas-guide';
|
||||
|
||||
/**
|
||||
* Represents options for {@link GraphColoring} selector.
|
||||
*/
|
||||
const SelectorGraphColoring: { value: GraphColoring; label: string }[] = //
|
||||
[...mapLabelColoring.entries()].map(item => ({ value: item[0], label: item[1] }));
|
||||
|
||||
export function GraphSelectors() {
|
||||
const coloring = useTermGraphStore(state => state.coloring);
|
||||
const setColoring = useTermGraphStore(state => state.setColoring);
|
||||
|
||||
return (
|
||||
<div className='relative border rounded-b-none select-none clr-input rounded-t-md pointer-events-auto'>
|
||||
<div className='absolute z-pop right-10 h-10 flex items-center'>
|
||||
{coloring === 'status' ? <BadgeHelp topic={HelpTopic.UI_CST_STATUS} contentClass='min-w-100' /> : null}
|
||||
{coloring === 'type' ? <BadgeHelp topic={HelpTopic.UI_CST_CLASS} contentClass='min-w-100' /> : null}
|
||||
{coloring === 'schemas' ? <SchemasGuide /> : null}
|
||||
</div>
|
||||
<SelectSingle
|
||||
noBorder
|
||||
placeholder='Цветовая схема'
|
||||
options={SelectorGraphColoring}
|
||||
isSearchable={false}
|
||||
value={coloring ? { value: coloring, label: mapLabelColoring.get(coloring) } : null}
|
||||
onChange={data => setColoring(data?.value ?? SelectorGraphColoring[0].value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -36,7 +36,7 @@ export function SchemasGuide() {
|
|||
})();
|
||||
|
||||
return (
|
||||
<div tabIndex={-1} id={globalIDs.graph_schemas}>
|
||||
<div className='p-1' tabIndex={-1} id={globalIDs.graph_schemas}>
|
||||
<IconHelp size='1.25rem' className='icon-primary' />
|
||||
<Tooltip anchorSelect={`#${globalIDs.graph_schemas}`} place='right' className='max-w-100 break-words text-base'>
|
||||
<div className='inline-flex items-center gap-2'>
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import { HelpTopic } from '@/features/help';
|
||||
import { BadgeHelp } from '@/features/help/components';
|
||||
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
|
||||
import { mapLabelColoring } from '../../../labels';
|
||||
import { useTermGraphStore } from '../../../stores/term-graph';
|
||||
|
||||
import { SchemasGuide } from './schemas-guide';
|
||||
|
||||
export function SelectColoring() {
|
||||
const coloring = useTermGraphStore(state => state.coloring);
|
||||
const setColoring = useTermGraphStore(state => state.setColoring);
|
||||
|
||||
return (
|
||||
<div className='relative border rounded-b-none select-none clr-input rounded-t-md pointer-events-auto'>
|
||||
<div className='absolute z-pop right-10 h-9 flex items-center'>
|
||||
{coloring === 'status' ? <BadgeHelp topic={HelpTopic.UI_CST_STATUS} contentClass='min-w-100' /> : null}
|
||||
{coloring === 'type' ? <BadgeHelp topic={HelpTopic.UI_CST_CLASS} contentClass='min-w-100' /> : null}
|
||||
{coloring === 'schemas' ? <SchemasGuide /> : null}
|
||||
</div>
|
||||
|
||||
<Select onValueChange={setColoring} defaultValue={coloring}>
|
||||
<SelectTrigger noBorder className='w-full'>
|
||||
<SelectValue placeholder='Цветовая схема' />
|
||||
</SelectTrigger>
|
||||
<SelectContent alignOffset={-1} sideOffset={-4}>
|
||||
{[...mapLabelColoring.entries()].map(item => (
|
||||
<SelectItem key={`coloring-${item[0]}`} value={item[0]}>
|
||||
{item[1]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -26,7 +26,7 @@ import { useRSEdit } from '../rsedit-context';
|
|||
import { TGEdgeTypes } from './graph/tg-edge-types';
|
||||
import { applyLayout } from './graph/tg-layout';
|
||||
import { TGNodeTypes } from './graph/tg-node-types';
|
||||
import { GraphSelectors } from './graph-selectors';
|
||||
import { SelectColoring } from './select-coloring';
|
||||
import { ToolbarFocusedCst } from './toolbar-focused-cst';
|
||||
import { ToolbarTermGraph } from './toolbar-term-graph';
|
||||
import { useFilteredGraph } from './use-filtered-graph';
|
||||
|
@ -184,7 +184,7 @@ export function TGFlow() {
|
|||
<span className='px-2 pb-1 select-none whitespace-nowrap backdrop-blur-xs rounded-xl'>
|
||||
Выбор {selected.length} из {schema.stats?.count_all ?? 0}
|
||||
</span>
|
||||
<GraphSelectors />
|
||||
<SelectColoring />
|
||||
<ViewHidden items={hidden} />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -47,11 +47,11 @@ export function ViewHidden({ items }: ViewHiddenProps) {
|
|||
return (
|
||||
<div className='grid relative'>
|
||||
<MiniButton
|
||||
className='absolute right-[calc(0.75rem-2px)] top-2 pointer-events-auto'
|
||||
className='absolute right-[calc(1rem-4px)] top-3 pointer-events-auto'
|
||||
noPadding
|
||||
noHover
|
||||
title={!isFolded ? 'Свернуть' : 'Развернуть'}
|
||||
icon={!isFolded ? <IconDropArrowUp size='1.25rem' /> : <IconDropArrow size='1.25rem' />}
|
||||
icon={!isFolded ? <IconDropArrowUp size='1rem' /> : <IconDropArrow size='1rem' />}
|
||||
onClick={toggleFolded}
|
||||
/>
|
||||
|
||||
|
|
|
@ -129,7 +129,7 @@
|
|||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--clr-prim-100);
|
||||
--color-muted-foreground: var(--clr-prim-600);
|
||||
--color-muted-foreground: var(--clr-prim-800);
|
||||
--color-accent: var(--clr-sec-100);
|
||||
--color-accent-foreground: var(--clr-prim-999);
|
||||
--color-destructive: var(--clr-warn-600);
|
||||
|
|
|
@ -227,3 +227,35 @@
|
|||
clip-path: inset(0% 0% 0% 0%);
|
||||
}
|
||||
}
|
||||
|
||||
@utility cc-select-popover {
|
||||
transform-origin: var(--radix-select-content-transform-origin);
|
||||
|
||||
&[data-state='open'] {
|
||||
--tw-enter-opacity: 0;
|
||||
--tw-enter-scale: 0.95;
|
||||
animation: enter var(--tw-duration, 0.15s) var(--tw-ease, ease);
|
||||
}
|
||||
|
||||
&[data-state='closed'] {
|
||||
--tw-exit-opacity: 0;
|
||||
--tw-exit-scale: 0.95;
|
||||
animation: exit var(--tw-duration, 0.15s) var(--tw-ease, ease);
|
||||
}
|
||||
|
||||
&[data-side='bottom'] {
|
||||
--tw-enter-translate-y: calc(-2 * var(--spacing));
|
||||
}
|
||||
|
||||
&[data-side='left'] {
|
||||
--tw-enter-translate-x: calc(2 * var(--spacing));
|
||||
}
|
||||
|
||||
&[data-side='right'] {
|
||||
--tw-enter-translate-x: calc(-2 * var(--spacing));
|
||||
}
|
||||
|
||||
&[data-side='top'] {
|
||||
--tw-enter-translate-y: calc(2 * var(--spacing));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user