@4alldigital/foundation-ui--core 3.12.2 → 3.13.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 +3 -3
- package/src/components/AddressForm/AddressForm.tsx +16 -0
- package/src/components/Header/Header.tsx +101 -44
- package/src/components/Menu/Menu.tsx +18 -1
- package/src/components/TextInput/TextInput.tsx +3 -3
- package/src/features/Search/views/SearchBox/SearchBox.tsx +1 -1
- package/src/features/Search/views/SearchBox/SearchBox.types.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4alldigital/foundation-ui--core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.1",
|
|
4
4
|
"description": "Foundation UI Core Component Library (source distribution)",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dev": "tsc --noEmit --watch"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"sonner": "^2.0.
|
|
23
|
+
"sonner": "^2.0.7"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"next": "^14.0.0 || ^15.0.0",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/he": "^1.2.3"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "619d7da9cbabe639be4a03054467f1fa90bc6cc9"
|
|
42
42
|
}
|
|
@@ -124,6 +124,8 @@ export const AddressForm = React.forwardRef<HTMLDivElement, AddressFormProps>(
|
|
|
124
124
|
{/* Full Name */}
|
|
125
125
|
<TextInput
|
|
126
126
|
id="shipping-name"
|
|
127
|
+
name="name"
|
|
128
|
+
autoComplete="shipping name"
|
|
127
129
|
placeholder="Full Name"
|
|
128
130
|
value={address.name || ''}
|
|
129
131
|
onChange={(e) => handleChange('name', e.target.value)}
|
|
@@ -136,6 +138,8 @@ export const AddressForm = React.forwardRef<HTMLDivElement, AddressFormProps>(
|
|
|
136
138
|
{/* Address Line 1 */}
|
|
137
139
|
<TextInput
|
|
138
140
|
id="shipping-line1"
|
|
141
|
+
name="address-line1"
|
|
142
|
+
autoComplete="shipping address-line1"
|
|
139
143
|
placeholder="Street Address"
|
|
140
144
|
value={address.line1 || ''}
|
|
141
145
|
onChange={(e) => handleChange('line1', e.target.value)}
|
|
@@ -148,6 +152,8 @@ export const AddressForm = React.forwardRef<HTMLDivElement, AddressFormProps>(
|
|
|
148
152
|
{/* Address Line 2 */}
|
|
149
153
|
<TextInput
|
|
150
154
|
id="shipping-line2"
|
|
155
|
+
name="address-line2"
|
|
156
|
+
autoComplete="shipping address-line2"
|
|
151
157
|
placeholder="Apartment, suite, etc. (optional)"
|
|
152
158
|
value={address.line2 || ''}
|
|
153
159
|
onChange={(e) => handleChange('line2', e.target.value)}
|
|
@@ -158,6 +164,8 @@ export const AddressForm = React.forwardRef<HTMLDivElement, AddressFormProps>(
|
|
|
158
164
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
159
165
|
<TextInput
|
|
160
166
|
id="shipping-city"
|
|
167
|
+
name="address-level2"
|
|
168
|
+
autoComplete="shipping address-level2"
|
|
161
169
|
placeholder="City"
|
|
162
170
|
value={address.city || ''}
|
|
163
171
|
onChange={(e) => handleChange('city', e.target.value)}
|
|
@@ -169,6 +177,8 @@ export const AddressForm = React.forwardRef<HTMLDivElement, AddressFormProps>(
|
|
|
169
177
|
|
|
170
178
|
<TextInput
|
|
171
179
|
id="shipping-state"
|
|
180
|
+
name="address-level1"
|
|
181
|
+
autoComplete="shipping address-level1"
|
|
172
182
|
placeholder="State / County / Province"
|
|
173
183
|
value={address.state || ''}
|
|
174
184
|
onChange={(e) => handleChange('state', e.target.value)}
|
|
@@ -183,6 +193,8 @@ export const AddressForm = React.forwardRef<HTMLDivElement, AddressFormProps>(
|
|
|
183
193
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
184
194
|
<TextInput
|
|
185
195
|
id="shipping-postal-code"
|
|
196
|
+
name="postal-code"
|
|
197
|
+
autoComplete="shipping postal-code"
|
|
186
198
|
placeholder="Postal / ZIP Code"
|
|
187
199
|
value={address.postal_code || ''}
|
|
188
200
|
onChange={(e) => handleChange('postal_code', e.target.value)}
|
|
@@ -194,6 +206,8 @@ export const AddressForm = React.forwardRef<HTMLDivElement, AddressFormProps>(
|
|
|
194
206
|
|
|
195
207
|
<FormSelect
|
|
196
208
|
id="shipping-country"
|
|
209
|
+
name="country"
|
|
210
|
+
autoComplete="shipping country"
|
|
197
211
|
placeholder="Country"
|
|
198
212
|
value={address.country || ''}
|
|
199
213
|
onChange={(e) => handleChange('country', e.target.value)}
|
|
@@ -208,7 +222,9 @@ export const AddressForm = React.forwardRef<HTMLDivElement, AddressFormProps>(
|
|
|
208
222
|
{/* Phone */}
|
|
209
223
|
<TextInput
|
|
210
224
|
id="shipping-phone"
|
|
225
|
+
name="phone"
|
|
211
226
|
type={InputType.TEL}
|
|
227
|
+
autoComplete="shipping tel"
|
|
212
228
|
placeholder="Phone Number (optional)"
|
|
213
229
|
value={address.phone || ''}
|
|
214
230
|
onChange={(e) => handleChange('phone', e.target.value)}
|
|
@@ -4,17 +4,16 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
4
4
|
import { clsx as cx } from 'clsx';
|
|
5
5
|
import { Props } from './Header.types';
|
|
6
6
|
import { useAppContext } from '../../context/App';
|
|
7
|
+
import { useThemeContext } from '../../context/Theme';
|
|
7
8
|
import Logo from '../Logo';
|
|
8
9
|
import Button from '../Button';
|
|
9
10
|
import Menu from '../Menu';
|
|
10
11
|
import { BTN_VARIANTS } from '../Button/Button.types';
|
|
11
|
-
// import Copy from '../Copy';
|
|
12
12
|
import Link from '../Link';
|
|
13
13
|
import { useLanguage } from '../../hooks';
|
|
14
14
|
import Container from '../Container';
|
|
15
15
|
import { twMerge } from 'tailwind-merge';
|
|
16
16
|
import Cart from '../Cart';
|
|
17
|
-
// import Avatar from '../Avatar';
|
|
18
17
|
|
|
19
18
|
const Header = ({
|
|
20
19
|
testID,
|
|
@@ -28,6 +27,7 @@ const Header = ({
|
|
|
28
27
|
}: Props) => {
|
|
29
28
|
const context = useAppContext();
|
|
30
29
|
const T = useLanguage();
|
|
30
|
+
const theme = useThemeContext();
|
|
31
31
|
|
|
32
32
|
const [mobileMenuClass, mobileMenuIcon] = useState('hidden');
|
|
33
33
|
const [showDropdown, setShowDropdown] = useState(false);
|
|
@@ -94,6 +94,13 @@ const Header = ({
|
|
|
94
94
|
</div>
|
|
95
95
|
{/* <!-- tablet/desktop --> */}
|
|
96
96
|
<div className="hidden md:flex items-center justify-center gap-2">
|
|
97
|
+
{theme?.toggleTheme && (
|
|
98
|
+
<Button
|
|
99
|
+
onClick={theme.toggleTheme}
|
|
100
|
+
icon={theme.isDarkTheme ? 'mdi:weather-sunny' : 'mdi:weather-night'}
|
|
101
|
+
ariaLabel={theme.isDarkTheme ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
97
104
|
{!context?.app?.isAuthenticated && context?.app?.loginCallback && (
|
|
98
105
|
<div className="w-32 flex justify-end">
|
|
99
106
|
<Button rounded onClick={context?.app?.loginCallback}>{T.UI.LOGIN}</Button>
|
|
@@ -129,51 +136,101 @@ const Header = ({
|
|
|
129
136
|
</div>
|
|
130
137
|
</div>
|
|
131
138
|
<div className={cx('navbar-menu relative z-50', mobileMenuClass, { hidden: hideMenu })}>
|
|
132
|
-
{/*
|
|
133
|
-
<div className="navbar-backdrop fixed inset-0 bg-black
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
{/* Mobile drawer backdrop */}
|
|
140
|
+
<div className="navbar-backdrop fixed inset-0 bg-black/40 backdrop-blur-sm" onClick={closeMobileMenu}></div>
|
|
141
|
+
{/* Mobile drawer panel */}
|
|
142
|
+
<nav className="fixed top-0 left-0 bottom-0 flex flex-col w-[85%] max-w-[320px] bg-body-bg dark:bg-body-bg-dark shadow-2xl overflow-y-auto">
|
|
143
|
+
{/* Header */}
|
|
144
|
+
<div className="flex items-center justify-between px-5 py-4 border-b border-black/[0.06] dark:border-white/[0.08]">
|
|
145
|
+
{context?.brand?.logo && (
|
|
146
|
+
<div className={cx('relative h-10', context?.brand?.logoAspectRatio)}>
|
|
147
|
+
<Logo logo={context?.brand?.logo} aspectRatioClass={context?.brand?.logoAspectRatio} />
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
<button
|
|
151
|
+
onClick={toggleMenuClass}
|
|
152
|
+
className="flex h-9 w-9 items-center justify-center rounded-full bg-black/[0.04] dark:bg-white/[0.08] text-body-text dark:text-body-text-dark transition-colors hover:bg-black/[0.08] dark:hover:bg-white/[0.12]"
|
|
153
|
+
aria-label="Close menu"
|
|
154
|
+
>
|
|
155
|
+
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round">
|
|
156
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
157
|
+
</svg>
|
|
158
|
+
</button>
|
|
146
159
|
</div>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
|
|
161
|
+
{/* Navigation */}
|
|
162
|
+
<div className="flex-1 px-3 py-4">
|
|
163
|
+
{/* Main nav */}
|
|
164
|
+
<div className="mb-2">
|
|
165
|
+
<p className="px-3 mb-2 text-[0.65rem] font-bold uppercase tracking-[0.1em] text-body-text/40 dark:text-body-text-dark/40">Menu</p>
|
|
166
|
+
{(context?.app?.isAuthenticated ? context?.header?.authNav : context?.header?.anonNav)?.map((link, index) => (
|
|
167
|
+
<Link
|
|
168
|
+
key={`mobile-nav-${link.id || index}`}
|
|
169
|
+
href={link.url}
|
|
170
|
+
className="flex items-center gap-3 rounded-xl px-3 py-2.5 text-[0.95rem] font-medium text-body-text dark:text-body-text-dark no-underline transition-colors hover:bg-black/[0.04] dark:hover:bg-white/[0.06]"
|
|
171
|
+
>
|
|
172
|
+
{link.title}
|
|
173
|
+
</Link>
|
|
174
|
+
))}
|
|
160
175
|
</div>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
176
|
+
|
|
177
|
+
{/* Divider */}
|
|
178
|
+
{context?.app?.isAuthenticated && context?.header?.profile && (
|
|
179
|
+
<>
|
|
180
|
+
<div className="mx-3 my-3 h-px bg-black/[0.06] dark:bg-white/[0.08]" />
|
|
181
|
+
<div className="mb-2">
|
|
182
|
+
<p className="px-3 mb-2 text-[0.65rem] font-bold uppercase tracking-[0.1em] text-body-text/40 dark:text-body-text-dark/40">Account</p>
|
|
183
|
+
{context.header.profile.filter(l => l.title !== 'Logout').map((link, index) => (
|
|
184
|
+
<Link
|
|
185
|
+
key={`mobile-profile-${link.id || index}`}
|
|
186
|
+
href={link.url}
|
|
187
|
+
className="flex items-center gap-3 rounded-xl px-3 py-2.5 text-[0.95rem] font-medium text-body-text dark:text-body-text-dark no-underline transition-colors hover:bg-black/[0.04] dark:hover:bg-white/[0.06]"
|
|
188
|
+
>
|
|
189
|
+
{link.title}
|
|
190
|
+
</Link>
|
|
191
|
+
))}
|
|
192
|
+
</div>
|
|
193
|
+
</>
|
|
165
194
|
)}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{/* Footer */}
|
|
198
|
+
<div className="px-5 py-5 border-t border-black/[0.06] dark:border-white/[0.08]">
|
|
199
|
+
{/* Dark mode toggle */}
|
|
200
|
+
{theme?.toggleTheme && (
|
|
201
|
+
<button
|
|
202
|
+
onClick={theme.toggleTheme}
|
|
203
|
+
className="flex w-full items-center justify-between rounded-xl px-3 py-2.5 mb-3 text-[0.85rem] font-medium text-body-text/70 dark:text-body-text-dark/70 transition-colors hover:bg-black/[0.04] dark:hover:bg-white/[0.06]"
|
|
204
|
+
>
|
|
205
|
+
<span>{theme.isDarkTheme ? 'Light Mode' : 'Dark Mode'}</span>
|
|
206
|
+
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
207
|
+
{theme.isDarkTheme ? (
|
|
208
|
+
<><circle cx="12" cy="12" r="5" /><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" /></>
|
|
209
|
+
) : (
|
|
210
|
+
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />
|
|
211
|
+
)}
|
|
212
|
+
</svg>
|
|
213
|
+
</button>
|
|
214
|
+
)}
|
|
215
|
+
{/* Auth action */}
|
|
216
|
+
{context?.app?.isAuthenticated ? (
|
|
217
|
+
<button
|
|
218
|
+
onClick={context?.app?.logoutCallback}
|
|
219
|
+
className="flex w-full items-center justify-center rounded-full py-2.5 text-[0.9rem] font-semibold text-body-text/60 dark:text-body-text-dark/60 border border-black/[0.08] dark:border-white/[0.1] transition-colors hover:bg-black/[0.04] dark:hover:bg-white/[0.06]"
|
|
220
|
+
>
|
|
221
|
+
{T.UI.LOGOUT}
|
|
222
|
+
</button>
|
|
223
|
+
) : (
|
|
224
|
+
<Button
|
|
225
|
+
rounded
|
|
226
|
+
wide
|
|
227
|
+
onClick={() => {
|
|
228
|
+
context?.app?.loginCallback?.();
|
|
229
|
+
toggleMenuClass();
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
{T.UI.LOGIN}
|
|
233
|
+
</Button>
|
|
177
234
|
)}
|
|
178
235
|
</div>
|
|
179
236
|
</nav>
|
|
@@ -7,7 +7,20 @@ import Heading from '../Heading';
|
|
|
7
7
|
import { HEADING_TAGS } from '../Heading/Heading.types';
|
|
8
8
|
import { twMerge } from 'tailwind-merge';
|
|
9
9
|
|
|
10
|
+
function useCurrentPathname(): string | null {
|
|
11
|
+
try {
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
13
|
+
const { usePathname } = require('next/navigation');
|
|
14
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
15
|
+
return usePathname();
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
const Menu = ({ testID, title, links, inline = true, separators = false, className }: Props) => {
|
|
22
|
+
const pathname = useCurrentPathname();
|
|
23
|
+
|
|
11
24
|
return (
|
|
12
25
|
<div data-testid={testID || '"Menu"'} className={twMerge(cx('flex flex-col gap-2', className))}>
|
|
13
26
|
{title && (
|
|
@@ -18,13 +31,17 @@ const Menu = ({ testID, title, links, inline = true, separators = false, classNa
|
|
|
18
31
|
<div className={cx('flex gap-0 items-center md:items-start', { 'flex-col md:flex-row ': inline }, { 'flex-col ': !inline })}>
|
|
19
32
|
{links?.map((link, index) => {
|
|
20
33
|
const key = `${link.id}--${index}`;
|
|
34
|
+
const isActive = pathname ? pathname === link.url || (link.url !== '/' && pathname.startsWith(link.url)) : false;
|
|
21
35
|
return (
|
|
22
36
|
<div key={key} className='flex gap-0'>
|
|
23
37
|
<div className="relative py-2">
|
|
24
|
-
<Link href={link.url} className="flex gap-1 transition-opacity hover:opacity-70">
|
|
38
|
+
<Link href={link.url} className={cx("flex gap-1 transition-opacity hover:opacity-70", isActive && "opacity-100 font-bold")}>
|
|
25
39
|
{link.icon && <Icon name={link.icon} />}
|
|
26
40
|
{link.title && <div>{link.title}</div>}
|
|
27
41
|
</Link>
|
|
42
|
+
{isActive && (
|
|
43
|
+
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-current rounded-full" />
|
|
44
|
+
)}
|
|
28
45
|
</div>
|
|
29
46
|
{separators && index + 1 < links.length && (
|
|
30
47
|
<div className="h-full hidden md:flex self-center px-2">
|
|
@@ -33,7 +33,7 @@ const TextInput = forwardRef(function MyInput(
|
|
|
33
33
|
|
|
34
34
|
if (type === InputType.TEXTAREA) {
|
|
35
35
|
return (
|
|
36
|
-
<div data-testid="TextInput" className="relative">
|
|
36
|
+
<div data-testid="TextInput" className="relative w-full">
|
|
37
37
|
<textarea
|
|
38
38
|
ref={ref}
|
|
39
39
|
id={name}
|
|
@@ -52,7 +52,7 @@ const TextInput = forwardRef(function MyInput(
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
|
-
<div data-testid="TextInput" className="relative">
|
|
55
|
+
<div data-testid="TextInput" className="relative w-full">
|
|
56
56
|
{icon && (
|
|
57
57
|
<div className="form-icon absolute inset-y-0 start-3 flex items-center ps-3.5 pointer-events-none">
|
|
58
58
|
<Icon name={icon} />
|
|
@@ -68,7 +68,7 @@ const TextInput = forwardRef(function MyInput(
|
|
|
68
68
|
disabled={disabled}
|
|
69
69
|
placeholder={placeholderSet}
|
|
70
70
|
onChange={onChange}
|
|
71
|
-
className={cx('form-item text-input', { 'pl-12': icon }, className)}
|
|
71
|
+
className={cx('form-item text-input w-full', { 'pl-12': icon }, className)}
|
|
72
72
|
{...rest}
|
|
73
73
|
/>
|
|
74
74
|
</div>
|
|
@@ -17,7 +17,7 @@ function SearchBox({
|
|
|
17
17
|
resetTrigger = () => {},
|
|
18
18
|
}: Props) {
|
|
19
19
|
return (
|
|
20
|
-
<div className={cx('m-0
|
|
20
|
+
<div className={cx('m-0 w-full', className)}>
|
|
21
21
|
<form onSubmit={e => onSubmit(e)} className="flex items-center">
|
|
22
22
|
<div className="flex-1 bg-body-bg/80 dark:bg-body-bg-dark/80 rounded">
|
|
23
23
|
<TextInput
|