keystone-design-bootstrap 1.0.73 → 1.0.75

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.
@@ -7,6 +7,7 @@ declare const THEME_CONFIG: {
7
7
  readonly aman: ".aman";
8
8
  readonly barelux: ".barelux";
9
9
  readonly balance: ".balance";
10
+ readonly custom: "";
10
11
  };
11
12
  type Theme = keyof typeof THEME_CONFIG;
12
13
  declare function getAvailableThemes(): Theme[];
@@ -6,8 +6,10 @@ var THEME_CONFIG = {
6
6
  // Aman Hotels variant files (hero-home.aman.tsx)
7
7
  barelux: ".barelux",
8
8
  // Bare Lux Studio variant files (hero-home.barelux.tsx)
9
- balance: ".balance"
9
+ balance: ".balance",
10
10
  // Balance Aesthetics variant files (hero-home.balance.tsx)
11
+ custom: ""
12
+ // Fully custom sites — no design-system CSS loaded, all styling built in the site itself
11
13
  };
12
14
  function getAvailableThemes() {
13
15
  return Object.keys(THEME_CONFIG);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/themes/index.ts"],"sourcesContent":["/**\n * Theme Configuration\n * Single source of truth for all themes\n */\n\nexport const THEME_CONFIG = {\n classic: '', // Base files with no suffix (hero-home.tsx)\n aman: '.aman', // Aman Hotels variant files (hero-home.aman.tsx)\n barelux: '.barelux', // Bare Lux Studio variant files (hero-home.barelux.tsx)\n balance: '.balance', // Balance Aesthetics variant files (hero-home.balance.tsx)\n} as const;\n\nexport type Theme = keyof typeof THEME_CONFIG;\n\nexport function getAvailableThemes(): Theme[] {\n return Object.keys(THEME_CONFIG) as Theme[];\n}\n\nexport function getThemeSuffix(theme: Theme): string {\n return THEME_CONFIG[theme] || '';\n}\n\nexport function isValidTheme(theme: string): theme is Theme {\n return theme in THEME_CONFIG;\n}\n"],"mappings":";AAKO,IAAM,eAAe;AAAA,EAC1B,SAAS;AAAA;AAAA,EACT,MAAM;AAAA;AAAA,EACN,SAAS;AAAA;AAAA,EACT,SAAS;AAAA;AACX;AAIO,SAAS,qBAA8B;AAC5C,SAAO,OAAO,KAAK,YAAY;AACjC;AAEO,SAAS,eAAe,OAAsB;AACnD,SAAO,aAAa,KAAK,KAAK;AAChC;AAEO,SAAS,aAAa,OAA+B;AAC1D,SAAO,SAAS;AAClB;","names":[]}
1
+ {"version":3,"sources":["../../src/themes/index.ts"],"sourcesContent":["/**\n * Theme Configuration\n * Single source of truth for all themes\n */\n\nexport const THEME_CONFIG = {\n classic: '', // Base files with no suffix (hero-home.tsx)\n aman: '.aman', // Aman Hotels variant files (hero-home.aman.tsx)\n barelux: '.barelux', // Bare Lux Studio variant files (hero-home.barelux.tsx)\n balance: '.balance', // Balance Aesthetics variant files (hero-home.balance.tsx)\n custom: '', // Fully custom sites — no design-system CSS loaded, all styling built in the site itself\n} as const;\n\nexport type Theme = keyof typeof THEME_CONFIG;\n\nexport function getAvailableThemes(): Theme[] {\n return Object.keys(THEME_CONFIG) as Theme[];\n}\n\nexport function getThemeSuffix(theme: Theme): string {\n return THEME_CONFIG[theme] || '';\n}\n\nexport function isValidTheme(theme: string): theme is Theme {\n return theme in THEME_CONFIG;\n}\n"],"mappings":";AAKO,IAAM,eAAe;AAAA,EAC1B,SAAS;AAAA;AAAA,EACT,MAAM;AAAA;AAAA,EACN,SAAS;AAAA;AAAA,EACT,SAAS;AAAA;AAAA,EACT,QAAQ;AAAA;AACV;AAIO,SAAS,qBAA8B;AAC5C,SAAO,OAAO,KAAK,YAAY;AACjC;AAEO,SAAS,eAAe,OAAsB;AACnD,SAAO,aAAa,KAAK,KAAK;AAChC;AAEO,SAAS,aAAa,OAA+B;AAC1D,SAAO,SAAS;AAClB;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keystone-design-bootstrap",
3
- "version": "1.0.73",
3
+ "version": "1.0.75",
4
4
  "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -44,6 +44,13 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
44
44
  const [error, setError] = useState<string | null>(null);
45
45
  const [loading, setLoading] = useState(false);
46
46
 
47
+ // Additional contact method collected on the new-user step.
48
+ // Kept separate so typing here doesn't affect the identifier-step conditions.
49
+ const [additionalPhone, setAdditionalPhone] = useState('');
50
+ const [additionalEmail, setAdditionalEmail] = useState('');
51
+ // Snapshot of which method the user identified with, set when moving to step 'new'.
52
+ const [identifiedWith, setIdentifiedWith] = useState<'email' | 'phone' | null>(null);
53
+
47
54
  // Phone helpers
48
55
  const country = useMemo(() => countries.find((c) => c.code === selectedCountry), [selectedCountry]);
49
56
  const phoneCode = country ? (country.phoneCode.startsWith('+') ? country.phoneCode : `+${country.phoneCode}`) : '+1';
@@ -59,17 +66,11 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
59
66
  const fullPhone = phoneDigits.length > 0 ? `${phoneCode}${phoneDigits}` : null;
60
67
  const emailVal = email.trim() || null;
61
68
 
62
- const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
63
- const digits = e.target.value.replace(/\D/g, '');
64
- const formatted = nationalMask ? formatDigitsToMask(digits, nationalMask) : digits;
65
- setPhoneValue(formatted);
66
- };
67
-
68
- const emailInput = (required = false) => (
69
+ const emailInput = (value: string, onChange: (v: string) => void, required = false) => (
69
70
  <input
70
71
  type="email"
71
- value={email}
72
- onChange={(e) => setEmail(e.target.value)}
72
+ value={value}
73
+ onChange={(e) => onChange(e.target.value)}
73
74
  placeholder="you@example.com"
74
75
  className={inputClass}
75
76
  autoComplete="email"
@@ -77,11 +78,11 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
77
78
  />
78
79
  );
79
80
 
80
- const phoneInput = (required = false) => (
81
+ const phoneInput = (value: string, onChange: (v: string) => void, required = false) => (
81
82
  <div className="flex rounded-input border border-primary overflow-hidden focus-within:border-brand focus-within:ring-1 focus-within:ring-brand transition-colors">
82
83
  <select
83
84
  value={selectedCountry}
84
- onChange={(e) => { setSelectedCountry(e.target.value); setPhoneValue(''); }}
85
+ onChange={(e) => { setSelectedCountry(e.target.value); onChange(''); }}
85
86
  className="border-r border-secondary bg-secondary px-2 py-2.5 text-base text-secondary focus:outline-none"
86
87
  aria-label="Country code"
87
88
  >
@@ -91,8 +92,11 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
91
92
  </select>
92
93
  <input
93
94
  type="tel"
94
- value={phoneValue}
95
- onChange={handlePhoneChange}
95
+ value={value}
96
+ onChange={(e) => {
97
+ const digits = e.target.value.replace(/\D/g, '');
98
+ onChange(nationalMask ? formatDigitsToMask(digits, nationalMask) : digits);
99
+ }}
96
100
  placeholder={nationalPlaceholder}
97
101
  className="flex-1 px-3 py-2.5 text-base text-primary placeholder-quaternary bg-transparent focus:outline-none"
98
102
  autoComplete="tel"
@@ -120,11 +124,17 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
120
124
  firePixelEvent('Lead');
121
125
  captureEvent('portal_login_identified', { method, user_exists: true });
122
126
  setWelcomeName(result.firstName ?? null);
123
- setStep(result.hasPassword === false ? 'new' : 'returning');
127
+ if (result.hasPassword === false) {
128
+ setIdentifiedWith(emailVal ? 'email' : 'phone');
129
+ setStep('new');
130
+ } else {
131
+ setStep('returning');
132
+ }
124
133
  } else if (result.exists === false) {
125
134
  await setPixelUserData({ email: emailVal, phone: fullPhone });
126
135
  firePixelEvent('Lead');
127
136
  captureEvent('portal_login_identified', { method, user_exists: false });
137
+ setIdentifiedWith(emailVal ? 'email' : 'phone');
128
138
  setStep('new');
129
139
  } else {
130
140
  captureEvent('portal_login_failed', { step: 'identifier', reason: 'unknown_error' });
@@ -177,12 +187,16 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
177
187
  setError(null);
178
188
  setLoading(true);
179
189
  try {
190
+ const additionalPhoneDigits = additionalPhone.replace(/\D/g, '');
191
+ const additionalFullPhone = additionalPhoneDigits.length > 0 ? `${phoneCode}${additionalPhoneDigits}` : null;
192
+ const additionalEmailVal = additionalEmail.trim() || null;
193
+
180
194
  const res = await fetch('/api/consumer/signup', {
181
195
  method: 'POST',
182
196
  headers: { 'Content-Type': 'application/json' },
183
197
  body: JSON.stringify({
184
- email: emailVal,
185
- phone: fullPhone,
198
+ email: emailVal ?? additionalEmailVal,
199
+ phone: fullPhone ?? additionalFullPhone,
186
200
  password,
187
201
  password_confirmation: passwordConfirm,
188
202
  first_name: firstName.trim() || undefined,
@@ -209,6 +223,9 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
209
223
  setStep('identifier');
210
224
  setPassword('');
211
225
  setPasswordConfirm('');
226
+ setAdditionalPhone('');
227
+ setAdditionalEmail('');
228
+ setIdentifiedWith(null);
212
229
  setError(null);
213
230
  };
214
231
 
@@ -257,7 +274,7 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
257
274
  <form onSubmit={handleContinue} className="space-y-3">
258
275
  <div>
259
276
  <label className={labelClass}>Email address</label>
260
- {emailInput()}
277
+ {emailInput(email, setEmail)}
261
278
  </div>
262
279
  <div className="flex items-center gap-3">
263
280
  <hr className="flex-1 border-secondary" />
@@ -266,7 +283,7 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
266
283
  </div>
267
284
  <div>
268
285
  <label className={labelClass}>Phone number</label>
269
- {phoneInput()}
286
+ {phoneInput(phoneValue, setPhoneValue)}
270
287
  </div>
271
288
  <div className="pt-1">
272
289
  <button
@@ -347,16 +364,16 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
347
364
  </div>
348
365
  )}
349
366
  {/* Collect the missing contact method so the business can reach out */}
350
- {emailVal && !fullPhone && (
367
+ {identifiedWith === 'email' && (
351
368
  <div>
352
369
  <label className={labelClass}>Phone number</label>
353
- {phoneInput(true)}
370
+ {phoneInput(additionalPhone, setAdditionalPhone, true)}
354
371
  </div>
355
372
  )}
356
- {fullPhone && !emailVal && (
373
+ {identifiedWith === 'phone' && (
357
374
  <div>
358
375
  <label className={labelClass}>Email address</label>
359
- {emailInput(true)}
376
+ {emailInput(additionalEmail, setAdditionalEmail, true)}
360
377
  </div>
361
378
  )}
362
379
  <div>
@@ -194,19 +194,23 @@ export async function KeystoneRootLayout(props: {
194
194
  jobApplicationFormDefinition={jobApplicationFormDefinition ?? null}
195
195
  marketingListSignupFormDefinition={marketingListSignupFormDefinition ?? null}
196
196
  >
197
- <HeaderNavigation
198
- config={dynamicConfig}
199
- companyInformation={companyInformation}
200
- websitePhotos={websitePhotos}
201
- props={headerProps}
202
- logoText={headerOverrides?.logoText}
203
- />
197
+ {theme !== 'custom' && (
198
+ <HeaderNavigation
199
+ config={dynamicConfig}
200
+ companyInformation={companyInformation}
201
+ websitePhotos={websitePhotos}
202
+ props={headerProps}
203
+ logoText={headerOverrides?.logoText}
204
+ />
205
+ )}
204
206
  {children}
205
- <FooterHome
206
- config={dynamicConfig}
207
- companyInformation={companyInformation}
208
- websitePhotos={websitePhotos}
209
- />
207
+ {theme !== 'custom' && (
208
+ <FooterHome
209
+ config={dynamicConfig}
210
+ companyInformation={companyInformation}
211
+ websitePhotos={websitePhotos}
212
+ />
213
+ )}
210
214
  {chatEnabled ? (
211
215
  <ChatWidget
212
216
  position={options?.chatPosition || 'bottom-right'}
@@ -8,6 +8,7 @@ export const THEME_CONFIG = {
8
8
  aman: '.aman', // Aman Hotels variant files (hero-home.aman.tsx)
9
9
  barelux: '.barelux', // Bare Lux Studio variant files (hero-home.barelux.tsx)
10
10
  balance: '.balance', // Balance Aesthetics variant files (hero-home.balance.tsx)
11
+ custom: '', // Fully custom sites — no design-system CSS loaded, all styling built in the site itself
11
12
  } as const;
12
13
 
13
14
  export type Theme = keyof typeof THEME_CONFIG;