@hanzo/ui 4.1.3 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/ui",
3
- "version": "4.1.3",
3
+ "version": "4.2.1",
4
4
  "description": "Library that contains shared UI primitives, support for a common design system, and other boilerplate support.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
@@ -105,6 +105,7 @@
105
105
  "devDependencies": {
106
106
  "@mdx-js/loader": "^3.0.0",
107
107
  "@mdx-js/react": "^3.0.0",
108
+ "@radix-ui/react-primitive": "^2.0.1",
108
109
  "@types/facebook-pixel": "^0.0.30",
109
110
  "@types/gtag.js": "^0.0.19",
110
111
  "@types/lodash.merge": "^4.6.9",
@@ -2,7 +2,7 @@ import React from 'react'
2
2
 
3
3
  const BreakpointIndicator: React.FC = () => {
4
4
 
5
- if (process.env.NODE_ENV === "production") return null
5
+ if (process?.env?.NODE_ENV !== undefined && process.env.NODE_ENV === "production") return null
6
6
 
7
7
  return (
8
8
  <div className="fixed bottom-1 left-1 z-floating flex h-6 w-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-primary">
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
  import React, { useState } from 'react'
3
+
3
4
  import { Check, ChevronDown } from 'lucide-react'
4
5
 
5
6
  import { cn } from '../util'
@@ -21,107 +22,168 @@ import {
21
22
 
22
23
  import type ListAdaptor from './list-adaptor'
23
24
 
24
- const ElementImage: React.FC<{
25
- url: string | undefined
26
- alt?: string
27
- w: number
28
- h: number
29
- className?: string
30
- }> = ({
31
- url,
32
- alt,
33
- w,
34
- h,
35
- className=''
36
- }) => (url ? (
37
- <img
38
- src={url}
39
- alt={alt ?? 'image'}
40
- height={h}
41
- width={w}
42
- loading="eager"
43
- className={className}
44
- />
45
- ) : null
46
- )
47
- // "rounded-sm object-contain"
25
+ const DEFAULT_IMAGE_SIZE = 32
26
+
27
+ interface ComboboxTriggerProps<T> {
28
+ current: T | null
29
+ currentLabel: string | null
30
+ imageUrl: string | null
31
+ placeholder?: string
32
+ buttonClx?: string
33
+ imageClx?: string
34
+ disabled?: boolean
35
+ imageSize?: number
36
+ noChevron?: boolean
37
+ open: boolean
38
+ }
48
39
 
49
- const Combobox = <T,>(
40
+ const DefaultTriggerInner = <T,>(
50
41
  {
51
- elements,
52
- adaptor,
42
+ current,
43
+ currentLabel,
44
+ imageUrl,
53
45
  buttonClx='',
54
- popoverClx='',
55
46
  imageClx='',
56
- initial,
57
- searchPlaceholder='Search...',
58
- buttonPlaceholder='Select...',
59
- noneFoundMessage='None found.',
60
- elementSelected,
47
+ placeholder='(select)',
61
48
  disabled=false,
62
- imageSize=32
63
- }: {
64
- elements: T[]
65
- adaptor: ListAdaptor<T>
66
- elementSelected: (e: T) => void
67
- buttonClx?: string
68
- popoverClx?: string
69
- imageClx?: string
70
- buttonPlaceholder?: string
71
- searchPlaceholder?: string
72
- noneFoundMessage?: string
73
- initial?: T,
74
- disabled?: boolean
75
- imageSize?: number
49
+ imageSize=DEFAULT_IMAGE_SIZE,
50
+ noChevron=false,
51
+ open,
52
+ ...rest
53
+ }: ComboboxTriggerProps<T>,
54
+ ref: React.ForwardedRef<HTMLButtonElement>
55
+ ) => (
56
+ <Button
57
+ ref={ref}
58
+ {...rest}
59
+ variant='outline'
60
+ role='combobox'
61
+ aria-expanded={open}
62
+ className={cn(
63
+ 'flex',
64
+ noChevron ? 'justify-start' : 'justify-between',
65
+ buttonClx
66
+ )}
67
+ disabled={disabled}
68
+ >
69
+ <div className='flex justify-start items-center gap-2'>
70
+ {(current && imageUrl) ? (
71
+ <img
72
+ src={imageUrl}
73
+ alt={currentLabel + ' image'}
74
+ height={imageSize}
75
+ width={imageSize}
76
+ loading="eager"
77
+ className={cn('block', imageClx)}
78
+ />
79
+ ) : (
80
+ <div style={{width: imageSize, height: imageSize}} />
81
+ )}
82
+ <span className='block'>{currentLabel}</span>
83
+ </div>
84
+ {!noChevron && (<ChevronDown className={cn('block', open ? '' : 'opacity-50')} />)}
85
+ </Button>
86
+ )
87
+
88
+ const DefaultTrigger = React.forwardRef(DefaultTriggerInner) as <T, P>(props: P & { ref?: React.ForwardedRef<HTMLButtonElement> }) => React.ReactNode
89
+
90
+ const Combobox = <T, P extends ComboboxTriggerProps<T>>({
91
+ elements,
92
+ initial,
93
+ current,
94
+ setCurrent,
95
+ closeOnSelect=true,
96
+ adaptor,
97
+ popoverClx='',
98
+ listItemClx='',
99
+ listItemSelectedClx='',
100
+ noCheckmark=false,
101
+ listItemImageClx='',
102
+ searchPlaceholder='Search...',
103
+ noneFoundMessage='None found.',
104
+ listItemImageSize=DEFAULT_IMAGE_SIZE,
105
+ noSearch=false,
106
+ popoverAlign = 'center',
107
+ popoverSideOffset = 4,
108
+ Trigger,
109
+ triggerProps
110
+ }: {
111
+ elements: T[]
112
+ initial?: T | null
113
+ current?: T | null
114
+ setCurrent: (c: T | null) => void
115
+ closeOnSelect?: boolean
116
+ adaptor: ListAdaptor<T>
117
+ popoverClx?: string
118
+ listItemClx?: string
119
+ listItemSelectedClx?: string
120
+ listItemImageClx?: string
121
+ listItemImageSize?: number
122
+ noCheckmark?: boolean
123
+ searchPlaceholder?: string
124
+ noneFoundMessage?: string
125
+ noSearch?: boolean
126
+ popoverAlign?: "center" | "end" | "start"
127
+ popoverSideOffset?: number
128
+ /** If (custom) Trigger is not supplied,
129
+ * passed to default trigger */
130
+ triggerProps: P
131
+ Trigger?:
132
+ <T, P>(props: P & { ref?: React.ForwardedRef<HTMLButtonElement> }) => React.ReactNode
76
133
  }) => {
77
134
 
78
- const [open, setOpen] = useState<boolean>(false)
79
- const [current, setCurrent] = useState<T | null>(initial ?? null)
135
+ const [_open, _setOpen] = useState<boolean>(false)
136
+ // for non-controlled base (must declare the hook either way)
137
+ const [_current, _setCurrent] = useState<T | null>(initial ?? null)
80
138
 
81
139
  const handleSelect = (selString: string) => {
82
140
 
83
141
  const found = elements.find((el: T) => (adaptor.valueEquals(el, selString)))
84
142
  if (found) {
143
+ // non-controlled ('initial' supplied (may have been null))
144
+ if (initial !== undefined) {
145
+ _setCurrent(found)
146
+ }
85
147
  setCurrent(found)
86
- elementSelected(found)
87
148
  }
88
- setOpen(false)
149
+ if (closeOnSelect) {
150
+ _setOpen(false)
151
+ }
89
152
  }
90
153
 
91
- const isCurrent = (el: T): boolean => (!!current && (adaptor.equals(el, current)))
154
+ const isCurrent = (el: T): boolean => {
92
155
 
93
- let currentValue: string | undefined
94
- let currentLabel: string | undefined
95
- let currentImageUrl: string | undefined
156
+ // non-controlled?
157
+ const curr = (current === undefined) ? _current : current
158
+ return !!curr && adaptor.equals(el, curr)
159
+ }
96
160
 
97
- if (current) {
98
- currentValue = adaptor.getValue(current)
99
- currentLabel = adaptor.getLabel ? adaptor.getLabel(current) : undefined
100
- currentImageUrl = adaptor.getImageUrl ? adaptor.getImageUrl(current) : undefined
161
+ const _triggerProps = current ? {
162
+ ...triggerProps,
163
+ current,
164
+ currentLabel: adaptor.getLabel ? adaptor.getLabel(current) : adaptor.getValue(current),
165
+ imageUrl: adaptor.getImageUrl ? adaptor.getImageUrl(current) : null,
166
+ open: _open
167
+ } : {
168
+ ...triggerProps,
169
+ current: null,
170
+ currentLabel: null,
171
+ imageUrl: null,
172
+ open: _open
101
173
  }
102
174
 
103
175
  return (
104
- <Popover open={open} onOpenChange={setOpen}>
176
+ <Popover open={_open} onOpenChange={_setOpen}>
105
177
  <PopoverTrigger asChild>
106
- <Button
107
- variant='outline'
108
- role='combobox'
109
- aria-expanded={open}
110
- className={'flex justify-between ' + buttonClx}
111
- disabled={disabled}
112
- >
113
- <div className='flex justify-start items-center gap-2'>
114
- {current && (
115
- <ElementImage url={currentImageUrl} w={imageSize} h={imageSize} className={imageClx} alt={currentValue + ' image'}/>
116
- )}
117
- <span>{ current ? (currentLabel ?? currentValue) : buttonPlaceholder }</span>
118
- </div>
119
- <ChevronDown className={open ? '' : 'opacity-50'} />
120
- </Button>
178
+ {Trigger ? (
179
+ <Trigger<T, P> {..._triggerProps} />
180
+ ) : (
181
+ <DefaultTrigger<T, P> {..._triggerProps} />
182
+ )}
121
183
  </PopoverTrigger>
122
- <PopoverContent className={'p-0 ' + popoverClx}>
184
+ <PopoverContent className={cn('p-0', popoverClx)} align={popoverAlign} sideOffset={popoverSideOffset}>
123
185
  <Command>
124
- <CommandInput placeholder={searchPlaceholder} />
186
+ {!noSearch && (<CommandInput placeholder={searchPlaceholder} />)}
125
187
  <CommandList>
126
188
  <CommandEmpty>{noneFoundMessage}</CommandEmpty>
127
189
  <CommandGroup>
@@ -130,21 +192,33 @@ const Combobox = <T,>(
130
192
  key={adaptor.getValue(el)}
131
193
  value={adaptor.getValue(el)}
132
194
  onSelect={handleSelect}
133
- className='flex justify-between'
195
+ className={cn(
196
+ 'flex',
197
+ noCheckmark ? 'justify-start' : 'justify-between',
198
+ listItemClx,
199
+ (isCurrent(el) ? listItemSelectedClx : '')
200
+ )}
134
201
  >
135
202
  <div className='flex justify-start items-center gap-2'>
136
- <ElementImage
137
- url={adaptor.getImageUrl ? adaptor.getImageUrl(el) : undefined}
138
- w={imageSize}
139
- h={imageSize}
140
- className={imageClx}
203
+ { (adaptor.getImageUrl && adaptor.getImageUrl(el)) ? (
204
+ <img
205
+ src={adaptor.getImageUrl(el)!}
141
206
  alt={adaptor.getValue(el) + ' image'}
207
+ height={listItemImageSize}
208
+ width={listItemImageSize}
209
+ loading="eager"
210
+ className={listItemImageClx}
142
211
  />
212
+ ) : (
213
+ <div style={{width: listItemImageSize, height: listItemImageSize}} />
214
+ )}
143
215
  <span>{ adaptor.getLabel ? adaptor.getLabel(el) : adaptor.getValue(el) }</span>
144
216
  </div>
217
+ {!noCheckmark && (
145
218
  <div>
146
219
  <Check className={cn('ml-auto', (isCurrent(el)) ? '' : 'invisible' )} />
147
220
  </div>
221
+ )}
148
222
  </CommandItem>
149
223
  ))}
150
224
  </CommandGroup>
@@ -155,4 +229,7 @@ const Combobox = <T,>(
155
229
  )
156
230
  }
157
231
 
158
- export default Combobox
232
+ export {
233
+ Combobox as default,
234
+ type ComboboxTriggerProps
235
+ }
@@ -135,7 +135,7 @@ const CommandShortcut = ({
135
135
  return (
136
136
  <span
137
137
  className={cn(
138
- "ml-auto text-xs tracking-widest text-muted-foreground",
138
+ "ml-auto text-xs tracking-widest text-muted",
139
139
  className
140
140
  )}
141
141
  {...props}
@@ -198,7 +198,7 @@ export { default as Badge } from './badge'
198
198
  export { default as BreakpointIndicator } from './breakpoint-indicator'
199
199
  export { default as Calendar } from './calendar'
200
200
  export { default as Checkbox } from './checkbox'
201
- export { default as Combobox } from './combobox'
201
+ export { default as Combobox, type ComboboxTriggerProps } from './combobox'
202
202
  export { default as DialogVideoController } from './dialog-video-controller'
203
203
  export { default as Input } from './input'
204
204
  export { default as Label } from './label'
@@ -3,7 +3,7 @@ interface ListAdaptor<T> {
3
3
  equals: (el1: T, el2: T) => boolean
4
4
  valueEquals: (el: T, v: string) => boolean
5
5
  getLabel?: (el: T) => string
6
- getImageUrl?: (el: T) => string
6
+ getImageUrl?: (el: T) => string | null
7
7
  }
8
8
 
9
9
  export {
package/util/index.ts CHANGED
@@ -6,8 +6,7 @@ export { cva, type VariantProps } from 'class-variance-authority'
6
6
 
7
7
  import type { Dimensions } from '../types'
8
8
 
9
- // @ts-ignore
10
- import _merge from 'lodash.merge'
9
+ import { default as _merge } from 'lodash.merge'
11
10
 
12
11
  export const cn = (...inputs: ClassValue[]) => (
13
12
  twMerge(clsx(inputs))