uibee 2.8.3 → 2.8.5

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.
@@ -16,5 +16,6 @@ export type SelectProps = {
16
16
  placeholder?: string;
17
17
  info?: string;
18
18
  clearable?: boolean;
19
+ searchable?: boolean;
19
20
  };
20
- export default function Select({ label, name, value, onChange, options, error, className, disabled, required, placeholder, info, clearable, }: SelectProps): import("react/jsx-runtime").JSX.Element;
21
+ export default function Select({ label, name, value, onChange, options, error, className, disabled, required, placeholder, info, clearable, searchable, }: SelectProps): import("react/jsx-runtime").JSX.Element;
@@ -3,11 +3,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect } from 'react';
4
4
  import Image from 'next/image';
5
5
  import { useClickOutside } from '../../hooks';
6
- import { ChevronDown, X } from 'lucide-react';
6
+ import { ChevronDown, X, Search } from 'lucide-react';
7
7
  import { FieldWrapper } from './shared';
8
- export default function Select({ label, name, value, onChange, options, error, className, disabled, required, placeholder = 'Select an option', info, clearable = true, }) {
8
+ export default function Select({ label, name, value, onChange, options, error, className, disabled, required, placeholder = 'Select an option', info, clearable = true, searchable = true, }) {
9
9
  const [isOpen, setIsOpen] = useState(false);
10
+ const [searchTerm, setSearchTerm] = useState('');
10
11
  const [selectedOption, setSelectedOption] = useState(options.find(opt => opt.value === value));
12
+ useEffect(() => {
13
+ if (!isOpen) {
14
+ setSearchTerm('');
15
+ }
16
+ }, [isOpen]);
11
17
  useEffect(() => {
12
18
  setSelectedOption(options.find(opt => opt.value === value));
13
19
  }, [value, options]);
@@ -30,6 +36,7 @@ export default function Select({ label, name, value, onChange, options, error, c
30
36
  onChange(null);
31
37
  }
32
38
  }
39
+ const filteredOptions = options.filter(option => option.label.toLowerCase().includes(searchTerm.toLowerCase()));
33
40
  return (_jsxs(FieldWrapper, { label: label, name: name, required: required, info: info, error: error, className: className, children: [_jsxs("div", { className: 'relative', ref: containerRef, children: [_jsxs("button", { type: 'button', onClick: () => !disabled && setIsOpen(!isOpen), disabled: disabled, "aria-haspopup": 'listbox', "aria-expanded": isOpen, "aria-labelledby": label ? undefined : name, className: `
34
41
  w-full rounded-md bg-login-500/50 border border-login-500
35
42
  text-login-text text-left
@@ -47,13 +54,17 @@ export default function Select({ label, name, value, onChange, options, error, c
47
54
  text-login-200 pointer-events-none
48
55
  transition-transform duration-200
49
56
  ${isOpen ? 'rotate-180' : ''}
50
- `, children: _jsx(ChevronDown, { className: 'w-4 h-4' }) })] })] }), isOpen && (_jsx("div", { className: `
57
+ `, children: _jsx(ChevronDown, { className: 'w-4 h-4' }) })] })] }), isOpen && (_jsxs("div", { className: `
51
58
  absolute z-50 w-full mt-1 bg-login-600 border border-login-500
52
- rounded-md shadow-lg max-h-60 overflow-auto noscroll
53
- `, children: options.length > 0 ? (_jsx("ul", { className: 'py-1', role: 'listbox', children: options.map((option) => (_jsx("li", { role: 'option', "aria-selected": selectedOption?.value === option.value, children: _jsxs("button", { type: 'button', onClick: () => handleSelect(option), className: `
54
- w-full text-left px-3 py-2 text-sm
55
- hover:bg-login-500 transition-colors duration-150
56
- flex items-center gap-2
57
- ${selectedOption?.value === option.value ? 'bg-login-500 text-login' : 'text-login-text'}
58
- `, children: [option.image && (_jsx(Image, { src: option.image, alt: '', width: 75, height: 25, className: 'rounded-md object-cover shrink-0' })), _jsx("span", { className: 'truncate', children: option.label })] }) }, option.value))) })) : (_jsx("div", { className: 'px-3 py-2 text-sm text-login-200', children: "No options available" })) }))] }), _jsx("input", { type: 'hidden', name: name, value: selectedOption?.value || '', required: required })] }));
59
+ rounded-md shadow-lg max-h-60 overflow-hidden flex flex-col
60
+ `, children: [searchable && (_jsx("div", { className: 'p-2 sticky top-0 bg-login-600 border-b border-login-500 z-10', children: _jsxs("div", { className: 'relative', children: [_jsx(Search, { className: 'absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-login-200' }), _jsx("input", { type: 'text', value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), placeholder: 'Search...', autoFocus: true, className: `
61
+ w-full bg-login-500/50 border border-login-500 rounded-md
62
+ py-1.5 pl-9 pr-3 text-sm text-login-text
63
+ focus:outline-none focus:border-login focus:ring-1 focus:ring-login
64
+ ` })] }) })), _jsx("div", { className: 'overflow-auto noscroll', children: filteredOptions.length > 0 ? (_jsx("ul", { className: 'py-1', role: 'listbox', children: filteredOptions.map((option) => (_jsx("li", { role: 'option', "aria-selected": selectedOption?.value === option.value, children: _jsxs("button", { type: 'button', onClick: () => handleSelect(option), className: `
65
+ w-full text-left px-3 py-2 text-sm
66
+ hover:bg-login-500 transition-colors duration-150
67
+ flex items-center gap-2
68
+ ${selectedOption?.value === option.value ? 'bg-login-500 text-login' : 'text-login-text'}
69
+ `, children: [option.image && (_jsx(Image, { src: option.image, alt: '', width: 75, height: 25, className: 'rounded-md object-cover shrink-0' })), _jsx("span", { className: 'truncate', children: option.label })] }) }, option.value))) })) : (_jsx("div", { className: 'px-3 py-2 text-sm text-login-200', children: searchTerm ? 'No results found' : 'No options available' })) })] }))] }), _jsx("input", { type: 'hidden', name: name, value: selectedOption?.value || '', required: required })] }));
59
70
  }
@@ -1,2 +1,2 @@
1
1
  import { LoginPageProps } from 'uibee/components';
2
- export default function LoginPage({ title, description, redirectURL, version, btg, handleSubmit }: LoginPageProps): import("react/jsx-runtime").JSX.Element;
2
+ export default function LoginPage({ title, description, redirectURL, version, btg, handleSubmit, guestRedirectURL, guestText }: LoginPageProps): import("react/jsx-runtime").JSX.Element;
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { LogIn } from 'lucide-react';
3
3
  import Logo from '../logo/logo';
4
4
  import Link from 'next/link';
5
- export default function LoginPage({ title, description, redirectURL, version, btg, handleSubmit }) {
5
+ export default function LoginPage({ title, description, redirectURL, version, btg, handleSubmit, guestRedirectURL, guestText }) {
6
6
  return (_jsx("main", { className: 'w-full h-full flex items-center justify-center bg-login-800 p-8', children: _jsxs("div", { className: 'flex flex-col justify-center items-center bg-login-600 px-4 py-12 rounded-xl w-full max-w-md gap-4 md:gap-6', children: [_jsx("div", { className: 'relative aspect-3/1 w-full', children: _jsx(Logo, { className: 'object-contain px-6 sm:px-12' }) }), _jsxs("h1", { className: 'text-3xl font-extrabold text-login text-center tracking-tight', children: [title, " ", btg ? ' - Break the Glass' : ''] }), description && (_jsx("p", { className: 'text-center font-medium text-lg mb-2 max-w-xs', children: description })), btg ? (_jsxs("form", { className: 'w-full flex flex-col gap-3 max-w-xs', onSubmit: e => {
7
7
  e.preventDefault();
8
8
  handleSubmit?.(new FormData(e.currentTarget));
@@ -13,5 +13,6 @@ export default function LoginPage({ title, description, redirectURL, version, bt
13
13
  max-w-xs py-3 px-6 rounded-xl bg-login font-bold
14
14
  text-lg hover:bg-login/80 transition-all
15
15
  duration-200 mb-2 mt-2 cursor-pointer
16
- `, children: ["Login", _jsx(LogIn, { className: 'w-6 h-6' })] })), _jsxs("span", { className: 'text-sm mt-2', children: ["v", version] })] }) }));
16
+ `, children: ["Login", _jsx(LogIn, { className: 'w-6 h-6' })] })), guestRedirectURL &&
17
+ _jsx(Link, { href: guestRedirectURL, className: 'text-sm font-semibold cursor-pointer opacity-50', children: guestText || 'Continue as guest' }), _jsxs("span", { className: 'text-sm mt-2', children: ["v", version] })] }) }));
17
18
  }
@@ -296,6 +296,9 @@
296
296
  .relative {
297
297
  position: relative;
298
298
  }
299
+ .sticky {
300
+ position: sticky;
301
+ }
299
302
  .inset-0 {
300
303
  inset: calc(var(--spacing) * 0);
301
304
  }
@@ -350,6 +353,9 @@
350
353
  .left-2 {
351
354
  left: calc(var(--spacing) * 2);
352
355
  }
356
+ .left-2\.5 {
357
+ left: calc(var(--spacing) * 2.5);
358
+ }
353
359
  .left-3 {
354
360
  left: calc(var(--spacing) * 3);
355
361
  }
@@ -1416,6 +1422,10 @@
1416
1422
  border-top-style: var(--tw-border-style);
1417
1423
  border-top-width: 1px;
1418
1424
  }
1425
+ .border-b {
1426
+ border-bottom-style: var(--tw-border-style);
1427
+ border-bottom-width: 1px;
1428
+ }
1419
1429
  .border-none {
1420
1430
  --tw-border-style: none;
1421
1431
  border-style: none;
@@ -1608,6 +1618,9 @@
1608
1618
  .py-1 {
1609
1619
  padding-block: calc(var(--spacing) * 1);
1610
1620
  }
1621
+ .py-1\.5 {
1622
+ padding-block: calc(var(--spacing) * 1.5);
1623
+ }
1611
1624
  .py-2 {
1612
1625
  padding-block: calc(var(--spacing) * 2);
1613
1626
  }
@@ -1647,6 +1660,9 @@
1647
1660
  .pl-4 {
1648
1661
  padding-left: calc(var(--spacing) * 4);
1649
1662
  }
1663
+ .pl-9 {
1664
+ padding-left: calc(var(--spacing) * 9);
1665
+ }
1650
1666
  .pl-10 {
1651
1667
  padding-left: calc(var(--spacing) * 10);
1652
1668
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uibee",
3
- "version": "2.8.3",
3
+ "version": "2.8.5",
4
4
  "description": "Shared components, functions and hooks for reuse across Login projects",
5
5
  "homepage": "https://github.com/Login-Linjeforening-for-IT/uibee#readme",
6
6
  "bugs": {
@@ -3,7 +3,7 @@
3
3
  import { useState, useEffect } from 'react'
4
4
  import Image from 'next/image'
5
5
  import { useClickOutside } from '../../hooks'
6
- import { ChevronDown, X } from 'lucide-react'
6
+ import { ChevronDown, X, Search } from 'lucide-react'
7
7
  import { FieldWrapper } from './shared'
8
8
 
9
9
  export type Option = {
@@ -25,6 +25,7 @@ export type SelectProps = {
25
25
  placeholder?: string
26
26
  info?: string
27
27
  clearable?: boolean
28
+ searchable?: boolean
28
29
  }
29
30
 
30
31
  export default function Select({
@@ -40,12 +41,20 @@ export default function Select({
40
41
  placeholder = 'Select an option',
41
42
  info,
42
43
  clearable = true,
44
+ searchable = true,
43
45
  }: SelectProps) {
44
46
  const [isOpen, setIsOpen] = useState(false)
47
+ const [searchTerm, setSearchTerm] = useState('')
45
48
  const [selectedOption, setSelectedOption] = useState<Option | undefined>(
46
49
  options.find(opt => opt.value === value)
47
50
  )
48
51
 
52
+ useEffect(() => {
53
+ if (!isOpen) {
54
+ setSearchTerm('')
55
+ }
56
+ }, [isOpen])
57
+
49
58
  useEffect(() => {
50
59
  setSelectedOption(options.find(opt => opt.value === value))
51
60
  }, [value, options])
@@ -70,6 +79,10 @@ export default function Select({
70
79
  }
71
80
  }
72
81
 
82
+ const filteredOptions = options.filter(option =>
83
+ option.label.toLowerCase().includes(searchTerm.toLowerCase())
84
+ )
85
+
73
86
  return (
74
87
  <FieldWrapper
75
88
  label={label}
@@ -141,41 +154,62 @@ export default function Select({
141
154
  {isOpen && (
142
155
  <div className={`
143
156
  absolute z-50 w-full mt-1 bg-login-600 border border-login-500
144
- rounded-md shadow-lg max-h-60 overflow-auto noscroll
157
+ rounded-md shadow-lg max-h-60 overflow-hidden flex flex-col
145
158
  `}>
146
- {options.length > 0 ? (
147
- <ul className='py-1' role='listbox'>
148
- {options.map((option) => (
149
- <li key={option.value} role='option' aria-selected={selectedOption?.value === option.value}>
150
- <button
151
- type='button'
152
- onClick={() => handleSelect(option)}
153
- className={`
154
- w-full text-left px-3 py-2 text-sm
155
- hover:bg-login-500 transition-colors duration-150
156
- flex items-center gap-2
157
- ${selectedOption?.value === option.value ? 'bg-login-500 text-login' : 'text-login-text'}
158
- `}
159
- >
160
- {option.image && (
161
- <Image
162
- src={option.image}
163
- alt=''
164
- width={75}
165
- height={25}
166
- className='rounded-md object-cover shrink-0'
167
- />
168
- )}
169
- <span className='truncate'>{option.label}</span>
170
- </button>
171
- </li>
172
- ))}
173
- </ul>
174
- ) : (
175
- <div className='px-3 py-2 text-sm text-login-200'>
176
- No options available
159
+ {searchable && (
160
+ <div className='p-2 sticky top-0 bg-login-600 border-b border-login-500 z-10'>
161
+ <div className='relative'>
162
+ <Search className='absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-login-200' />
163
+ <input
164
+ type='text'
165
+ value={searchTerm}
166
+ onChange={(e) => setSearchTerm(e.target.value)}
167
+ placeholder='Search...'
168
+ autoFocus
169
+ className={`
170
+ w-full bg-login-500/50 border border-login-500 rounded-md
171
+ py-1.5 pl-9 pr-3 text-sm text-login-text
172
+ focus:outline-none focus:border-login focus:ring-1 focus:ring-login
173
+ `}
174
+ />
175
+ </div>
177
176
  </div>
178
177
  )}
178
+ <div className='overflow-auto noscroll'>
179
+ {filteredOptions.length > 0 ? (
180
+ <ul className='py-1' role='listbox'>
181
+ {filteredOptions.map((option) => (
182
+ <li key={option.value} role='option' aria-selected={selectedOption?.value === option.value}>
183
+ <button
184
+ type='button'
185
+ onClick={() => handleSelect(option)}
186
+ className={`
187
+ w-full text-left px-3 py-2 text-sm
188
+ hover:bg-login-500 transition-colors duration-150
189
+ flex items-center gap-2
190
+ ${selectedOption?.value === option.value ? 'bg-login-500 text-login': 'text-login-text'}
191
+ `}
192
+ >
193
+ {option.image && (
194
+ <Image
195
+ src={option.image}
196
+ alt=''
197
+ width={75}
198
+ height={25}
199
+ className='rounded-md object-cover shrink-0'
200
+ />
201
+ )}
202
+ <span className='truncate'>{option.label}</span>
203
+ </button>
204
+ </li>
205
+ ))}
206
+ </ul>
207
+ ) : (
208
+ <div className='px-3 py-2 text-sm text-login-200'>
209
+ {searchTerm ? 'No results found' : 'No options available'}
210
+ </div>
211
+ )}
212
+ </div>
179
213
  </div>
180
214
  )}
181
215
  </div>
@@ -3,7 +3,16 @@ import { LogIn } from 'lucide-react'
3
3
  import Logo from '@components/logo/logo'
4
4
  import Link from 'next/link'
5
5
 
6
- export default function LoginPage({ title, description, redirectURL, version, btg, handleSubmit }: LoginPageProps) {
6
+ export default function LoginPage({
7
+ title,
8
+ description,
9
+ redirectURL,
10
+ version,
11
+ btg,
12
+ handleSubmit,
13
+ guestRedirectURL,
14
+ guestText
15
+ }: LoginPageProps) {
7
16
  return (
8
17
  <main className='w-full h-full flex items-center justify-center bg-login-800 p-8'>
9
18
  <div
@@ -69,6 +78,14 @@ export default function LoginPage({ title, description, redirectURL, version, bt
69
78
  <LogIn className='w-6 h-6' />
70
79
  </Link>
71
80
  )}
81
+ {guestRedirectURL &&
82
+ <Link
83
+ href={guestRedirectURL}
84
+ className='text-sm font-semibold cursor-pointer opacity-50'
85
+ >
86
+ {guestText || 'Continue as guest'}
87
+ </Link>
88
+ }
72
89
  <span className='text-sm mt-2'>v{version}</span>
73
90
  </div>
74
91
  </main>
@@ -7,6 +7,8 @@ declare module 'uibee/components' {
7
7
  version: string
8
8
  btg?: boolean
9
9
  handleSubmit?: (formData: FormData) => void
10
+ guestRedirectURL?: string
11
+ guestText?: string
10
12
  }
11
13
 
12
14
  export type ToastType = 'info' | 'success' | 'warning' | 'error'