2026-09-01 21:03:04 +03:30
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import type { ReactNode } from 'react';
|
|
|
|
|
import { useTranslations } from 'next-intl';
|
2026-09-04 17:04:48 +03:30
|
|
|
import { ArrowLeft, Plus, Trash2 } from 'lucide-react';
|
2026-09-01 21:03:04 +03:30
|
|
|
import { Button } from '@/components/ui/shared/Button';
|
|
|
|
|
import {
|
|
|
|
|
ResponsiveDialogOverlay,
|
|
|
|
|
ResponsiveDialogPanel,
|
|
|
|
|
} from '@/components/ui/shared/ResponsiveDialog';
|
|
|
|
|
import type { ProsthesisCatalogEntry } from '@/types/treatment-catalog';
|
2026-09-04 17:04:48 +03:30
|
|
|
import { prosthesisFamilyTone } from '@/components/shared/catalog-type-colors';
|
2026-09-02 17:24:01 +03:30
|
|
|
import { prosthesisTypeColorFromCatalog, prosthesisTypeFillAccent } from '@/components/treatment/prosthesisTypeDisplay';
|
2026-09-01 21:03:04 +03:30
|
|
|
import {
|
2026-09-04 17:04:48 +03:30
|
|
|
catalogByCode,
|
2026-09-01 21:03:04 +03:30
|
|
|
categoryCodesInCatalog,
|
2026-09-04 17:04:48 +03:30
|
|
|
categoryDisabledForJobs,
|
2026-09-01 21:03:04 +03:30
|
|
|
categoryTileColor,
|
2026-09-04 17:04:48 +03:30
|
|
|
crownRestorationCode,
|
2026-09-01 21:03:04 +03:30
|
|
|
leavesFor,
|
|
|
|
|
subcategoriesFor,
|
2026-09-04 17:04:48 +03:30
|
|
|
type AddonSlot,
|
2026-09-01 21:03:04 +03:30
|
|
|
type ArchTarget,
|
|
|
|
|
type PickerScope,
|
|
|
|
|
} from '@/components/treatment/prosthesisTree';
|
|
|
|
|
|
|
|
|
|
const CHIP_INK = '#14253d';
|
|
|
|
|
const TILE_CLASS =
|
2026-09-04 17:04:48 +03:30
|
|
|
'flex min-h-[3.25rem] w-full flex-col items-center justify-center gap-0.5 rounded-[var(--radius-md)] border px-2.5 py-2 text-center transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 disabled:cursor-not-allowed disabled:opacity-45';
|
2026-09-01 21:03:04 +03:30
|
|
|
const TILE_TEXT_CLASS =
|
2026-09-04 17:04:48 +03:30
|
|
|
'w-full whitespace-normal break-words text-center text-xs font-medium leading-snug sm:text-[13px]';
|
|
|
|
|
const TILE_GRID_CLASS =
|
|
|
|
|
'grid gap-2 [grid-template-columns:repeat(auto-fill,minmax(8rem,1fr))]';
|
|
|
|
|
const HEADER_BTN_CLASS = 'h-auto min-h-8 w-full px-2 py-1 text-xs leading-tight whitespace-normal';
|
|
|
|
|
|
|
|
|
|
function tileSurfaceClass(active?: boolean) {
|
|
|
|
|
return active
|
|
|
|
|
? 'border-primary ring-1 ring-primary/30'
|
|
|
|
|
: 'border-border/70 hover:border-border hover:bg-background-card/70';
|
|
|
|
|
}
|
2026-09-01 21:03:04 +03:30
|
|
|
|
|
|
|
|
export type PickerStep =
|
|
|
|
|
| { kind: 'category' }
|
|
|
|
|
| { kind: 'subcategory'; category: string; arch?: ArchTarget }
|
|
|
|
|
| { kind: 'leaves'; category: string; subcategory?: string; arch?: ArchTarget }
|
2026-09-04 17:04:48 +03:30
|
|
|
| { kind: 'addon'; slot: AddonSlot };
|
2026-09-01 21:03:04 +03:30
|
|
|
|
|
|
|
|
interface ProsthesisJobPopoverProps {
|
|
|
|
|
open: boolean;
|
|
|
|
|
mobile: boolean;
|
|
|
|
|
catalog: readonly ProsthesisCatalogEntry[];
|
|
|
|
|
step: PickerStep;
|
|
|
|
|
scope: PickerScope;
|
|
|
|
|
lockedArch?: ArchTarget;
|
|
|
|
|
jobCodes: string[];
|
|
|
|
|
tooth: string | null;
|
2026-09-04 17:04:48 +03:30
|
|
|
allowCrownAddon: boolean;
|
2026-09-01 21:03:04 +03:30
|
|
|
onStep: (step: PickerStep) => void;
|
|
|
|
|
onLockedArchChange?: (arch: ArchTarget) => void;
|
|
|
|
|
onPickLeaf: (code: string, arch?: ArchTarget) => void;
|
2026-09-04 17:04:48 +03:30
|
|
|
onPickAddon: (code: string | null, kind: AddonSlot) => void;
|
2026-09-01 21:03:04 +03:30
|
|
|
onRemoveJob: (code: string) => void;
|
|
|
|
|
onClear: () => void;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Tile({
|
|
|
|
|
label,
|
|
|
|
|
color,
|
|
|
|
|
active,
|
2026-09-04 17:04:48 +03:30
|
|
|
disabled,
|
|
|
|
|
hint,
|
|
|
|
|
className = TILE_CLASS,
|
2026-09-01 21:03:04 +03:30
|
|
|
onClick,
|
|
|
|
|
}: {
|
|
|
|
|
label: string;
|
|
|
|
|
color?: string;
|
|
|
|
|
active?: boolean;
|
2026-09-04 17:04:48 +03:30
|
|
|
disabled?: boolean;
|
|
|
|
|
hint?: string;
|
|
|
|
|
className?: string;
|
2026-09-01 21:03:04 +03:30
|
|
|
onClick: () => void;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-09-04 17:04:48 +03:30
|
|
|
title={hint ?? label}
|
2026-09-01 21:03:04 +03:30
|
|
|
onClick={onClick}
|
2026-09-04 17:04:48 +03:30
|
|
|
disabled={disabled}
|
|
|
|
|
className={`${className} ${tileSurfaceClass(active)}`}
|
2026-09-01 21:03:04 +03:30
|
|
|
style={color ? { backgroundColor: color, color: CHIP_INK } : undefined}
|
|
|
|
|
>
|
|
|
|
|
<span className={TILE_TEXT_CLASS}>{label}</span>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parentStep(step: PickerStep): PickerStep | null {
|
2026-09-04 17:04:48 +03:30
|
|
|
switch (step.kind) {
|
|
|
|
|
case 'category':
|
|
|
|
|
return null;
|
|
|
|
|
case 'addon':
|
|
|
|
|
case 'subcategory':
|
|
|
|
|
return { kind: 'category' };
|
|
|
|
|
case 'leaves':
|
|
|
|
|
return step.subcategory
|
|
|
|
|
? { kind: 'subcategory', category: step.category, arch: step.arch }
|
|
|
|
|
: { kind: 'category' };
|
2026-09-01 21:03:04 +03:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ProsthesisJobPopover({
|
|
|
|
|
open,
|
|
|
|
|
mobile,
|
|
|
|
|
catalog,
|
|
|
|
|
step,
|
|
|
|
|
scope,
|
|
|
|
|
lockedArch,
|
|
|
|
|
jobCodes,
|
|
|
|
|
tooth,
|
2026-09-04 17:04:48 +03:30
|
|
|
allowCrownAddon,
|
2026-09-01 21:03:04 +03:30
|
|
|
onStep,
|
|
|
|
|
onLockedArchChange,
|
|
|
|
|
onPickLeaf,
|
|
|
|
|
onPickAddon,
|
|
|
|
|
onRemoveJob,
|
|
|
|
|
onClear,
|
|
|
|
|
onClose,
|
|
|
|
|
}: ProsthesisJobPopoverProps) {
|
|
|
|
|
const t = useTranslations('prosthesis');
|
|
|
|
|
if (!open) return null;
|
|
|
|
|
|
|
|
|
|
const categories = categoryCodesInCatalog(catalog, scope);
|
2026-09-04 17:04:48 +03:30
|
|
|
const byCode = catalogByCode(catalog);
|
|
|
|
|
const crownLeaves = leavesFor(catalog, 'crown', undefined, 'tooth');
|
|
|
|
|
const currentCrown = crownRestorationCode(jobCodes, byCode) ?? '';
|
2026-09-01 21:03:04 +03:30
|
|
|
const hasLeaf = jobCodes.length > 0;
|
|
|
|
|
const archForLeaves =
|
|
|
|
|
step.kind === 'leaves' || step.kind === 'subcategory' ? step.arch : lockedArch;
|
|
|
|
|
|
|
|
|
|
function categoryLabel(code: string) {
|
|
|
|
|
return t(`category_${code}` as never);
|
|
|
|
|
}
|
|
|
|
|
function subLabel(code: string) {
|
|
|
|
|
return t(`sub_${code}` as never);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const back = parentStep(step);
|
|
|
|
|
|
|
|
|
|
function openCategory(code: string) {
|
2026-09-04 17:04:48 +03:30
|
|
|
if (categoryDisabledForJobs(code, jobCodes, byCode)) return;
|
2026-09-02 17:24:01 +03:30
|
|
|
const subs = subcategoriesFor(catalog, code, scope);
|
2026-09-01 21:03:04 +03:30
|
|
|
const arch = scope === 'arch' ? lockedArch : undefined;
|
|
|
|
|
if (subs.length > 0) {
|
|
|
|
|
onStep({ kind: 'subcategory', category: code, arch });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
onStep({ kind: 'leaves', category: code, arch });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function leafTile(leaf: ProsthesisCatalogEntry) {
|
|
|
|
|
return (
|
|
|
|
|
<Tile
|
|
|
|
|
key={leaf.code}
|
|
|
|
|
label={leaf.label}
|
|
|
|
|
color={prosthesisTypeColorFromCatalog(leaf.code, catalog)}
|
|
|
|
|
active={jobCodes.includes(leaf.code)}
|
|
|
|
|
onClick={() => onPickLeaf(leaf.code, archForLeaves)}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const addonTiles: ReactNode[] = [];
|
2026-09-04 17:04:48 +03:30
|
|
|
if (scope === 'tooth' && allowCrownAddon && crownLeaves.length > 0) {
|
|
|
|
|
const addonActive = step.kind === 'addon' && step.slot === 'crown';
|
|
|
|
|
const openAddon = () =>
|
|
|
|
|
onStep(addonActive ? { kind: 'category' } : { kind: 'addon', slot: 'crown' });
|
|
|
|
|
if (currentCrown) {
|
2026-09-01 21:03:04 +03:30
|
|
|
addonTiles.push(
|
|
|
|
|
<Tile
|
2026-09-04 17:04:48 +03:30
|
|
|
key="slot-crown"
|
|
|
|
|
className={`${TILE_CLASS} h-full`}
|
|
|
|
|
label={byCode.get(currentCrown)?.label ?? currentCrown}
|
|
|
|
|
color={prosthesisTypeColorFromCatalog(currentCrown, catalog)}
|
|
|
|
|
active={addonActive}
|
|
|
|
|
onClick={openAddon}
|
2026-09-01 21:03:04 +03:30
|
|
|
/>,
|
|
|
|
|
);
|
2026-09-04 17:04:48 +03:30
|
|
|
} else {
|
2026-09-01 21:03:04 +03:30
|
|
|
addonTiles.push(
|
2026-09-04 17:04:48 +03:30
|
|
|
<button
|
|
|
|
|
key="slot-crown"
|
|
|
|
|
type="button"
|
|
|
|
|
title={t('addonCrownCanBeAdded')}
|
|
|
|
|
aria-pressed={addonActive}
|
|
|
|
|
onClick={openAddon}
|
|
|
|
|
className={`${TILE_CLASS} h-full ${tileSurfaceClass(addonActive)}`}
|
|
|
|
|
>
|
|
|
|
|
<span className="inline-flex max-w-full items-center justify-center gap-1 rounded-[var(--radius-md)] border border-dashed border-border px-3 py-1.5 text-sm font-medium text-text-primary">
|
|
|
|
|
<Plus className="h-3.5 w-3.5 shrink-0" aria-hidden />
|
|
|
|
|
<span className="whitespace-normal break-words text-center leading-tight">
|
|
|
|
|
{t('addonCrownCanBeAdded')}
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
</button>,
|
2026-09-01 21:03:04 +03:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let selectedNode: { label: string; color?: string } | null = null;
|
|
|
|
|
let childTiles: ReactNode[] = [];
|
|
|
|
|
let childrenKey = 'root';
|
|
|
|
|
|
2026-09-04 17:04:48 +03:30
|
|
|
switch (step.kind) {
|
|
|
|
|
case 'category':
|
|
|
|
|
childTiles = categories.map((code) => {
|
|
|
|
|
const disabled = categoryDisabledForJobs(code, jobCodes, byCode);
|
|
|
|
|
return (
|
|
|
|
|
<Tile
|
|
|
|
|
key={code}
|
|
|
|
|
label={categoryLabel(code)}
|
|
|
|
|
color={categoryTileColor(code)}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
hint={disabled ? t('categoryDisabledHint') : undefined}
|
|
|
|
|
onClick={() => openCategory(code)}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case 'addon':
|
|
|
|
|
selectedNode = { label: t('slotCrown'), color: categoryTileColor('crown') };
|
|
|
|
|
childrenKey = 'addon-crown';
|
|
|
|
|
childTiles = [
|
2026-09-01 21:03:04 +03:30
|
|
|
<Tile
|
2026-09-04 17:04:48 +03:30
|
|
|
key="none"
|
|
|
|
|
label={t('addonNone')}
|
|
|
|
|
active={!currentCrown}
|
|
|
|
|
onClick={() => onPickAddon(null, 'crown')}
|
|
|
|
|
/>,
|
|
|
|
|
...crownLeaves.map((opt) => (
|
|
|
|
|
<Tile
|
|
|
|
|
key={opt.code}
|
|
|
|
|
label={opt.label}
|
|
|
|
|
color={prosthesisTypeColorFromCatalog(opt.code, catalog)}
|
|
|
|
|
active={currentCrown === opt.code}
|
|
|
|
|
onClick={() => onPickAddon(opt.code, 'crown')}
|
|
|
|
|
/>
|
|
|
|
|
)),
|
|
|
|
|
];
|
|
|
|
|
break;
|
|
|
|
|
case 'subcategory': {
|
|
|
|
|
const parentFill = categoryTileColor(step.category);
|
|
|
|
|
selectedNode = { label: categoryLabel(step.category), color: parentFill };
|
|
|
|
|
childrenKey = `sub-${step.category}`;
|
|
|
|
|
childTiles = [
|
|
|
|
|
...subcategoriesFor(catalog, step.category, scope).map((sub, i) => (
|
|
|
|
|
<Tile
|
|
|
|
|
key={sub}
|
|
|
|
|
label={subLabel(sub)}
|
|
|
|
|
color={prosthesisFamilyTone(parentFill, i + 1)}
|
|
|
|
|
onClick={() =>
|
|
|
|
|
onStep({
|
|
|
|
|
kind: 'leaves',
|
|
|
|
|
category: step.category,
|
|
|
|
|
subcategory: sub,
|
|
|
|
|
arch: step.arch,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
)),
|
|
|
|
|
...leavesFor(catalog, step.category, undefined, scope).map(leafTile),
|
|
|
|
|
];
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case 'leaves':
|
|
|
|
|
if (step.subcategory) {
|
|
|
|
|
selectedNode = {
|
|
|
|
|
label: subLabel(step.subcategory),
|
|
|
|
|
color: prosthesisFamilyTone(categoryTileColor(step.category), 1),
|
|
|
|
|
};
|
|
|
|
|
childrenKey = `leaves-${step.category}-${step.subcategory}`;
|
|
|
|
|
childTiles = leavesFor(catalog, step.category, step.subcategory, scope).map(leafTile);
|
|
|
|
|
} else {
|
|
|
|
|
selectedNode = {
|
|
|
|
|
label: categoryLabel(step.category),
|
|
|
|
|
color: categoryTileColor(step.category),
|
|
|
|
|
};
|
|
|
|
|
childrenKey = `leaves-${step.category}`;
|
|
|
|
|
childTiles = leavesFor(catalog, step.category, undefined, scope).map(leafTile);
|
|
|
|
|
}
|
|
|
|
|
break;
|
2026-09-01 21:03:04 +03:30
|
|
|
}
|
|
|
|
|
|
2026-09-04 17:04:48 +03:30
|
|
|
const tileGrid = (
|
|
|
|
|
<div
|
|
|
|
|
key={childrenKey}
|
|
|
|
|
className={`picker-expand min-w-0 flex-1 ${TILE_GRID_CLASS}${
|
|
|
|
|
selectedNode ? '' : ' h-full min-h-0 [grid-auto-rows:minmax(3.25rem,1fr)]'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{childTiles}
|
2026-09-01 21:03:04 +03:30
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
2026-09-04 17:04:48 +03:30
|
|
|
const branchColor = selectedNode?.color ?? 'var(--color-border-strong)';
|
|
|
|
|
const tileRow = selectedNode ? (
|
|
|
|
|
<div className="flex min-h-0 flex-col gap-2">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (back) onStep(back);
|
|
|
|
|
}}
|
|
|
|
|
disabled={!back}
|
|
|
|
|
className={`${TILE_CLASS} flex-row justify-start gap-2 px-3 text-start`}
|
|
|
|
|
style={
|
|
|
|
|
selectedNode.color
|
|
|
|
|
? { backgroundColor: selectedNode.color, color: CHIP_INK }
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
aria-label={t('pickerBack')}
|
|
|
|
|
>
|
|
|
|
|
<ArrowLeft
|
|
|
|
|
className="lucide-inherit h-4 w-4 shrink-0 rtl:rotate-180"
|
|
|
|
|
style={
|
|
|
|
|
selectedNode.color
|
|
|
|
|
? { color: prosthesisTypeFillAccent(selectedNode.color) }
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
aria-hidden
|
|
|
|
|
/>
|
|
|
|
|
<span className={`${TILE_TEXT_CLASS} text-start`}>{selectedNode.label}</span>
|
|
|
|
|
</button>
|
|
|
|
|
<div className="flex items-start">
|
|
|
|
|
<div className="relative h-[3.25rem] w-3 shrink-0" aria-hidden>
|
|
|
|
|
<span
|
|
|
|
|
className="absolute w-0.5"
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: branchColor,
|
|
|
|
|
insetInlineStart: 'calc(50% - 1px)',
|
|
|
|
|
top: 0,
|
|
|
|
|
height: '50%',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<span
|
|
|
|
|
className="absolute h-0.5"
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: branchColor,
|
|
|
|
|
top: 'calc(50% - 1px)',
|
|
|
|
|
insetInlineStart: '50%',
|
|
|
|
|
width: '50%',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{tileGrid}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
tileGrid
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const title =
|
|
|
|
|
scope === 'arch'
|
2026-09-01 21:03:04 +03:30
|
|
|
? lockedArch === 'both'
|
|
|
|
|
? t('pickerArchBoth')
|
|
|
|
|
: lockedArch === 'lower'
|
|
|
|
|
? t('pickerArchLower')
|
|
|
|
|
: t('pickerArchUpper')
|
|
|
|
|
: tooth
|
|
|
|
|
? t('pickerTooth', { tooth })
|
|
|
|
|
: t('pickerTitle');
|
|
|
|
|
|
2026-09-04 17:04:48 +03:30
|
|
|
const jobChips =
|
|
|
|
|
jobCodes.length === 0 ? (
|
|
|
|
|
<p className="text-xs text-text-muted">{t('noneYet')}</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex flex-wrap gap-1.5">
|
|
|
|
|
{jobCodes.map((code) => {
|
|
|
|
|
const entry = byCode.get(code);
|
|
|
|
|
const color = prosthesisTypeColorFromCatalog(code, catalog);
|
|
|
|
|
const trashColor = prosthesisTypeFillAccent(color);
|
|
|
|
|
return (
|
|
|
|
|
<span
|
|
|
|
|
key={code}
|
|
|
|
|
className="inline-flex items-stretch overflow-hidden rounded-[var(--radius-md)] border text-[11px] font-medium"
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: color,
|
|
|
|
|
color: CHIP_INK,
|
|
|
|
|
borderColor: 'rgba(0,0,0,0.16)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span className="inline-flex items-center gap-1 px-2 py-0.5">
|
|
|
|
|
<span className="opacity-70">
|
|
|
|
|
{entry?.category
|
|
|
|
|
? t(`category_${entry.category}` as never)
|
|
|
|
|
: t('regionCrown')}
|
|
|
|
|
</span>
|
|
|
|
|
{entry?.label ?? code}
|
|
|
|
|
</span>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="inline-flex items-center justify-center border-s px-1.5 transition-colors hover:brightness-90"
|
|
|
|
|
style={{
|
|
|
|
|
borderColor: trashColor,
|
|
|
|
|
color: trashColor,
|
|
|
|
|
}}
|
|
|
|
|
aria-label={t('removeJob')}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onRemoveJob(code);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="lucide-inherit h-3.5 w-3.5" aria-hidden />
|
|
|
|
|
</button>
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const body = (
|
|
|
|
|
<div className="grid h-full min-h-0 flex-1 grid-cols-[4fr_1fr] gap-x-3">
|
|
|
|
|
<section className="flex min-h-0 min-w-0 flex-col gap-2.5">
|
|
|
|
|
<p className="shrink-0 text-xs font-semibold leading-none text-text-primary">{title}</p>
|
|
|
|
|
{scope === 'arch' && onLockedArchChange ? (
|
|
|
|
|
<div
|
|
|
|
|
className="flex h-8 w-1/2 shrink-0 rounded-[var(--radius-md)] border border-border p-0.5"
|
|
|
|
|
role="group"
|
|
|
|
|
aria-label={title}
|
|
|
|
|
>
|
|
|
|
|
{(['upper', 'lower', 'both'] as const).map((arch) => (
|
|
|
|
|
<button
|
|
|
|
|
key={arch}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => onLockedArchChange(arch)}
|
|
|
|
|
className={`h-full min-w-0 flex-1 rounded-[var(--radius-sm)] px-2 text-xs font-medium leading-tight ${
|
|
|
|
|
lockedArch === arch
|
|
|
|
|
? 'bg-primary text-white'
|
|
|
|
|
: 'text-text-secondary hover:text-text-primary'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{t(`arch_${arch}`)}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
<div className="shrink-0">{jobChips}</div>
|
|
|
|
|
<p className="shrink-0 text-[10px] leading-tight text-text-muted">
|
|
|
|
|
{scope === 'arch' ? t('clearHintArch') : t('clearHint')}
|
|
|
|
|
</p>
|
|
|
|
|
<div className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden pe-0.5">
|
|
|
|
|
{tileRow}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
<section className="flex min-h-0 min-w-0 flex-col gap-1">
|
|
|
|
|
<div className="flex shrink-0 flex-col gap-1">
|
2026-09-01 21:03:04 +03:30
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className={HEADER_BTN_CLASS}
|
|
|
|
|
disabled={!hasLeaf}
|
|
|
|
|
onClick={onClear}
|
|
|
|
|
>
|
|
|
|
|
{scope === 'arch' ? t('clearArch') : t('clear')}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="primary"
|
|
|
|
|
size="sm"
|
|
|
|
|
className={HEADER_BTN_CLASS}
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
>
|
|
|
|
|
{t('done')}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-09-04 17:04:48 +03:30
|
|
|
{addonTiles.length > 0 ? (
|
|
|
|
|
<div className="flex min-h-0 flex-1 flex-col gap-1">
|
|
|
|
|
<p className="shrink-0 text-[10px] font-medium leading-tight text-text-muted">
|
|
|
|
|
{t('suggestionsLabel')}
|
|
|
|
|
</p>
|
|
|
|
|
<div className="min-h-0 flex-1">{addonTiles}</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</section>
|
2026-09-01 21:03:04 +03:30
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (mobile) {
|
|
|
|
|
return (
|
|
|
|
|
<ResponsiveDialogOverlay onBackdropClick={onClose}>
|
|
|
|
|
<ResponsiveDialogPanel
|
|
|
|
|
role="dialog"
|
|
|
|
|
aria-modal="true"
|
2026-09-04 17:04:48 +03:30
|
|
|
maxWidthClass="sm:max-w-2xl"
|
2026-09-01 21:03:04 +03:30
|
|
|
className="max-h-[90dvh] flex flex-col"
|
|
|
|
|
>
|
|
|
|
|
<h4 className="sr-only">{t('pickerTitle')}</h4>
|
|
|
|
|
<div className="min-h-0 flex-1 overflow-hidden">{body}</div>
|
|
|
|
|
</ResponsiveDialogPanel>
|
|
|
|
|
</ResponsiveDialogOverlay>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="absolute inset-0 z-30 pointer-events-none">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="absolute inset-0 bg-background-primary/55 pointer-events-auto"
|
|
|
|
|
aria-label={t('done')}
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
/>
|
|
|
|
|
<div
|
2026-09-04 17:04:48 +03:30
|
|
|
className="absolute inset-0 z-10 pointer-events-auto flex flex-col overflow-hidden rounded-[var(--radius-md)] border border-border bg-background-secondary px-3 pb-3 pt-2 shadow-lg"
|
2026-09-01 21:03:04 +03:30
|
|
|
role="dialog"
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
onPointerDown={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
<h4 className="sr-only">{t('pickerTitle')}</h4>
|
|
|
|
|
{body}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|