@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4alldigital/foundation-ui--core",
3
- "version": "3.12.2",
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.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": "3d674cdb288d74e5021fcf47bdf674c44355b7af"
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
- {/* <!-- Mobile Menu --> */}
133
- <div className="navbar-backdrop fixed inset-0 bg-black opacity-25"></div>
134
- <nav className="fixed top-0 left-0 bottom-0 flex flex-col w-5/6 max-w-sm py-4 px-4 bg-body-bg dark:bg-body-bg-dark border-r overflow-y-auto">
135
- <div className="flex justify-between">
136
- <div className="relative w-32">
137
- {context?.brand?.logo && (
138
- <div className="relative w-32">
139
- <Logo logo={context?.brand?.logo} aspectRatioClass={context?.brand?.logoAspectRatio} />
140
- </div>
141
- )}
142
- </div>
143
- <div>
144
- <Button variant={BTN_VARIANTS.PRIMARY} icon="mdi:close" onClick={toggleMenuClass} ariaLabel="Close menu" />
145
- </div>
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
- <div className="flex flex-col flex-1 gap-4">
148
- <div className="flex flex-1 justify-center pt-4">
149
- {context?.app?.isAuthenticated && context?.header?.authNav && (
150
- <Menu links={context.header.authNav} separators />
151
- )}
152
- {!context?.app?.isAuthenticated && context?.header?.anonNav && (
153
- <Menu links={context.header.anonNav} separators />
154
- )}
155
- </div>
156
- <div className="flex flex-1 justify-center pt-4">
157
- {context?.app?.isAuthenticated && context?.header?.profile && (
158
- <Menu links={context.header.profile} separators />
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
- {context?.app?.isAuthenticated && (
162
- <div className="md:hidden flex justify-center">
163
- <Button rounded onClick={context?.app?.logoutCallback}>{T.UI.LOGOUT}</Button>
164
- </div>
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
- {!context?.app?.isAuthenticated && (
167
- <div className="md:hidden flex justify-center">
168
- <Button
169
- rounded
170
- onClick={() => {
171
- context?.app?.loginCallback?.();
172
- toggleMenuClass();
173
- }}>
174
- {T.UI.LOGIN}
175
- </Button>
176
- </div>
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 md:px-48', className)}>
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
@@ -7,4 +7,5 @@ export interface Props {
7
7
  placeholder?: string;
8
8
  allowReset?: boolean;
9
9
  resetTrigger?: (data?: any, options?: any) => void;
10
+ shouldClearFilters?: boolean;
10
11
  }