@@ -4,14 +4,20 @@ import {
44 ArrowLeft ,
55 Check ,
66 Cloud ,
7+ FolderTree ,
8+ Hash ,
9+ Languages ,
10+ Layers ,
711 Loader2 ,
812 PanelLeftClose ,
913 PanelLeftOpen ,
1014 PanelRightClose ,
1115 PanelRightOpen ,
1216 Save ,
1317 Settings2 ,
18+ ShieldCheck ,
1419 Sparkles ,
20+ Tag ,
1521 X ,
1622} from 'lucide-react' ;
1723import { Button } from '@/components/ui/button' ;
@@ -33,6 +39,7 @@ import { useTranslation } from '@/hooks/useTranslation';
3339import { docsAPI , projectAssignmentsAPI } from '@/lib/api' ;
3440import { useResolvedEntityId } from '@/hooks/useResolvedEntityId' ;
3541import { parsePositiveIntegerParam } from '@/utils/validation' ;
42+ import { cn } from '@/lib/utils' ;
3643import { formatRelativeTime } from '@/utils/datetime' ;
3744import type { Doc , DocDir , DocFolder , DocListItem , DocSpace , DocStatus } from '@/types' ;
3845
@@ -117,6 +124,12 @@ export function DocEditor() {
117124 return words . length ;
118125 } , [ content ] ) ;
119126
127+ // Preview chips for the tags field: split on commas, trim, drop empties.
128+ const tagList = useMemo (
129+ ( ) => tags . split ( ',' ) . map ( ( s ) => s . trim ( ) ) . filter ( Boolean ) ,
130+ [ tags ] ,
131+ ) ;
132+
120133 useEffect ( ( ) => {
121134 let cancelled = false ;
122135 ( async ( ) => {
@@ -364,14 +377,20 @@ export function DocEditor() {
364377 { showMeta && (
365378 < >
366379 < div
367- className = "fixed inset-0 z-40 bg-black /40 lg:hidden"
380+ className = "fixed inset-0 z-40 bg-slate-900 /40 backdrop-blur-sm lg:hidden"
368381 onClick = { ( ) => setShowMeta ( false ) }
369382 aria-hidden
370383 />
371384 < aside
372- className = { `fixed inset-y-0 z-50 w-80 max-w-[85vw] space-y-4 overflow-y-auto bg-background p-4 shadow-xl lg:static lg:z-auto lg:w-auto lg:max-w-none lg:overflow-visible lg:bg-transparent lg:p-0 lg:shadow-none ${ isRTL ? 'left-0 border-r border-slate-200 dark:border-slate-800 lg:border-r-0' : 'right-0 border-l border-slate-200 dark:border-slate-800 lg:border-l-0' } ` }
385+ className = { cn (
386+ 'fixed inset-y-0 z-50 flex w-80 max-w-[85vw] flex-col overflow-y-auto bg-background shadow-2xl' ,
387+ 'lg:static lg:z-auto lg:w-auto lg:max-w-none lg:self-start lg:overflow-visible lg:bg-transparent lg:shadow-none' ,
388+ 'lg:sticky lg:top-6' ,
389+ isRTL ? 'left-0' : 'right-0' ,
390+ ) }
373391 >
374- < div className = "flex items-center justify-between lg:hidden" >
392+ { /* Mobile drawer header — desktop uses the panel header below. */ }
393+ < div className = "flex items-center justify-between border-b border-slate-200 px-4 py-3 dark:border-slate-800 lg:hidden" >
375394 < span className = "flex items-center gap-2 text-sm font-semibold" >
376395 < Settings2 className = "h-4 w-4 text-muted-foreground" />
377396 { t ( 'docMetadata' ) }
@@ -380,59 +399,145 @@ export function DocEditor() {
380399 < X className = "h-4 w-4" />
381400 </ Button >
382401 </ div >
383- < div className = "rounded-lg border border-slate-200 p-4 dark:border-slate-800" >
384- < h3 className = "mb-3 hidden items-center gap-2 text-sm font-semibold lg:flex" >
385- < Settings2 className = "h-4 w-4 text-muted-foreground" />
386- { t ( 'docMetadata' ) }
387- </ h3 >
388- < div className = "space-y-3" >
389- < div className = "space-y-1" >
390- < Label className = "text-xs" > { t ( 'status' ) } </ Label >
391- < Select value = { status } onValueChange = { ( v ) => setStatus ( v as DocStatus ) } >
392- < SelectTrigger > < SelectValue /> </ SelectTrigger >
393- < SelectContent >
394- { STATUSES . map ( ( s ) => < SelectItem key = { s } value = { s } > { t ( `docStatus_${ s } ` as any ) } </ SelectItem > ) }
395- </ SelectContent >
396- </ Select >
397- </ div >
398- { space && (
399- < div className = "space-y-1" >
400- < Label className = "text-xs" > { t ( 'docFolder' ) } </ Label >
401- < Select value = { folderId ? String ( folderId ) : 'none' } onValueChange = { ( v ) => setFolderId ( v === 'none' ? null : Number ( v ) ) } >
402- < SelectTrigger > < SelectValue /> </ SelectTrigger >
403- < SelectContent >
404- < SelectItem value = "none" > { t ( 'docNoFolder' ) } </ SelectItem >
405- { folders . map ( ( f ) => < SelectItem key = { f . id } value = { String ( f . id ) } > { f . name } </ SelectItem > ) }
406- </ SelectContent >
407- </ Select >
402+
403+ { /* Seamless properties panel: one cohesive card, hairline-divided sections. */ }
404+ < div className = "flex-1 p-4 lg:p-0" >
405+ < div className = "overflow-hidden rounded-2xl border border-slate-200/80 bg-card shadow-sm ring-1 ring-black/[0.02] dark:border-slate-800 dark:ring-white/[0.02]" >
406+ { /* Panel header (desktop only — mobile has its own above). */ }
407+ < div className = "hidden items-center gap-2 border-b border-slate-200/80 bg-gradient-to-br from-slate-50 to-transparent px-4 py-3 dark:border-slate-800 dark:from-slate-800/40 lg:flex" >
408+ < span className = "flex h-7 w-7 items-center justify-center rounded-lg bg-primary/10 text-primary" >
409+ < Settings2 className = "h-4 w-4" />
410+ </ span >
411+ < span className = "text-sm font-semibold tracking-tight" > { t ( 'docMetadata' ) } </ span >
412+ </ div >
413+
414+ < div className = "divide-y divide-slate-200/70 dark:divide-slate-800/70" >
415+ { /* Status — segmented control */ }
416+ < section className = "px-4 py-3.5" >
417+ < Label className = "mb-2 flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground" >
418+ < span className = { cn ( 'h-2 w-2 rounded-full' , status === 'published' ? 'bg-emerald-500' : status === 'archived' ? 'bg-amber-500' : 'bg-slate-400' ) } />
419+ { t ( 'status' ) }
420+ </ Label >
421+ < div className = "flex gap-1 rounded-xl bg-slate-100 p-1 dark:bg-slate-800/60" >
422+ { STATUSES . map ( ( s ) => (
423+ < button
424+ key = { s }
425+ type = "button"
426+ onClick = { ( ) => setStatus ( s ) }
427+ className = { cn (
428+ 'flex-1 rounded-lg px-2 py-1.5 text-xs font-medium transition-all' ,
429+ status === s
430+ ? cn ( 'shadow-sm' , statusTone [ s ] || statusTone . draft )
431+ : 'text-muted-foreground hover:text-foreground' ,
432+ ) }
433+ >
434+ { t ( `docStatus_${ s } ` as any ) }
435+ </ button >
436+ ) ) }
437+ </ div >
438+ </ section >
439+
440+ { /* Folder */ }
441+ { space && (
442+ < section className = "px-4 py-3.5" >
443+ < Label className = "mb-2 flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground" >
444+ < FolderTree className = "h-3.5 w-3.5" />
445+ { t ( 'docFolder' ) }
446+ </ Label >
447+ < Select value = { folderId ? String ( folderId ) : 'none' } onValueChange = { ( v ) => setFolderId ( v === 'none' ? null : Number ( v ) ) } >
448+ < SelectTrigger className = "h-9 rounded-lg" > < SelectValue /> </ SelectTrigger >
449+ < SelectContent >
450+ < SelectItem value = "none" > { t ( 'docNoFolder' ) } </ SelectItem >
451+ { folders . map ( ( f ) => < SelectItem key = { f . id } value = { String ( f . id ) } > { f . name } </ SelectItem > ) }
452+ </ SelectContent >
453+ </ Select >
454+ </ section >
455+ ) }
456+
457+ { /* Classification */ }
458+ < section className = "px-4 py-3.5" >
459+ < Label className = "mb-2 flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground" >
460+ < ShieldCheck className = "h-3.5 w-3.5" />
461+ { t ( 'docClassification' ) }
462+ </ Label >
463+ < Input
464+ value = { classification }
465+ onChange = { ( e ) => setClassification ( e . target . value ) }
466+ placeholder = { t ( 'docClassificationPlaceholder' ) }
467+ className = "h-9 rounded-lg"
468+ />
469+ < p className = "mt-1.5 text-[11px] leading-snug text-muted-foreground" > { t ( 'docClassificationHelp' ) } </ p >
470+ </ section >
471+
472+ { /* Tags */ }
473+ < section className = "px-4 py-3.5" >
474+ < Label className = "mb-2 flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground" >
475+ < Tag className = "h-3.5 w-3.5" />
476+ { t ( 'tags' ) }
477+ </ Label >
478+ < Input
479+ value = { tags }
480+ onChange = { ( e ) => setTags ( e . target . value ) }
481+ placeholder = { t ( 'docTagsPlaceholder' ) }
482+ dir = "auto"
483+ className = "h-9 rounded-lg"
484+ />
485+ { tagList . length > 0 && (
486+ < div className = "mt-2 flex flex-wrap gap-1.5" >
487+ { tagList . map ( ( tag , i ) => (
488+ < span
489+ key = { `${ tag } -${ i } ` }
490+ dir = "auto"
491+ className = "inline-flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-medium text-primary"
492+ >
493+ < Hash className = "h-3 w-3 opacity-60" />
494+ { tag }
495+ </ span >
496+ ) ) }
497+ </ div >
498+ ) }
499+ </ section >
500+
501+ { /* Direction — segmented control */ }
502+ < section className = "px-4 py-3.5" >
503+ < Label className = "mb-2 flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground" >
504+ < Languages className = "h-3.5 w-3.5" />
505+ { t ( 'docDirection' ) }
506+ </ Label >
507+ < div className = "flex gap-1 rounded-xl bg-slate-100 p-1 dark:bg-slate-800/60" >
508+ { DIRECTIONS . map ( ( d ) => (
509+ < button
510+ key = { d }
511+ type = "button"
512+ onClick = { ( ) => setDir ( d ) }
513+ className = { cn (
514+ 'flex-1 rounded-lg px-2 py-1.5 text-xs font-medium transition-all' ,
515+ dir === d
516+ ? 'bg-background text-foreground shadow-sm'
517+ : 'text-muted-foreground hover:text-foreground' ,
518+ ) }
519+ >
520+ { t ( `docDir_${ d } ` as any ) }
521+ </ button >
522+ ) ) }
523+ </ div >
524+ </ section >
525+
526+ { /* Space + version footer */ }
527+ { space && (
528+ < section className = "flex items-center gap-2 bg-slate-50/60 px-4 py-3 dark:bg-slate-800/30" >
529+ < span className = "flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-slate-200/70 text-muted-foreground dark:bg-slate-700/50" >
530+ < Layers className = "h-3.5 w-3.5" />
531+ </ span >
532+ < div className = "min-w-0" >
533+ < p className = "truncate text-xs font-medium text-foreground" > { space . name } </ p >
534+ < p className = "text-[11px] text-muted-foreground" > { t ( 'docSpace' ) } · { t ( 'docVersion' ) } v{ doc . current_version } </ p >
535+ </ div >
536+ </ section >
537+ ) }
408538 </ div >
409- ) }
410- < div className = "space-y-1" >
411- < Label className = "text-xs" > { t ( 'docClassification' ) } </ Label >
412- < Input value = { classification } onChange = { ( e ) => setClassification ( e . target . value ) } placeholder = { t ( 'docClassificationPlaceholder' ) } />
413- < p className = "text-[11px] text-muted-foreground" > { t ( 'docClassificationHelp' ) } </ p >
414- </ div >
415- < div className = "space-y-1" >
416- < Label className = "text-xs" > { t ( 'tags' ) } </ Label >
417- < Input value = { tags } onChange = { ( e ) => setTags ( e . target . value ) } placeholder = { t ( 'docTagsPlaceholder' ) } dir = "auto" />
418- </ div >
419- < div className = "space-y-1" >
420- < Label className = "text-xs" > { t ( 'docDirection' ) } </ Label >
421- < Select value = { dir } onValueChange = { ( v ) => setDir ( v as DocDir ) } >
422- < SelectTrigger > < SelectValue /> </ SelectTrigger >
423- < SelectContent >
424- { DIRECTIONS . map ( ( d ) => < SelectItem key = { d } value = { d } > { t ( `docDir_${ d } ` as any ) } </ SelectItem > ) }
425- </ SelectContent >
426- </ Select >
427539 </ div >
428540 </ div >
429- </ div >
430- { space && (
431- < div className = "rounded-lg border border-slate-200 p-4 text-xs text-muted-foreground dark:border-slate-800" >
432- < p > { t ( 'docSpace' ) } : < span className = "font-medium text-foreground" > { space . name } </ span > </ p >
433- < p className = "mt-1" > { t ( 'docVersion' ) } : v{ doc . current_version } </ p >
434- </ div >
435- ) }
436541 </ aside >
437542 </ >
438543 ) }
0 commit comments