keystone-design-bootstrap 1.0.59 → 1.0.60

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": "keystone-design-bootstrap",
3
- "version": "1.0.59",
3
+ "version": "1.0.60",
4
4
  "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -13,7 +13,7 @@ interface LoginFormProps {
13
13
  }
14
14
 
15
15
  const inputClass =
16
- 'block w-full rounded border border-gray-300 bg-primary px-3 py-2.5 text-sm text-primary placeholder-gray-400 focus:border-gray-700 focus:outline-none focus:ring-1 focus:ring-gray-700 transition-colors';
16
+ 'block w-full rounded-input border border-primary bg-primary px-3 py-2.5 text-sm text-primary placeholder-quaternary focus:border-brand focus:outline-none focus:ring-1 focus:ring-brand transition-colors';
17
17
  const labelClass = 'block text-sm text-secondary mb-1';
18
18
 
19
19
  function isValidEmail(value: string): boolean {
@@ -185,7 +185,7 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
185
185
  </div>
186
186
 
187
187
  {error && (
188
- <div className="mb-4 rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
188
+ <div className="mb-4 rounded-input border border-error bg-error-primary px-3 py-2 text-sm text-error-primary">
189
189
  {error}
190
190
  </div>
191
191
  )}
@@ -206,17 +206,17 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
206
206
  />
207
207
  </div>
208
208
  <div className="flex items-center gap-3">
209
- <hr className="flex-1 border-gray-200" />
210
- <span className="text-xs text-gray-400">or</span>
211
- <hr className="flex-1 border-gray-200" />
209
+ <hr className="flex-1 border-secondary" />
210
+ <span className="text-xs text-quaternary">or</span>
211
+ <hr className="flex-1 border-secondary" />
212
212
  </div>
213
213
  <div>
214
214
  <label className={labelClass}>Phone number</label>
215
- <div className="flex rounded border border-gray-300 overflow-hidden focus-within:border-gray-700 focus-within:ring-1 focus-within:ring-gray-700 transition-colors">
215
+ <div className="flex rounded-input border border-primary overflow-hidden focus-within:border-brand focus-within:ring-1 focus-within:ring-brand transition-colors">
216
216
  <select
217
217
  value={selectedCountry}
218
218
  onChange={(e) => { setSelectedCountry(e.target.value); setPhoneValue(''); }}
219
- className="border-r border-gray-200 bg-gray-50 px-2 py-2.5 text-sm text-gray-700 focus:outline-none"
219
+ className="border-r border-secondary bg-secondary px-2 py-2.5 text-sm text-secondary focus:outline-none"
220
220
  aria-label="Country code"
221
221
  >
222
222
  {countryOptions.map((opt) => (
@@ -228,7 +228,7 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
228
228
  value={phoneValue}
229
229
  onChange={handlePhoneChange}
230
230
  placeholder={nationalPlaceholder}
231
- className="flex-1 px-3 py-2.5 text-sm text-gray-900 placeholder-gray-400 bg-transparent focus:outline-none"
231
+ className="flex-1 px-3 py-2.5 text-sm text-primary placeholder-quaternary bg-transparent focus:outline-none"
232
232
  />
233
233
  </div>
234
234
  </div>
@@ -236,7 +236,7 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
236
236
  <button
237
237
  type="submit"
238
238
  disabled={loading}
239
- className="w-full rounded border border-secondary bg-primary px-4 py-2.5 text-sm font-medium text-primary hover:bg-secondary transition-colors disabled:opacity-50"
239
+ className="w-full rounded-interactive bg-brand-solid px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-solid_hover transition-colors disabled:opacity-50"
240
240
  >
241
241
  {loading ? 'Looking you up…' : 'Continue'}
242
242
  </button>
@@ -264,7 +264,7 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
264
264
  <button
265
265
  type="submit"
266
266
  disabled={loading}
267
- className="w-full rounded border border-secondary bg-primary px-4 py-2.5 text-sm font-medium text-primary hover:bg-secondary transition-colors disabled:opacity-50"
267
+ className="w-full rounded-interactive bg-brand-solid px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-solid_hover transition-colors disabled:opacity-50"
268
268
  >
269
269
  {loading ? 'Signing in…' : 'Sign in'}
270
270
  </button>
@@ -339,7 +339,7 @@ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
339
339
  <button
340
340
  type="submit"
341
341
  disabled={loading}
342
- className="w-full rounded border border-secondary bg-primary px-4 py-2.5 text-sm font-medium text-primary hover:bg-secondary transition-colors disabled:opacity-50"
342
+ className="w-full rounded-interactive bg-brand-solid px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-solid_hover transition-colors disabled:opacity-50"
343
343
  >
344
344
  {loading ? 'Creating account…' : 'Create account'}
345
345
  </button>
@@ -67,12 +67,12 @@ export function MessageComposer({ contactId }: { contactId: number }) {
67
67
  placeholder="Type a message… (Enter to send)"
68
68
  rows={1}
69
69
  disabled={isPending}
70
- className="flex-1 resize-none rounded-xl border border-gray-300 bg-primary px-3 py-2 text-sm text-primary placeholder-gray-400 focus:border-gray-700 focus:outline-none focus:ring-1 focus:ring-gray-700 transition-colors disabled:opacity-50"
70
+ className="flex-1 resize-none rounded-input border border-primary bg-primary px-3 py-2 text-sm text-primary placeholder-quaternary focus:border-brand focus:outline-none focus:ring-1 focus:ring-brand transition-colors disabled:opacity-50"
71
71
  />
72
72
  <button
73
73
  type="submit"
74
74
  disabled={!body.trim() || isPending}
75
- className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-gray-900 text-white transition-colors hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed"
75
+ className="flex h-9 w-9 shrink-0 items-center justify-center rounded-interactive bg-brand-solid text-white transition-colors hover:bg-brand-solid_hover disabled:opacity-40 disabled:cursor-not-allowed"
76
76
  aria-label="Send message"
77
77
  >
78
78
  {isPending ? (
@@ -165,7 +165,7 @@ function LoginWall({ message, cta = 'Sign in' }: { message: string; cta?: string
165
165
  <p className="text-sm font-medium text-primary">{message}</p>
166
166
  <button
167
167
  data-open-login-modal
168
- className="mt-4 cursor-pointer rounded-lg bg-gray-900 px-5 py-2 text-sm font-semibold text-white transition-colors hover:bg-gray-700"
168
+ className="mt-4 cursor-pointer rounded-interactive bg-brand-solid px-5 py-2 text-sm font-semibold text-white transition-colors hover:bg-brand-solid_hover"
169
169
  >
170
170
  {cta}
171
171
  </button>
@@ -175,7 +175,7 @@ function LoginWall({ message, cta = 'Sign in' }: { message: string; cta?: string
175
175
 
176
176
  function EmptyState({ message }: { message: string }) {
177
177
  return (
178
- <div className="rounded-xl border border-secondary bg-secondary py-16 text-center">
178
+ <div className="rounded-component border border-secondary bg-secondary py-16 text-center">
179
179
  <p className="text-sm text-tertiary">{message}</p>
180
180
  </div>
181
181
  );
@@ -214,7 +214,7 @@ function ServiceItemRow({
214
214
  <Link
215
215
  key={offer.id}
216
216
  href={specialsHref}
217
- className="inline-flex items-center rounded-full bg-brand-50 border border-brand-200 px-2 py-0.5 text-xs font-medium text-brand-700 hover:bg-brand-100 transition-colors"
217
+ className="inline-flex items-center rounded-badge bg-secondary border border-brand px-2 py-0.5 text-xs font-medium text-brand-secondary hover:bg-secondary_hover transition-colors"
218
218
  >
219
219
  Special: {offer.name}
220
220
  </Link>
@@ -223,7 +223,7 @@ function ServiceItemRow({
223
223
  key={offer.id}
224
224
  data-open-login-modal
225
225
  data-login-redirect={specialsHref}
226
- className="inline-flex items-center rounded-full bg-brand-50 border border-brand-200 px-2 py-0.5 text-xs font-medium text-brand-700 hover:bg-brand-100 transition-colors cursor-pointer"
226
+ className="inline-flex items-center rounded-badge bg-secondary border border-brand px-2 py-0.5 text-xs font-medium text-brand-secondary hover:bg-secondary_hover transition-colors cursor-pointer"
227
227
  >
228
228
  Special: {offer.name}
229
229
  </button>
@@ -267,7 +267,7 @@ function ServicesPanel({
267
267
  return (
268
268
  <>
269
269
  <PortalTabTracker event="ViewContent" params={{ contentName: 'Services', contentCategory: 'Services' }} />
270
- <div className="divide-y divide-tertiary rounded-xl border border-secondary bg-primary overflow-hidden">
270
+ <div className="divide-y divide-tertiary rounded-component border border-secondary bg-primary overflow-hidden">
271
271
  {activeServices.map((service) => (
272
272
  <details key={service.id} className="group">
273
273
  <summary className="flex cursor-pointer list-none items-center justify-between px-5 py-4 hover:bg-secondary transition-colors">
@@ -325,7 +325,7 @@ function PackagesPanel({
325
325
  {packages.map((pkg) => {
326
326
  const activeOffers = (pkg.offers ?? []).filter((o) => o.active !== false && !o.expired);
327
327
  return (
328
- <div key={pkg.id} className="group rounded-xl border border-secondary bg-primary p-4 flex flex-col gap-3">
328
+ <div key={pkg.id} className="group rounded-component border border-secondary bg-primary p-4 flex flex-col gap-3">
329
329
  <div className="flex items-start gap-3">
330
330
  <RowThumbnail
331
331
  photoAttachments={pkg.photo_attachments}
@@ -371,7 +371,7 @@ function PackagesPanel({
371
371
  <Link
372
372
  key={offer.id}
373
373
  href={specialsHref}
374
- className="inline-flex items-center rounded-full bg-brand-50 border border-brand-200 px-2.5 py-0.5 text-xs font-medium text-brand-700 hover:bg-brand-100 transition-colors"
374
+ className="inline-flex items-center rounded-badge bg-secondary border border-brand px-2.5 py-0.5 text-xs font-medium text-brand-secondary hover:bg-secondary_hover transition-colors"
375
375
  >
376
376
  Special: {offer.name}
377
377
  </Link>
@@ -380,7 +380,7 @@ function PackagesPanel({
380
380
  key={offer.id}
381
381
  data-open-login-modal
382
382
  data-login-redirect={specialsHref}
383
- className="inline-flex items-center rounded-full bg-brand-50 border border-brand-200 px-2.5 py-0.5 text-xs font-medium text-brand-700 hover:bg-brand-100 transition-colors cursor-pointer"
383
+ className="inline-flex items-center rounded-badge bg-secondary border border-brand px-2.5 py-0.5 text-xs font-medium text-brand-secondary hover:bg-secondary_hover transition-colors cursor-pointer"
384
384
  >
385
385
  Special: {offer.name}
386
386
  </button>
@@ -408,7 +408,7 @@ function SpecialsPanel({ specials }: { specials: SpecialItem[] }) {
408
408
  <PortalTabTracker event="ViewContent" params={{ contentName: 'Specials', contentCategory: 'Specials' }} />
409
409
  <div className="space-y-3">
410
410
  {specials.map((special) => (
411
- <div key={special.id} className="group flex items-start gap-3 rounded-xl border border-secondary bg-primary px-4 py-4">
411
+ <div key={special.id} className="group flex items-start gap-3 rounded-component border border-secondary bg-primary px-4 py-4">
412
412
  <RowThumbnail
413
413
  photoAttachments={special.photoAttachments}
414
414
  seed={`special-${special.id}`}
@@ -454,10 +454,10 @@ function MessagesPanel({
454
454
  businessName;
455
455
 
456
456
  return (
457
- <div className="flex flex-col rounded-xl border border-secondary bg-primary overflow-hidden">
457
+ <div className="flex flex-col rounded-component border border-secondary bg-primary overflow-hidden">
458
458
  {/* Thread header */}
459
459
  <div className="flex items-center gap-2.5 border-b border-secondary px-4 py-3 shrink-0">
460
- <div className="flex h-7 w-7 items-center justify-center rounded-full bg-gray-900 text-xs font-semibold text-white shrink-0">
460
+ <div className="flex h-7 w-7 items-center justify-center rounded-full bg-brand-solid text-xs font-semibold text-white shrink-0">
461
461
  {getInitials(threadBusiness)}
462
462
  </div>
463
463
  <span className="text-sm font-semibold text-primary">{threadBusiness}</span>
@@ -480,8 +480,8 @@ function MessagesPanel({
480
480
  const isOutbound = m.direction === 'outbound';
481
481
  return (
482
482
  <div key={m.id} className={`flex ${isOutbound ? 'justify-start' : 'justify-end'}`}>
483
- <div className={`max-w-[80%] rounded-2xl px-4 py-2.5 text-sm ${
484
- isOutbound ? 'bg-secondary text-primary rounded-tl-sm' : 'bg-gray-900 text-white rounded-tr-sm'
483
+ <div className={`max-w-[80%] rounded-component px-4 py-2.5 text-sm ${
484
+ isOutbound ? 'bg-secondary text-primary' : 'bg-brand-solid text-white'
485
485
  }`}>
486
486
  {m.sender_display_name && (
487
487
  <p className="mb-0.5 text-xs font-medium opacity-60">{m.sender_display_name}</p>
@@ -522,7 +522,7 @@ function BookPanel({
522
522
  return (
523
523
  <>
524
524
  <PortalTabTracker event="InitiateCheckout" />
525
- <div className="rounded-xl border border-secondary overflow-hidden" style={{ height: '70vh' }}>
525
+ <div className="rounded-component border border-secondary overflow-hidden" style={{ height: '70vh' }}>
526
526
  <iframe
527
527
  src={bookingHref}
528
528
  className="w-full h-full"
@@ -549,7 +549,7 @@ function BookPanel({
549
549
  href={bookingHref}
550
550
  target="_blank"
551
551
  rel="noopener noreferrer"
552
- className="mt-5 inline-flex items-center gap-2 rounded-lg bg-gray-900 px-6 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-gray-700"
552
+ className="mt-5 inline-flex items-center gap-2 rounded-interactive bg-brand-solid px-6 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-brand-solid_hover"
553
553
  >
554
554
  {bookingLabel}
555
555
  <svg className="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
@@ -659,7 +659,7 @@ export async function PortalPage({
659
659
  <div className="flex items-center gap-3">
660
660
  {consumerDisplayName && (
661
661
  <div className="hidden sm:flex items-center gap-2 rounded-full border border-secondary bg-secondary px-3 py-1.5">
662
- <div className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-900 text-[9px] font-bold text-white shrink-0">
662
+ <div className="flex h-5 w-5 items-center justify-center rounded-full bg-brand-solid text-[9px] font-bold text-white shrink-0">
663
663
  {getInitials(consumerDisplayName)}
664
664
  </div>
665
665
  <span className="text-xs font-medium text-secondary max-w-[120px] truncate">
@@ -672,7 +672,7 @@ export async function PortalPage({
672
672
  ) : (
673
673
  <button
674
674
  data-open-login-modal
675
- className="cursor-pointer rounded-lg border border-secondary px-3 py-1.5 text-xs font-medium text-secondary hover:bg-secondary transition-colors"
675
+ className="cursor-pointer rounded-interactive border border-secondary px-3 py-1.5 text-xs font-medium text-secondary hover:bg-secondary transition-colors"
676
676
  >
677
677
  Sign in
678
678
  </button>
@@ -691,8 +691,8 @@ export async function PortalPage({
691
691
  const isActive = tab === t.id;
692
692
  const isGated = !isLoggedIn && (t.id === 'specials' || t.id === 'messages' || t.id === 'book');
693
693
  const href = `${portalHref}?tab=${t.id}`;
694
- const className = `shrink-0 rounded-lg px-4 py-2 text-sm font-medium transition-colors whitespace-nowrap ${
695
- isActive ? 'bg-gray-900 text-white' : 'text-secondary hover:bg-secondary hover:text-primary'
694
+ const className = `shrink-0 rounded-interactive px-4 py-2 text-sm font-medium transition-colors whitespace-nowrap ${
695
+ isActive ? 'bg-brand-solid text-white' : 'text-secondary hover:bg-secondary hover:text-primary'
696
696
  }`;
697
697
  if (isGated) {
698
698
  return (
@@ -26,7 +26,7 @@ export function RowThumbnail({
26
26
 
27
27
  if (list.length === 0) {
28
28
  return (
29
- <div className={`${sizeClassName} shrink-0 rounded-lg bg-secondary border border-tertiary flex items-center justify-center`}>
29
+ <div className={`${sizeClassName} shrink-0 rounded-component bg-secondary border border-tertiary flex items-center justify-center`}>
30
30
  <svg className="size-5 text-quaternary" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
31
31
  <path strokeLinecap="round" strokeLinejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
32
32
  </svg>
@@ -36,7 +36,7 @@ export function RowThumbnail({
36
36
 
37
37
  if (list.length === 1) {
38
38
  return (
39
- <div className={`${sizeClassName} shrink-0 rounded-lg overflow-hidden`}>
39
+ <div className={`${sizeClassName} shrink-0 rounded-component overflow-hidden`}>
40
40
  {/* eslint-disable-next-line @next/next/no-img-element */}
41
41
  <img
42
42
  src={list[0]!.url}
@@ -48,7 +48,7 @@ export function RowThumbnail({
48
48
  }
49
49
 
50
50
  return (
51
- <div className={`${sizeClassName} shrink-0 rounded-lg overflow-hidden relative`}>
51
+ <div className={`${sizeClassName} shrink-0 rounded-component overflow-hidden relative`}>
52
52
  <div
53
53
  className={`absolute inset-0 ${transitioning ? 'transition-opacity ease-in-out' : 'transition-none'}`}
54
54
  style={{ opacity: transitioning ? 0 : 1, ...(transitioning ? CROSSFADE_STYLE : {}) }}
@@ -40,6 +40,12 @@
40
40
  /* Border colors */
41
41
  --color-border-primary: #E5E2D9;
42
42
  --color-border-secondary: #D5D2C9;
43
+
44
+ /* Radius — minimal/sharp aesthetic */
45
+ --radius-component: 0.25rem;
46
+ --radius-interactive: 0.25rem;
47
+ --radius-input: 0.25rem;
48
+ --radius-badge: 0.25rem;
43
49
  }
44
50
 
45
51
  /* Body text uses Inter */
@@ -40,6 +40,12 @@
40
40
  /* Border colors */
41
41
  --color-border-primary: #EEEAE7;
42
42
  --color-border-secondary: #E5E1DE;
43
+
44
+ /* Radius — soft/rounded aesthetic */
45
+ --radius-component: 0.75rem;
46
+ --radius-interactive: 0.5rem;
47
+ --radius-input: 0.5rem;
48
+ --radius-badge: 9999px;
43
49
  }
44
50
 
45
51
  /* Body text uses Poppins */
@@ -54,6 +54,12 @@
54
54
  --radius-3xl: 1.5rem;
55
55
  --radius-full: 9999px;
56
56
 
57
+ /* Semantic radius tokens — override per theme to get consistent rounding everywhere */
58
+ --radius-component: 0.5rem; /* cards, panels, containers */
59
+ --radius-interactive: 0.375rem; /* buttons, tabs */
60
+ --radius-input: 0.375rem; /* form inputs */
61
+ --radius-badge: 9999px; /* pill badges / tags */
62
+
57
63
  /* ==================== SHADOWS ==================== */
58
64
  --shadow-xs: 0px 1px 2px rgba(10, 13, 18, 0.05);
59
65
  --shadow-sm: 0px 1px 3px rgba(10, 13, 18, 0.1), 0px 1px 2px -1px rgba(10, 13, 18, 0.1);