keystone-design-bootstrap 1.0.79 → 1.0.80

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.79",
3
+ "version": "1.0.80",
4
4
  "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -41,6 +41,15 @@ export interface PortalPageProps {
41
41
  portalHref?: string;
42
42
  bookingHref?: string | null;
43
43
  bookingLabel?: string;
44
+ /**
45
+ * Controls which tabs are rendered in the portal header.
46
+ * Default: ['book', 'services', 'packages', 'specials', 'messages']
47
+ */
48
+ enabledTabs?: Tab[];
49
+ /**
50
+ * Optional per-tab label overrides, e.g. { messages: 'Chat with Us' }.
51
+ */
52
+ tabLabels?: Partial<Record<Tab, string>>;
44
53
  /**
45
54
  * When true, always opens booking in a new tab even if the URL supports iframes.
46
55
  * Useful when the booking page's iframe experience is broken or undesirable.
@@ -187,10 +196,12 @@ function EmptyState({ message }: { message: string }) {
187
196
  function ServiceItemRow({
188
197
  item,
189
198
  isLoggedIn,
199
+ specialsEnabled,
190
200
  specialsHref,
191
201
  }: {
192
202
  item: ServiceItem;
193
203
  isLoggedIn: boolean;
204
+ specialsEnabled: boolean;
194
205
  specialsHref: string;
195
206
  }) {
196
207
  const activeOffers = (item.offers ?? []).filter((o) => o.active !== false && !o.expired);
@@ -208,7 +219,7 @@ function ServiceItemRow({
208
219
  {item.duration_minutes != null && item.duration_minutes > 0 && (
209
220
  <p className="mt-0.5 text-xs text-quaternary">{item.duration_minutes} min</p>
210
221
  )}
211
- {activeOffers.length > 0 && (
222
+ {specialsEnabled && activeOffers.length > 0 && (
212
223
  <div className="mt-1.5 flex flex-wrap gap-1">
213
224
  {activeOffers.map((offer) =>
214
225
  isLoggedIn ? (
@@ -252,10 +263,12 @@ function ServiceItemRow({
252
263
  function ServicesPanel({
253
264
  services,
254
265
  isLoggedIn,
266
+ specialsEnabled,
255
267
  portalHref,
256
268
  }: {
257
269
  services: Service[];
258
270
  isLoggedIn: boolean;
271
+ specialsEnabled: boolean;
259
272
  portalHref: string;
260
273
  }) {
261
274
  const activeServices = services.filter((s) => (s.service_items?.length ?? 0) > 0);
@@ -292,6 +305,7 @@ function ServicesPanel({
292
305
  key={item.id}
293
306
  item={item}
294
307
  isLoggedIn={isLoggedIn}
308
+ specialsEnabled={specialsEnabled}
295
309
  specialsHref={specialsHref}
296
310
  />
297
311
  ))}
@@ -309,10 +323,12 @@ function ServicesPanel({
309
323
  function PackagesPanel({
310
324
  packages,
311
325
  isLoggedIn,
326
+ specialsEnabled,
312
327
  portalHref,
313
328
  }: {
314
329
  packages: Package[];
315
330
  isLoggedIn: boolean;
331
+ specialsEnabled: boolean;
316
332
  portalHref: string;
317
333
  }) {
318
334
  const specialsHref = `${portalHref}?tab=specials`;
@@ -365,7 +381,7 @@ function PackagesPanel({
365
381
  </ul>
366
382
  )}
367
383
 
368
- {activeOffers.length > 0 && (
384
+ {specialsEnabled && activeOffers.length > 0 && (
369
385
  <div className="flex flex-wrap gap-1.5">
370
386
  {activeOffers.map((offer) =>
371
387
  isLoggedIn ? (
@@ -559,19 +575,20 @@ function BookPanel({
559
575
 
560
576
  // ─── Main Portal Page (Self-Contained Async Server Component) ─────────────────
561
577
 
562
- const VALID_TABS = ['services', 'packages', 'specials', 'messages', 'book'] as const;
563
- type Tab = (typeof VALID_TABS)[number];
578
+ const ALL_TABS = ['services', 'packages', 'specials', 'messages', 'book'] as const;
579
+ type Tab = (typeof ALL_TABS)[number];
564
580
 
565
581
  export async function PortalPage({
566
582
  searchParams,
567
583
  portalHref = '/portal',
568
584
  bookingHref,
569
585
  bookingLabel = 'Book Now',
586
+ enabledTabs,
587
+ tabLabels,
570
588
  forceExternalBooking = false,
571
589
  contactHref = '/contact',
572
590
  }: PortalPageProps) {
573
591
  const params = await searchParams ?? {};
574
- const tabParam = VALID_TABS.includes(params.tab as Tab) ? (params.tab as Tab) : null;
575
592
  const contactIdParsed = params.contact ? parseInt(params.contact, 10) : NaN;
576
593
  const contactId = Number.isFinite(contactIdParsed) ? contactIdParsed : null;
577
594
 
@@ -591,15 +608,32 @@ export async function PortalPage({
591
608
  const resolvedBookingHref =
592
609
  bookingHref ?? companyInformation?.external_management_url ?? null;
593
610
 
594
- // Build the tabs array first so the default can simply be whatever is first.
595
- const tabs: Array<{ id: Tab; label: string }> = [
596
- ...(resolvedBookingHref ? [{ id: 'book' as Tab, label: bookingLabel }] : []),
597
- { id: 'services', label: 'Services' },
598
- { id: 'packages', label: 'Packages' },
599
- { id: 'specials', label: 'Specials' },
600
- { id: 'messages', label: 'Messages' },
601
- ...(!resolvedBookingHref ? [{ id: 'book' as Tab, label: bookingLabel }] : []),
602
- ];
611
+ const hasCustomTabOrder = Array.isArray(enabledTabs) && enabledTabs.length > 0;
612
+ const requestedTabs = (enabledTabs ?? ['book', 'services', 'packages', 'specials', 'messages']).filter((tab, index, arr) => (
613
+ ALL_TABS.includes(tab) && arr.indexOf(tab) === index
614
+ ));
615
+ const normalizedTabs = requestedTabs.length > 0 ? requestedTabs : ['book', 'services', 'packages', 'specials', 'messages'];
616
+ const nonBookTabs = normalizedTabs.filter((tab) => tab !== 'book');
617
+ const includesBookTab = normalizedTabs.includes('book');
618
+
619
+ const labelForTab = (tab: Tab): string => {
620
+ if (tab === 'book') return tabLabels?.book ?? bookingLabel;
621
+ if (tab === 'services') return tabLabels?.services ?? 'Services';
622
+ if (tab === 'packages') return tabLabels?.packages ?? 'Packages';
623
+ if (tab === 'specials') return tabLabels?.specials ?? 'Specials';
624
+ return tabLabels?.messages ?? 'Messages';
625
+ };
626
+
627
+ // Keep existing behavior: when booking is configured, "Book Now" appears first; otherwise last.
628
+ const tabs: Array<{ id: Tab; label: string }> = hasCustomTabOrder
629
+ ? normalizedTabs.map((tab) => ({ id: tab, label: labelForTab(tab) }))
630
+ : [
631
+ ...(includesBookTab && resolvedBookingHref ? [{ id: 'book' as Tab, label: labelForTab('book') }] : []),
632
+ ...nonBookTabs.map((tab) => ({ id: tab, label: labelForTab(tab) })),
633
+ ...(includesBookTab && !resolvedBookingHref ? [{ id: 'book' as Tab, label: labelForTab('book') }] : []),
634
+ ];
635
+ const selectableTabIds = tabs.map((tab) => tab.id);
636
+ const tabParam = selectableTabIds.includes(params.tab as Tab) ? (params.tab as Tab) : null;
603
637
 
604
638
  // Default to the first tab so tab order and default selection are always in sync.
605
639
  const tab: Tab = tabParam ?? tabs[0].id;
@@ -641,6 +675,7 @@ export async function PortalPage({
641
675
  const serviceList = services ?? [];
642
676
  const packageList = packages ?? [];
643
677
  const specials = aggregateSpecials(serviceList, packageList);
678
+ const specialsEnabled = selectableTabIds.includes('specials');
644
679
 
645
680
  return (
646
681
  <div className="min-h-screen bg-primary">
@@ -724,10 +759,20 @@ export async function PortalPage({
724
759
  {/* Tab Content */}
725
760
  <div className="mx-auto max-w-4xl px-4 py-8">
726
761
  {tab === 'services' && (
727
- <ServicesPanel services={serviceList} isLoggedIn={isLoggedIn} portalHref={portalHref} />
762
+ <ServicesPanel
763
+ services={serviceList}
764
+ isLoggedIn={isLoggedIn}
765
+ specialsEnabled={specialsEnabled}
766
+ portalHref={portalHref}
767
+ />
728
768
  )}
729
769
  {tab === 'packages' && (
730
- <PackagesPanel packages={packageList} isLoggedIn={isLoggedIn} portalHref={portalHref} />
770
+ <PackagesPanel
771
+ packages={packageList}
772
+ isLoggedIn={isLoggedIn}
773
+ specialsEnabled={specialsEnabled}
774
+ portalHref={portalHref}
775
+ />
731
776
  )}
732
777
  {tab === 'specials' && (
733
778
  isLoggedIn ? (