@wealthx/shadcn 1.5.32 → 1.5.33

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.
Files changed (58) hide show
  1. package/.turbo/turbo-build.log +92 -92
  2. package/CHANGELOG.md +6 -0
  3. package/dist/{chunk-SYJ6LVJ6.mjs → chunk-3ZU5BH6X.mjs} +1 -1
  4. package/dist/{chunk-FTQ2AKZ2.mjs → chunk-4QTHK7ML.mjs} +1 -1
  5. package/dist/{chunk-T5HU4S4X.mjs → chunk-C7ZTZTEW.mjs} +1 -1
  6. package/dist/{chunk-KI57CBJR.mjs → chunk-DQNNP6I4.mjs} +33 -24
  7. package/dist/{chunk-AE4JKISB.mjs → chunk-EEI4FLEE.mjs} +1 -1
  8. package/dist/{chunk-IEQX4UVP.mjs → chunk-EY36WDCF.mjs} +1 -1
  9. package/dist/{chunk-HB5BKRMH.mjs → chunk-F3CU6KEI.mjs} +11 -1
  10. package/dist/chunk-H65NB7KI.mjs +182 -0
  11. package/dist/{chunk-TRM3KIHT.mjs → chunk-ICCPK3J2.mjs} +1 -1
  12. package/dist/{chunk-KGVVK6OS.mjs → chunk-ORMC3TV3.mjs} +3 -1
  13. package/dist/{chunk-HSXMTFIM.mjs → chunk-UD5UF5OC.mjs} +1 -1
  14. package/dist/{chunk-IW33VLL5.mjs → chunk-X3VEDQPO.mjs} +7 -3
  15. package/dist/{chunk-AAZSLTER.mjs → chunk-XGRSPFFC.mjs} +16 -7
  16. package/dist/components/ui/about-you-form.js +9 -6
  17. package/dist/components/ui/about-you-form.mjs +2 -2
  18. package/dist/components/ui/ai-conversations/index.js +4 -1
  19. package/dist/components/ui/ai-conversations/index.mjs +2 -2
  20. package/dist/components/ui/appointment-availability-settings.js +24 -12
  21. package/dist/components/ui/appointment-availability-settings.mjs +3 -3
  22. package/dist/components/ui/appointment-book-dialog.js +33 -24
  23. package/dist/components/ui/appointment-book-dialog.mjs +1 -1
  24. package/dist/components/ui/appointment-detail-sheet.js +3 -1
  25. package/dist/components/ui/appointment-detail-sheet.mjs +1 -1
  26. package/dist/components/ui/appointment-gmail-connect.js +127 -70
  27. package/dist/components/ui/appointment-gmail-connect.mjs +1 -1
  28. package/dist/components/ui/backoffice-signup-steps.js +23 -16
  29. package/dist/components/ui/backoffice-signup-steps.mjs +3 -3
  30. package/dist/components/ui/bank-statement-generate-dialog.js +12 -9
  31. package/dist/components/ui/bank-statement-generate-dialog.mjs +3 -3
  32. package/dist/components/ui/color-picker.js +21 -14
  33. package/dist/components/ui/color-picker.mjs +2 -2
  34. package/dist/components/ui/date-picker.js +6 -3
  35. package/dist/components/ui/date-picker.mjs +2 -2
  36. package/dist/components/ui/opportunity-edit-modals.js +45 -42
  37. package/dist/components/ui/opportunity-edit-modals.mjs +3 -3
  38. package/dist/components/ui/opportunity-summary-tab.js +48 -45
  39. package/dist/components/ui/opportunity-summary-tab.mjs +4 -4
  40. package/dist/components/ui/pipeline-dialogs.js +12 -9
  41. package/dist/components/ui/pipeline-dialogs.mjs +3 -3
  42. package/dist/components/ui/popover.js +22 -1
  43. package/dist/components/ui/popover.mjs +3 -1
  44. package/dist/components/ui/savings-goal-modal.js +11 -8
  45. package/dist/components/ui/savings-goal-modal.mjs +2 -2
  46. package/dist/index.js +349 -257
  47. package/dist/index.mjs +15 -13
  48. package/dist/styles.css +1 -1
  49. package/package.json +1 -1
  50. package/src/components/index.tsx +4 -0
  51. package/src/components/ui/appointment-availability-settings.tsx +32 -19
  52. package/src/components/ui/appointment-book-dialog.tsx +52 -73
  53. package/src/components/ui/appointment-detail-sheet.tsx +3 -1
  54. package/src/components/ui/appointment-gmail-connect.tsx +89 -29
  55. package/src/components/ui/color-picker.tsx +12 -4
  56. package/src/components/ui/popover.tsx +33 -2
  57. package/src/styles/styles-css.ts +1 -1
  58. package/dist/chunk-7TMPOZDE.mjs +0 -122
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wealthx/shadcn",
3
- "version": "1.5.32",
3
+ "version": "1.5.33",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -216,6 +216,7 @@ export type {
216
216
  AppointmentBookingSlot,
217
217
  AppointmentClient,
218
218
  AppointmentOfflineLocation,
219
+ AppointmentOnlinePlatform,
219
220
  } from "./ui/appointment-book-dialog";
220
221
 
221
222
  export { AppointmentCalendarView } from "./ui/appointment-calendar-view";
@@ -236,6 +237,7 @@ export type {
236
237
  export { AppointmentGmailConnect } from "./ui/appointment-gmail-connect";
237
238
  export type {
238
239
  AppointmentGmailConnectProps,
240
+ EmailProvider,
239
241
  GmailConnection,
240
242
  } from "./ui/appointment-gmail-connect";
241
243
 
@@ -951,6 +953,7 @@ export {
951
953
  PopoverHeader,
952
954
  PopoverTitle,
953
955
  PopoverDescription,
956
+ PopoverPortalProvider,
954
957
  } from "./ui/popover";
955
958
  export type {
956
959
  PopoverProps,
@@ -960,6 +963,7 @@ export type {
960
963
  PopoverHeaderProps,
961
964
  PopoverTitleProps,
962
965
  PopoverDescriptionProps,
966
+ PopoverPortalProviderProps,
963
967
  } from "./ui/popover";
964
968
 
965
969
  export { PageHeader } from "./ui/page-header";
@@ -88,9 +88,11 @@ export interface AppointmentAvailabilitySettingsProps {
88
88
  publicHolidays?: AppointmentBlockedDate[];
89
89
  onSave?: (
90
90
  schedule: AppointmentDaySchedule[],
91
- prefs: AppointmentAvailabilityPrefs,
91
+ prefs: AppointmentAvailabilityPrefs
92
92
  ) => void;
93
93
  onBlockedDatesChange?: (dates: AppointmentBlockedDate[]) => void;
94
+ /** Fired immediately whenever any booking preference value changes (before Save). */
95
+ onPrefsChange?: (prefs: AppointmentAvailabilityPrefs) => void;
94
96
  }
95
97
 
96
98
  // ---------------------------------------------------------------------------
@@ -376,21 +378,22 @@ export function AppointmentAvailabilitySettings({
376
378
  publicHolidays: publicHolidaysProp,
377
379
  onSave,
378
380
  onBlockedDatesChange,
381
+ onPrefsChange,
379
382
  }: AppointmentAvailabilitySettingsProps) {
380
383
  const [schedule, setSchedule] =
381
384
  React.useState<AppointmentDaySchedule[]>(initialSchedule);
382
385
 
383
386
  const [meetingDuration, setMeetingDuration] = React.useState(
384
- prefsProp?.meetingDuration ?? "30",
387
+ prefsProp?.meetingDuration ?? "30"
385
388
  );
386
389
  const [schedulingBuffer, setSchedulingBuffer] = React.useState(
387
- prefsProp?.schedulingBuffer ?? "0",
390
+ prefsProp?.schedulingBuffer ?? "0"
388
391
  );
389
392
  const [maxSlotsPerDay, setMaxSlotsPerDay] = React.useState(
390
- prefsProp?.maxSlotsPerDay ?? "8",
393
+ prefsProp?.maxSlotsPerDay ?? "8"
391
394
  );
392
395
  const [timezone, setTimezone] = React.useState(
393
- prefsProp?.timezone ?? "Australia/Sydney",
396
+ prefsProp?.timezone ?? "Australia/Sydney"
394
397
  );
395
398
  const [defaultMeetingPlatform, setDefaultMeetingPlatform] = React.useState<
396
399
  "google-meet" | "microsoft-teams" | "any"
@@ -416,7 +419,7 @@ export function AppointmentAvailabilitySettings({
416
419
  }
417
420
  });
418
421
  return entries.sort((a, b) => a.date.localeCompare(b.date));
419
- },
422
+ }
420
423
  );
421
424
  const [addMoreOpen, setAddMoreOpen] = React.useState(false);
422
425
 
@@ -443,11 +446,12 @@ export function AppointmentAvailabilitySettings({
443
446
  maxSlotsPerDay,
444
447
  timezone,
445
448
  defaultMeetingPlatform,
446
- ],
449
+ ]
447
450
  );
448
451
 
449
452
  const saveGuard = React.useRef(false);
450
453
  const timeOffGuard = React.useRef(false);
454
+ const prefsChangeGuard = React.useRef(false);
451
455
 
452
456
  React.useEffect(() => {
453
457
  if (!saveGuard.current) {
@@ -458,6 +462,15 @@ export function AppointmentAvailabilitySettings({
458
462
  // eslint-disable-next-line react-hooks/exhaustive-deps
459
463
  }, [schedule, currentPrefs]);
460
464
 
465
+ React.useEffect(() => {
466
+ if (!prefsChangeGuard.current) {
467
+ prefsChangeGuard.current = true;
468
+ return;
469
+ }
470
+ onPrefsChange?.(currentPrefs);
471
+ // eslint-disable-next-line react-hooks/exhaustive-deps
472
+ }, [currentPrefs]);
473
+
461
474
  React.useEffect(() => {
462
475
  if (!timeOffGuard.current) {
463
476
  timeOffGuard.current = true;
@@ -481,17 +494,17 @@ export function AppointmentAvailabilitySettings({
481
494
 
482
495
  const toggleDay = (index: number) => {
483
496
  setSchedule((prev) =>
484
- prev.map((d, i) => (i === index ? { ...d, enabled: !d.enabled } : d)),
497
+ prev.map((d, i) => (i === index ? { ...d, enabled: !d.enabled } : d))
485
498
  );
486
499
  };
487
500
 
488
501
  const updateTime = (
489
502
  index: number,
490
503
  field: "startTime" | "endTime",
491
- value: string,
504
+ value: string
492
505
  ) => {
493
506
  setSchedule((prev) =>
494
- prev.map((d, i) => (i === index ? { ...d, [field]: value } : d)),
507
+ prev.map((d, i) => (i === index ? { ...d, [field]: value } : d))
495
508
  );
496
509
  };
497
510
 
@@ -501,7 +514,7 @@ export function AppointmentAvailabilitySettings({
501
514
 
502
515
  const toggleTimeOff = (date: string, enabled: boolean) => {
503
516
  setTimeOffEntries((prev) =>
504
- prev.map((e) => (e.date === date ? { ...e, enabled } : e)),
517
+ prev.map((e) => (e.date === date ? { ...e, enabled } : e))
505
518
  );
506
519
  };
507
520
 
@@ -522,7 +535,7 @@ export function AppointmentAvailabilitySettings({
522
535
  timeStart: entry.timeStart,
523
536
  timeEnd: entry.timeEnd,
524
537
  },
525
- ].sort((a, b) => a.date.localeCompare(b.date)),
538
+ ].sort((a, b) => a.date.localeCompare(b.date))
526
539
  );
527
540
  };
528
541
 
@@ -592,7 +605,7 @@ export function AppointmentAvailabilitySettings({
592
605
  value={meetingDuration}
593
606
  onValueChange={(v) => setMeetingDuration(v as string)}
594
607
  >
595
- <SelectTrigger className="w-40">
608
+ <SelectTrigger className="w-64">
596
609
  <SelectValue>
597
610
  {(v) => selectLabel(MEETING_DURATION_OPTIONS, v)}
598
611
  </SelectValue>
@@ -615,7 +628,7 @@ export function AppointmentAvailabilitySettings({
615
628
  value={schedulingBuffer}
616
629
  onValueChange={(v) => setSchedulingBuffer(v as string)}
617
630
  >
618
- <SelectTrigger className="w-40">
631
+ <SelectTrigger className="w-64">
619
632
  <SelectValue>
620
633
  {(v) => selectLabel(SCHEDULING_BUFFER_OPTIONS, v)}
621
634
  </SelectValue>
@@ -638,7 +651,7 @@ export function AppointmentAvailabilitySettings({
638
651
  value={maxSlotsPerDay}
639
652
  onValueChange={(v) => setMaxSlotsPerDay(v as string)}
640
653
  >
641
- <SelectTrigger className="w-40">
654
+ <SelectTrigger className="w-64">
642
655
  <SelectValue>
643
656
  {(v) => selectLabel(MAX_SLOTS_OPTIONS, v)}
644
657
  </SelectValue>
@@ -661,7 +674,7 @@ export function AppointmentAvailabilitySettings({
661
674
  value={timezone}
662
675
  onValueChange={(v) => setTimezone(v as string)}
663
676
  >
664
- <SelectTrigger className="w-56">
677
+ <SelectTrigger className="w-64">
665
678
  <SelectValue>
666
679
  {(v) => selectLabel(TIMEZONE_OPTIONS, v)}
667
680
  </SelectValue>
@@ -684,11 +697,11 @@ export function AppointmentAvailabilitySettings({
684
697
  value={defaultMeetingPlatform}
685
698
  onValueChange={(v) =>
686
699
  setDefaultMeetingPlatform(
687
- v as "google-meet" | "microsoft-teams" | "any",
700
+ v as "google-meet" | "microsoft-teams" | "any"
688
701
  )
689
702
  }
690
703
  >
691
- <SelectTrigger className="w-48">
704
+ <SelectTrigger className="w-64">
692
705
  <SelectValue>
693
706
  {(v) => selectLabel(MEETING_PLATFORM_OPTIONS, v)}
694
707
  </SelectValue>
@@ -744,7 +757,7 @@ export function AppointmentAvailabilitySettings({
744
757
  htmlFor={`toff-${entry.date}`}
745
758
  className={cn(
746
759
  "cursor-pointer text-sm font-medium",
747
- !entry.enabled && "text-muted-foreground",
760
+ !entry.enabled && "text-muted-foreground"
748
761
  )}
749
762
  >
750
763
  {entry.label ?? formattedDate}
@@ -145,19 +145,16 @@ export interface AppointmentBookDialogProps {
145
145
  */
146
146
  defaultMeetingFormat?: AppointmentMeetingFormat;
147
147
  /**
148
- * Which online meeting platform the advisor has integrated.
148
+ * Which online meeting platform(s) the advisor has integrated.
149
149
  *
150
+ * - `"online"` — 1 email connected; show a generic "Online Meeting" button
150
151
  * - `"google-meet"` — show only Google Meet
151
152
  * - `"microsoft-teams"` — show only MS Teams
152
- * - `"any"` — show both Google Meet and MS Teams
153
+ * - `"any"` — 2 emails connected; show both Google Meet and MS Teams
153
154
  *
154
- * In **client mode**, this replaces the generic "Online Meeting" option
155
- * with the specific platform(s). In **advisor mode** this has no effect
156
- * (advisors always see all four formats).
157
- *
158
- * When omitted in client mode, the generic "Online Meeting" option is shown.
155
+ * When omitted, no online option is shown (advisor has no email integration).
159
156
  */
160
- onlinePlatform?: "google-meet" | "microsoft-teams" | "any";
157
+ onlinePlatform?: AppointmentOnlinePlatform;
161
158
  /**
162
159
  * Guest identity for **client / public booking mode** only.
163
160
  * When all fields are provided the guest form is hidden; when any field is
@@ -303,69 +300,49 @@ interface FormatOption {
303
300
  icon: React.ReactNode;
304
301
  }
305
302
 
306
- /** All four formats — shown in advisor mode */
307
- const FORMAT_OPTIONS: FormatOption[] = [
308
- { value: "call", label: "Call", icon: <Phone className="h-4 w-4" /> },
309
- {
310
- value: "google-meet",
311
- label: "Google Meet",
312
- icon: <Video className="h-4 w-4" />,
313
- },
314
- {
315
- value: "microsoft-teams",
316
- label: "MS Teams",
317
- icon: <Users className="h-4 w-4" />,
318
- },
319
- {
320
- value: "offline",
321
- label: "Offline",
322
- icon: <MapPin className="h-4 w-4" />,
323
- },
324
- ];
325
-
326
- /** Simplified three-option set shown in client mode */
327
- const CLIENT_FORMAT_OPTIONS: FormatOption[] = [
328
- { value: "call", label: "Call", icon: <Phone className="h-4 w-4" /> },
329
- {
330
- value: "online",
331
- label: "Online Meeting",
332
- icon: <Video className="h-4 w-4" />,
333
- },
334
- {
335
- value: "offline",
336
- label: "Offline Meeting",
337
- icon: <MapPin className="h-4 w-4" />,
338
- },
339
- ];
303
+ export type AppointmentOnlinePlatform =
304
+ | "online"
305
+ | "google-meet"
306
+ | "microsoft-teams"
307
+ | "any";
308
+
309
+ const FMT_CALL: FormatOption = {
310
+ value: "call",
311
+ label: "Call",
312
+ icon: <Phone className="h-4 w-4" />,
313
+ };
314
+ const FMT_ONLINE: FormatOption = {
315
+ value: "online",
316
+ label: "Online Meeting",
317
+ icon: <Video className="h-4 w-4" />,
318
+ };
319
+ const FMT_GOOGLE_MEET: FormatOption = {
320
+ value: "google-meet",
321
+ label: "Google Meet",
322
+ icon: <Video className="h-4 w-4" />,
323
+ };
324
+ const FMT_MS_TEAMS: FormatOption = {
325
+ value: "microsoft-teams",
326
+ label: "MS Teams",
327
+ icon: <Users className="h-4 w-4" />,
328
+ };
329
+ const FMT_OFFLINE: FormatOption = {
330
+ value: "offline",
331
+ label: "Offline Meeting",
332
+ icon: <MapPin className="h-4 w-4" />,
333
+ };
340
334
 
341
335
  function getFormatOptions(
342
- platform?: "google-meet" | "microsoft-teams" | "any",
336
+ platform?: AppointmentOnlinePlatform
343
337
  ): FormatOption[] {
344
- const call: FormatOption = {
345
- value: "call",
346
- label: "Call",
347
- icon: <Phone className="h-4 w-4" />,
348
- };
349
- const googleMeet: FormatOption = {
350
- value: "google-meet",
351
- label: "Google Meet",
352
- icon: <Video className="h-4 w-4" />,
353
- };
354
- const msTeams: FormatOption = {
355
- value: "microsoft-teams",
356
- label: "MS Teams",
357
- icon: <Users className="h-4 w-4" />,
358
- };
359
- const offline: FormatOption = {
360
- value: "offline",
361
- label: "Offline Meeting",
362
- icon: <MapPin className="h-4 w-4" />,
363
- };
364
-
365
- if (platform === "google-meet") return [call, googleMeet, offline];
366
- if (platform === "microsoft-teams") return [call, msTeams, offline];
367
- if (platform === "any") return [call, googleMeet, msTeams, offline];
368
- return [call, offline];
338
+ if (platform === "online") return [FMT_CALL, FMT_ONLINE, FMT_OFFLINE];
339
+ if (platform === "google-meet")
340
+ return [FMT_CALL, FMT_GOOGLE_MEET, FMT_OFFLINE];
341
+ if (platform === "microsoft-teams")
342
+ return [FMT_CALL, FMT_MS_TEAMS, FMT_OFFLINE];
343
+ if (platform === "any")
344
+ return [FMT_CALL, FMT_GOOGLE_MEET, FMT_MS_TEAMS, FMT_OFFLINE];
345
+ return [FMT_CALL, FMT_OFFLINE];
369
346
  }
370
347
 
371
348
  function MeetingFormatSection({
@@ -474,7 +451,9 @@ function SummaryRow({
474
451
  }) {
475
452
  return (
476
453
  <div
477
- className={`flex items-center justify-between px-4 py-3 ${border ? "border-b border-border" : ""}`}
454
+ className={`flex items-center justify-between px-4 py-3 ${
455
+ border ? "border-b border-border" : ""
456
+ }`}
478
457
  >
479
458
  <span className="text-xs text-muted-foreground">{label}</span>
480
459
  {typeof value === "string" ? (
@@ -578,7 +557,7 @@ export function AppointmentBookDialog({
578
557
  ?.filter((d) => !d.enabled)
579
558
  .map((d) => DAY_MAP[d.day])
580
559
  .filter((n): n is number => n !== undefined),
581
- [schedule],
560
+ [schedule]
582
561
  );
583
562
 
584
563
  // Stable "today" reference — avoids prop identity churn on CalendarPicker each render
@@ -590,7 +569,7 @@ export function AppointmentBookDialog({
590
569
  React.useState<AppointmentMeetingFormat>(defaultMeetingFormat ?? "call");
591
570
  const [offlineLocation, setOfflineLocation] =
592
571
  React.useState<AppointmentOfflineLocation>(
593
- advisorOfficeAddress ? "office" : "custom",
572
+ advisorOfficeAddress ? "office" : "custom"
594
573
  );
595
574
  const [customAddress, setCustomAddress] = React.useState("");
596
575
  const [date, setDate] = React.useState<Date | undefined>(() => new Date());
@@ -599,7 +578,7 @@ export function AppointmentBookDialog({
599
578
  >(undefined);
600
579
  const [notes, setNotes] = React.useState("");
601
580
  const [selectedPhone, setSelectedPhone] = React.useState<string | undefined>(
602
- undefined,
581
+ undefined
603
582
  );
604
583
 
605
584
  // Guest identity — client / public booking mode only
@@ -613,7 +592,7 @@ export function AppointmentBookDialog({
613
592
 
614
593
  const selectedClient = React.useMemo(
615
594
  () => clients.find((c) => c.id === clientId),
616
- [clients, clientId],
595
+ [clients, clientId]
617
596
  );
618
597
 
619
598
  // Pre-select client when dialog opens with an initialClientId (e.g. rebook after cancellation)
@@ -100,7 +100,7 @@ export interface AppointmentDetailSheetProps {
100
100
  id: string,
101
101
  date: Date,
102
102
  slot: AppointmentTimeSlot,
103
- note: string,
103
+ note: string
104
104
  ) => void;
105
105
  /** Called when "Book a New Appointment" is clicked on a cancelled appointment */
106
106
  onBookNew?: (clientId?: string) => void;
@@ -159,6 +159,7 @@ const MEETING_FORMAT_META: Record<
159
159
  icon: <Users className={ICON_CLASS} />,
160
160
  label: "Microsoft Teams",
161
161
  },
162
+ online: { icon: <Video className={ICON_CLASS} />, label: "Online Meeting" },
162
163
  offline: { icon: <MapPin className={ICON_CLASS} />, label: "In Person" },
163
164
  };
164
165
 
@@ -166,6 +167,7 @@ const MEETING_FORMAT_META: Record<
166
167
  const ONLINE_FORMATS = new Set<AppointmentMeetingFormat>([
167
168
  "google-meet",
168
169
  "microsoft-teams",
170
+ "online",
169
171
  ]);
170
172
 
171
173
  // ---------------------------------------------------------------------------
@@ -1,3 +1,4 @@
1
+ import { cn } from "@/lib/utils";
1
2
  import { Badge } from "./badge";
2
3
  import { Button } from "./button";
3
4
  import { Input } from "./input";
@@ -8,6 +9,8 @@ import { Check, Copy, ExternalLink, Link2, Mail, Settings } from "lucide-react";
8
9
  // Types
9
10
  // ---------------------------------------------------------------------------
10
11
 
12
+ export type EmailProvider = "gmail" | "outlook";
13
+
11
14
  export interface GmailConnection {
12
15
  connected: boolean;
13
16
  email?: string;
@@ -16,13 +19,20 @@ export interface GmailConnection {
16
19
  }
17
20
 
18
21
  export interface AppointmentGmailConnectProps {
19
- /** Global Gmail connection state sourced from Settings > Integrations */
22
+ /**
23
+ * Email provider that is connected. Determines displayed labels.
24
+ * Defaults to "gmail".
25
+ */
26
+ provider?: EmailProvider;
27
+ /** Email connection state sourced from Settings > Integrations */
20
28
  connection: GmailConnection;
21
29
  /** Booking link to display when connected */
22
30
  calendarLink?: string;
31
+ /** Highlights the card with a primary-colored border — used when this provider matches the default meeting platform. */
32
+ highlighted?: boolean;
23
33
  /** Called when user clicks the copy button next to the calendar link */
24
34
  onCopyLink?: () => void;
25
- /** Called when user clicks "Go to Integrations" in the not-connected state */
35
+ /** Called when user clicks "Go to Integrations" */
26
36
  onGoToIntegrations?: () => void;
27
37
  }
28
38
 
@@ -30,6 +40,38 @@ export interface AppointmentGmailConnectProps {
30
40
  // Constants
31
41
  // ---------------------------------------------------------------------------
32
42
 
43
+ function GmailIcon({ className }: { className?: string }) {
44
+ return (
45
+ <svg viewBox="0 0 24 24" className={className} aria-hidden="true">
46
+ <path
47
+ fill="#EA4335"
48
+ d="M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z"
49
+ />
50
+ </svg>
51
+ );
52
+ }
53
+
54
+ function OutlookIcon({ className }: { className?: string }) {
55
+ return (
56
+ <svg viewBox="0 0 24 24" className={className} aria-hidden="true">
57
+ <path
58
+ fill="#0078D4"
59
+ d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.9-1.801"
60
+ />
61
+ </svg>
62
+ );
63
+ }
64
+
65
+ type IconComponent = (props: { className?: string }) => JSX.Element;
66
+
67
+ const PROVIDER_META: Record<
68
+ EmailProvider,
69
+ { label: string; iconBg: string; Icon: IconComponent }
70
+ > = {
71
+ gmail: { label: "Gmail", iconBg: "bg-[#EA4335]/10", Icon: GmailIcon },
72
+ outlook: { label: "Outlook", iconBg: "bg-[#0078D4]/10", Icon: OutlookIcon },
73
+ };
74
+
33
75
  const PERMISSIONS = [
34
76
  "Read your calendar availability",
35
77
  "Send appointment confirmation emails",
@@ -41,43 +83,61 @@ const PERMISSIONS = [
41
83
  // ---------------------------------------------------------------------------
42
84
 
43
85
  export function AppointmentGmailConnect({
86
+ provider = "gmail",
44
87
  connection,
45
88
  calendarLink,
89
+ highlighted = false,
46
90
  onCopyLink,
47
91
  onGoToIntegrations,
48
92
  }: AppointmentGmailConnectProps) {
93
+ const { label, iconBg, Icon } = PROVIDER_META[provider];
94
+
49
95
  if (connection.connected) {
50
96
  return (
51
- <div className="flex flex-col border border-border bg-card">
97
+ <div
98
+ className={cn(
99
+ "flex flex-col border bg-card",
100
+ highlighted ? "border-primary" : "border-border"
101
+ )}
102
+ >
52
103
  {/* Header */}
53
- <div className="flex items-center gap-3 px-5 py-4">
54
- <div className="flex h-10 w-10 shrink-0 items-center justify-center bg-primary/10">
55
- <Mail className="h-5 w-5 text-primary" />
104
+ <div className="flex items-start gap-3 px-5 py-4">
105
+ <div
106
+ className={cn(
107
+ "flex h-10 w-10 shrink-0 items-center justify-center",
108
+ iconBg
109
+ )}
110
+ >
111
+ <Icon className="h-5 w-5" />
56
112
  </div>
57
- <div className="flex-1 min-w-0">
58
- <p className="text-base font-semibold">Gmail Connected</p>
113
+ <div className="min-w-0 flex-1">
114
+ <p className="text-base font-semibold">{label} Connected</p>
59
115
  <p className="truncate text-sm text-muted-foreground">
60
116
  {connection.email}
61
117
  </p>
62
118
  </div>
63
- <Badge variant="success">
64
- <Check size={10} />
65
- Active
66
- </Badge>
67
- {connection.connectedAt && (
68
- <p className="shrink-0 text-xs text-muted-foreground">
69
- Connected since {connection.connectedAt}
70
- </p>
71
- )}
72
- <Button
73
- size="sm"
74
- variant="ghost"
75
- className="shrink-0 gap-1.5 text-muted-foreground"
76
- onClick={onGoToIntegrations}
77
- >
78
- <Settings className="h-3.5 w-3.5" />
79
- Manage
80
- </Button>
119
+ <div className="flex shrink-0 flex-col items-end gap-1.5">
120
+ <Button
121
+ size="sm"
122
+ variant="ghost"
123
+ className="gap-1.5 text-muted-foreground"
124
+ onClick={onGoToIntegrations}
125
+ >
126
+ <Settings className="h-3.5 w-3.5" />
127
+ Manage
128
+ </Button>
129
+ <div className="flex items-center gap-2">
130
+ <Badge variant="success">
131
+ <Check size={10} />
132
+ Active
133
+ </Badge>
134
+ {connection.connectedAt && (
135
+ <p className="text-xs text-muted-foreground">
136
+ Connected since {connection.connectedAt}
137
+ </p>
138
+ )}
139
+ </div>
140
+ </div>
81
141
  </div>
82
142
 
83
143
  <Separator />
@@ -141,10 +201,10 @@ export function AppointmentGmailConnect({
141
201
  </div>
142
202
 
143
203
  <div className="flex flex-col gap-1.5">
144
- <p className="text-base font-semibold">Gmail not connected</p>
204
+ <p className="text-base font-semibold">{label} not connected</p>
145
205
  <p className="max-w-xs text-sm text-muted-foreground">
146
- Connect your Gmail account in Integrations to enable calendar booking
147
- and appointment confirmations.
206
+ Connect your {label} account in Integrations to enable calendar
207
+ booking and appointment confirmations.
148
208
  </p>
149
209
  </div>
150
210
 
@@ -107,7 +107,7 @@ function ColorSwatch({
107
107
  size === "md" ? "size-7" : "size-5",
108
108
  selected &&
109
109
  "ring-2 ring-foreground ring-offset-1 ring-offset-background",
110
- className,
110
+ className
111
111
  )}
112
112
  style={{ backgroundColor: color }}
113
113
  />
@@ -130,16 +130,24 @@ function ColorPickerContent({
130
130
  presets = COLOR_PICKER_PRESETS,
131
131
  }: ColorPickerContentProps) {
132
132
  const [hexInput, setHexInput] = React.useState(value);
133
+ const hexInputRef = React.useRef(hexInput);
134
+ hexInputRef.current = hexInput;
133
135
 
134
136
  React.useEffect(() => {
135
- setHexInput(value);
137
+ // Only sync from external value (e.g. swatch click) when the user's current
138
+ // input doesn't already produce the same color — prevents cursor jump mid-type.
139
+ if (value !== hexInputRef.current) {
140
+ setHexInput(value);
141
+ }
136
142
  }, [value]);
137
143
 
138
144
  function handleHexInputChange(e: React.ChangeEvent<HTMLInputElement>) {
139
145
  const raw = e.target.value;
140
146
  setHexInput(raw);
141
147
  const normalized = normalizeHex(raw);
142
- if (isValidHex(normalized)) {
148
+ // Only commit on a complete 6-digit hex so intermediate 3-digit matches
149
+ // (#abc shorthand) don't fire onChange mid-type and shift the cursor.
150
+ if (/^#[0-9A-Fa-f]{6}$/.test(normalized)) {
143
151
  onChange(normalized);
144
152
  }
145
153
  }
@@ -271,7 +279,7 @@ function ColorPicker({
271
279
  "flex h-9 min-w-[140px] cursor-pointer items-center gap-2.5 border border-input bg-background px-3 text-sm outline-none transition-colors",
272
280
  "hover:border-ring",
273
281
  "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
274
- "disabled:cursor-not-allowed disabled:opacity-50",
282
+ "disabled:cursor-not-allowed disabled:opacity-50"
275
283
  )}
276
284
  >
277
285
  <span