@@ -2,17 +2,16 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
22import { Command , CommandEmpty , CommandGroup , CommandInput , CommandItem } from "@/components/ui/command" ;
33import { X } from "lucide-react" ;
44import { useState } from "react" ;
5- import { Input } from "@/components/ui/input" ;
5+ import React from "react" ;
6+
7+ const CATEGORY_MAX_CHARS = 20 ;
68
79interface CategoryLabelProps {
810 category : string ;
911 updateCategory ?: ( category : string , lineItems : string [ ] , removeCategory : boolean ) => void ;
1012 lineItemIds : string [ ] ;
1113 editableTags : boolean ;
12- }
13-
14- interface CategoryBadgeProps extends CategoryLabelProps {
15- allCategories : string [ ] ;
14+ allCategories ?: string [ ] ;
1615}
1716
1817interface CreateCategoryProps {
@@ -22,7 +21,45 @@ interface CreateCategoryProps {
2221 lineItemIds : string [ ] ;
2322}
2423
25- export default function CategoryLabel ( { category, updateCategory, lineItemIds, editableTags } : CategoryLabelProps ) {
24+ interface CategoryBadgeSpanProps {
25+ category : string ;
26+ variant ?: "default" | "flex" ;
27+ clickable ?: boolean ;
28+ children ?: React . ReactNode ;
29+ }
30+
31+ export const CategoryBadgeSpan = React . forwardRef < HTMLSpanElement , CategoryBadgeSpanProps > (
32+ ( { category, variant = "default" , clickable = false , children, ...props } , ref ) => {
33+ const baseClasses = "px-2 py-1 rounded text-xs font-bold h-6 select-none" ;
34+ const variantClasses = variant === "flex" ? "flex items-center gap-1 flex-shrink-0" : "inline-block" ;
35+ const clickableClass = clickable ? "cursor-pointer" : "" ;
36+
37+ const displayText =
38+ category . length > CATEGORY_MAX_CHARS ? `${ category . substring ( 0 , CATEGORY_MAX_CHARS ) } ...` : category ;
39+
40+ return (
41+ < span
42+ ref = { ref }
43+ { ...props }
44+ className = { `${ baseClasses } ${ variantClasses } ${ clickableClass } text-black` }
45+ style = { { backgroundColor : getTagColor ( category ) . backgroundColor } }
46+ >
47+ { displayText }
48+ { children }
49+ </ span >
50+ ) ;
51+ }
52+ ) ;
53+
54+ CategoryBadgeSpan . displayName = "CategoryBadgeSpan" ;
55+
56+ export default function CategoryLabel ( {
57+ category,
58+ updateCategory,
59+ lineItemIds,
60+ editableTags,
61+ allCategories,
62+ } : CategoryLabelProps ) {
2663 const categories = category . length > 0 ? category . split ( "," ) : [ ] ;
2764
2865 if ( editableTags && updateCategory && categories . length === 0 ) {
@@ -43,7 +80,7 @@ export default function CategoryLabel({ category, updateCategory, lineItemIds, e
4380 < CategoryBadge
4481 key = { index }
4582 category = { cat }
46- allCategories = { categories }
83+ allCategories = { allCategories }
4784 updateCategory = { updateCategory }
4885 lineItemIds = { lineItemIds }
4986 editableTags = { editableTags }
@@ -60,27 +97,14 @@ export default function CategoryLabel({ category, updateCategory, lineItemIds, e
6097 < PopoverTrigger asChild >
6198 < button className = "text-gray-400 text-sm hover:text-gray-600 underline" > + Add category</ button >
6299 </ PopoverTrigger >
63- < PopoverContent className = "w-[200px] p-4" >
64- < div className = "space-y-2" >
65- < Input
66- type = "text"
67- placeholder = "Enter category name..."
68- value = { searchValue }
69- onChange = { ( e ) => setSearchValue ( e . target . value ) }
70- maxLength = { 100 }
71- inputMode = "text"
72- className = "h-8 w-full rounded-full bg-muted text-black text-sm border border-border/40 px-3
73- focus-visible:ring-1 focus-visible:ring-ring/40
74- placeholder:text-xs placeholder:text-black
75- [&::placeholder]:text-xs [&::placeholder]:text-black"
76- />
77- < Create
78- searchValue = { searchValue }
79- setSearchValue = { setSearchValue }
80- updateCategory = { updateCategory ! }
81- lineItemIds = { lineItemIds }
82- />
83- </ div >
100+ < PopoverContent className = "w-64 p-0" >
101+ < CategoryCommand
102+ searchValue = { searchValue }
103+ setSearchValue = { setSearchValue }
104+ updateCategory = { updateCategory ! }
105+ lineItemIds = { lineItemIds }
106+ allCategories = { allCategories ?? [ ] }
107+ />
84108 </ PopoverContent >
85109 </ Popover >
86110 ) ;
@@ -93,108 +117,119 @@ export function CategoryBadge({
93117 updateCategory,
94118 lineItemIds,
95119 editableTags,
96- } : CategoryBadgeProps ) {
120+ } : CategoryLabelProps ) {
97121 const [ searchValue , setSearchValue ] = useState ( "" ) ;
98- const displayCategory = category . length > 20 ? `${ category . substring ( 0 , 20 ) } ...` : category ;
99122
100- if ( ! editableTags || ! updateCategory ) {
101- return (
102- < span
103- className = "px-[8px] py-[4px] rounded-[4px] text-[12px] font-bold h-[24px] inline-block"
104- style = { { backgroundColor : getTagColor ( category ) . backgroundColor } }
105- >
106- { displayCategory }
107- </ span >
108- ) ;
123+ if ( ! editableTags || ! updateCategory || ! allCategories ) {
124+ return < CategoryBadgeSpan category = { category } /> ;
109125 }
110126
111127 return (
112128 < Popover >
113129 < PopoverTrigger asChild >
114- < span
115- className = "px-[8px] py-[4px] rounded-[4px] text-[12px] h-[24px] font-bold cursor-pointer inline-block"
116- style = { { backgroundColor : getTagColor ( category ) . backgroundColor } }
117- >
118- { displayCategory }
119- </ span >
130+ < CategoryBadgeSpan category = { category } clickable = { true } />
120131 </ PopoverTrigger >
121- < PopoverContent className = "w-[200px] p-0" >
122- < Command >
123- < div className = "flex items-center gap-2 border-b bg-muted/60 px-2 py-0" cmdk-input-wrapper = "" >
124- < span
125- className = "flex items-center gap-1 px-[8px] py-[4px] h-[24px] rounded-[4px] text-[12px] font-bold text-black flex-shrink-0"
126- style = { getTagColor ( category ) }
127- >
128- { category }
132+ < PopoverContent className = "w-64 p-0" >
133+ < CategoryCommand
134+ searchValue = { searchValue }
135+ setSearchValue = { setSearchValue }
136+ updateCategory = { updateCategory }
137+ lineItemIds = { lineItemIds }
138+ allCategories = { allCategories }
139+ headerContent = {
140+ < CategoryBadgeSpan category = { category } variant = "flex" >
129141 < button
130142 onClick = { ( e ) => {
131143 e . stopPropagation ( ) ;
132144 updateCategory ( category , lineItemIds , true ) ;
133145 } }
134- className = "hover:bg-gray-100 hover:bg-opacity-20 rounded-full p-0.5"
146+ className = "hover:bg-gray-100 hover:bg-opacity-20 rounded-full p-0.5 ml-1 "
135147 >
136- < X className = "h-3 w-3" />
148+ < X className = "h-4 w-4" strokeWidth = { 1.5 } />
137149 </ button >
138- </ span >
139- < div
140- className = "flex-1
141- [&_[data-slot=command-input-wrapper]]:border-0
142- [&_[data-slot=command-input-wrapper]]:px-0
143- [&_[data-slot=command-input-wrapper]]:h-auto
144- [&_svg]:hidden"
145- >
146- < CommandInput
147- value = { searchValue }
148- onValueChange = { ( value ) => {
149- if ( value . length <= 100 ) {
150- setSearchValue ( value ) ;
151- }
152- } }
153- className = "overflow-hidden border-0 px-0 flex-1"
154- inputMode = "text"
155- />
156- </ div >
157- </ div >
158- < div className = "px-3 py-0.5 text-xs text-muted-foreground border-b" >
159- Select an option or create one
160- </ div >
161- < CommandEmpty >
162- < Create
163- searchValue = { searchValue }
164- setSearchValue = { setSearchValue }
165- updateCategory = { updateCategory }
166- lineItemIds = { lineItemIds }
167- />
168- </ CommandEmpty >
169- < CommandGroup >
170- { allCategories . map ( ( cat ) => (
171- < CommandItem
172- key = { cat }
173- onSelect = { ( ) => updateCategory ( cat , lineItemIds , false ) }
174- className = "flex items-center gap-2"
175- >
176- < span
177- className = "px-[8px] py-[4px] h-[24px] rounded-[4px] text-[12px] font-bold text-black"
178- style = { getTagColor ( cat ) }
179- >
180- { cat }
181- </ span >
182- </ CommandItem >
183- ) ) }
184- </ CommandGroup >
185- </ Command >
150+ </ CategoryBadgeSpan >
151+ }
152+ />
186153 </ PopoverContent >
187154 </ Popover >
188155 ) ;
189156}
190157
158+ interface CategoryCommandProps {
159+ searchValue : string ;
160+ setSearchValue : ( value : string ) => void ;
161+ updateCategory : ( category : string , lineItems : string [ ] , removeCategory : boolean ) => void ;
162+ lineItemIds : string [ ] ;
163+ allCategories : string [ ] ;
164+ headerContent ?: React . ReactNode ;
165+ }
166+
167+ function CategoryCommand ( {
168+ searchValue,
169+ setSearchValue,
170+ updateCategory,
171+ lineItemIds,
172+ allCategories,
173+ headerContent,
174+ } : CategoryCommandProps ) {
175+ return (
176+ < Command >
177+ < div className = "flex items-center gap-2 border-b bg-muted/60 px-2 py-0" cmdk-input-wrapper = "" >
178+ { headerContent }
179+ < div
180+ className = "flex-1 [&_[data-slot=command-input-wrapper]]:border-0
181+ [&_[data-slot=command-input-wrapper]]:px-0 [&_[data-slot=command-input-wrapper]]:h-auto [&_svg]:hidden"
182+ >
183+ < CommandInput
184+ value = { searchValue }
185+ onValueChange = { ( value ) => {
186+ if ( value . length <= 100 ) {
187+ setSearchValue ( value ) ;
188+ }
189+ } }
190+ className = "overflow-hidden border-0 px-0 flex-1"
191+ inputMode = "text"
192+ />
193+ </ div >
194+ </ div >
195+ < div className = "px-3 py-2 text-sm text-black" > Select an option or create one</ div >
196+ < CommandEmpty className = "py-0 pb-0.5" >
197+ < Create
198+ searchValue = { searchValue }
199+ setSearchValue = { setSearchValue }
200+ updateCategory = { updateCategory }
201+ lineItemIds = { lineItemIds }
202+ />
203+ </ CommandEmpty >
204+ < CommandGroup >
205+ { allCategories . map ( ( cat ) => (
206+ < CommandItem
207+ key = { cat }
208+ onSelect = { ( ) => updateCategory ( cat , lineItemIds , false ) }
209+ className = "flex items-center gap-2"
210+ >
211+ < CategoryBadgeSpan category = { cat } />
212+ </ CommandItem >
213+ ) ) }
214+ </ CommandGroup >
215+ </ Command >
216+ ) ;
217+ }
218+
191219function Create ( { searchValue, updateCategory, lineItemIds, setSearchValue } : CreateCategoryProps ) {
192- const displayText = searchValue . length > 15 ? `${ searchValue . substring ( 0 , 15 ) } ...` : searchValue ;
220+ const previewColor = {
221+ backgroundColor : "hsl(0, 0%, 85%)" ,
222+ color : "#000000" ,
223+ } ;
224+
225+ const displayText =
226+ searchValue . length > CATEGORY_MAX_CHARS ? `${ searchValue . substring ( 0 , CATEGORY_MAX_CHARS ) } ...` : searchValue ;
193227
194228 return (
195229 < button
196230 type = "button"
197- className = "relative mt-1 flex cursor-pointer select-none items-center rounded-sm px-2 py-0.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground"
231+ className = "relative w-full flex cursor-pointer select-none items-center gap-2 rounded-sm px-3 py-1.5 text-sm
232+ outline-none hover:bg-accent hover:text-accent-foreground"
198233 onClick = { ( ) => {
199234 const name = searchValue . trim ( ) ;
200235 if ( name ) {
@@ -203,7 +238,10 @@ function Create({ searchValue, updateCategory, lineItemIds, setSearchValue }: Cr
203238 }
204239 } }
205240 >
206- Create { displayText }
241+ < span className = "text-sm text-black" > Create</ span >
242+ < span className = "px-2 py-1 rounded text-xs font-bold h-6 inline-block text-black" style = { previewColor } >
243+ { displayText }
244+ </ span >
207245 </ button >
208246 ) ;
209247}
0 commit comments