keystone-design-bootstrap 1.0.7 → 1.0.8

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.7",
3
+ "version": "1.0.8",
4
4
  "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -1,59 +1,65 @@
1
1
  "use client";
2
2
 
3
- import React, { ReactNode } from 'react';
4
- import { Carousel } from '../carousel/carousel';
3
+ import React from 'react';
4
+ import { Carousel } from './carousel';
5
5
  import { ChevronLeft, ChevronRight } from '@untitledui/icons';
6
6
 
7
7
  interface CarouselSectionWrapperProps {
8
- title?: string;
9
- children: ReactNode;
8
+ title: string;
9
+ subtitle?: string;
10
10
  hasItems: boolean;
11
+ children: React.ReactNode;
11
12
  emptyMessage?: string;
12
13
  }
13
14
 
14
15
  export const CarouselSectionWrapper = ({
15
- title = "",
16
- children,
16
+ title,
17
+ subtitle,
17
18
  hasItems,
18
- emptyMessage = "No items available"
19
+ children,
20
+ emptyMessage = "No items available",
19
21
  }: CarouselSectionWrapperProps) => {
20
- if (!hasItems) {
21
- return (
22
- <>
23
- {title && (
24
- <div className="mb-12">
25
- <h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
26
- {title}
27
- </h2>
28
- </div>
29
- )}
30
- <div className="text-center py-12">
31
- <p className="font-body text-base text-tertiary">{emptyMessage}</p>
32
- </div>
33
- </>
34
- );
35
- }
36
-
37
22
  return (
38
- <Carousel.Root opts={{ align: "start", loop: true }}>
39
- <div className="flex items-center justify-between mb-12">
40
- <h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
41
- {title}
42
- </h2>
43
- <div className="flex gap-2">
44
- <Carousel.PrevTrigger className="rounded-full p-2 border border-secondary hover:bg-primary_hover transition-colors disabled:opacity-30 disabled:cursor-not-allowed">
45
- <ChevronLeft className="w-6 h-6 text-fg-primary" />
46
- </Carousel.PrevTrigger>
47
- <Carousel.NextTrigger className="rounded-full p-2 border border-secondary hover:bg-primary_hover transition-colors disabled:opacity-30 disabled:cursor-not-allowed">
48
- <ChevronRight className="w-6 h-6 text-fg-primary" />
49
- </Carousel.NextTrigger>
50
- </div>
23
+ <section>
24
+ <div className="mx-auto max-w-container px-4 md:px-8">
25
+ {hasItems ? (
26
+ <Carousel.Root opts={{ align: "start", loop: true }}>
27
+ <div className="flex items-center justify-between mb-12">
28
+ <h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
29
+ {title}
30
+ </h2>
31
+ <div className="flex gap-2">
32
+ <Carousel.PrevTrigger className="rounded-full p-2 border border-secondary hover:bg-primary_hover transition-colors disabled:opacity-30 disabled:cursor-not-allowed">
33
+ <ChevronLeft className="w-6 h-6 text-fg-primary" />
34
+ </Carousel.PrevTrigger>
35
+ <Carousel.NextTrigger className="rounded-full p-2 border border-secondary hover:bg-primary_hover transition-colors disabled:opacity-30 disabled:cursor-not-allowed">
36
+ <ChevronRight className="w-6 h-6 text-fg-primary" />
37
+ </Carousel.NextTrigger>
38
+ </div>
39
+ </div>
40
+
41
+ <Carousel.Content className="-ml-4">
42
+ {children}
43
+ </Carousel.Content>
44
+ </Carousel.Root>
45
+ ) : (
46
+ <>
47
+ <div className="mb-12">
48
+ <h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
49
+ {title}
50
+ </h2>
51
+ {subtitle && (
52
+ <p className="mt-4 text-lg text-tertiary md:mt-6 md:text-xl">
53
+ {subtitle}
54
+ </p>
55
+ )}
56
+ </div>
57
+ <div className="text-center py-12">
58
+ <p className="font-body text-base text-tertiary">{emptyMessage}</p>
59
+ </div>
60
+ </>
61
+ )}
51
62
  </div>
52
-
53
- <Carousel.Content className="-ml-4">
54
- {children}
55
- </Carousel.Content>
56
- </Carousel.Root>
63
+ </section>
57
64
  );
58
65
  };
59
-
@@ -157,3 +157,7 @@ export { CarouselSectionWrapper } from './carousel/carousel-section-wrapper';
157
157
 
158
158
  // Re-export combobox (still exists in select directory but not wrapped with theming)
159
159
  export { ComboBox } from './select/combobox';
160
+
161
+ // Video components (no theming needed)
162
+ export { VideoModal } from './video-modal';
163
+ export { VideoPlayButton } from './video-play-button';
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+
5
+ interface VideoModalProps {
6
+ isOpen: boolean;
7
+ onClose: () => void;
8
+ videoUrl: string;
9
+ }
10
+
11
+ export function VideoModal({ isOpen, onClose, videoUrl }: VideoModalProps) {
12
+ // Prevent body scroll when modal is open
13
+ useEffect(() => {
14
+ if (isOpen) {
15
+ document.body.style.overflow = 'hidden';
16
+ } else {
17
+ document.body.style.overflow = '';
18
+ }
19
+ return () => {
20
+ document.body.style.overflow = '';
21
+ };
22
+ }, [isOpen]);
23
+
24
+ if (!isOpen) return null;
25
+
26
+ return (
27
+ <div
28
+ className="fixed inset-0 z-[60] flex items-center justify-center p-4 backdrop-blur-md bg-black/50"
29
+ onClick={onClose}
30
+ >
31
+ {/* Close button */}
32
+ <button
33
+ onClick={onClose}
34
+ className="absolute top-4 right-4 md:top-8 md:right-8 text-white text-4xl md:text-5xl hover:text-gray-300 z-10 transition-colors"
35
+ aria-label="Close video"
36
+ >
37
+ ×
38
+ </button>
39
+
40
+ {/* Video container */}
41
+ <div
42
+ className="relative w-full max-w-5xl aspect-video"
43
+ onClick={(e) => e.stopPropagation()}
44
+ >
45
+ <iframe
46
+ src={videoUrl}
47
+ title="Video player"
48
+ className="w-full h-full rounded-lg shadow-2xl"
49
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
50
+ allowFullScreen
51
+ />
52
+ </div>
53
+ </div>
54
+ );
55
+ }
56
+
@@ -0,0 +1,25 @@
1
+ 'use client';
2
+
3
+ interface VideoPlayButtonProps {
4
+ onClick: () => void;
5
+ className?: string;
6
+ }
7
+
8
+ export function VideoPlayButton({ onClick, className = '' }: VideoPlayButtonProps) {
9
+ return (
10
+ <button
11
+ onClick={onClick}
12
+ className={`group flex items-center justify-center w-24 h-24 md:w-32 md:h-32 rounded-full bg-black/40 backdrop-blur-sm hover:bg-black/50 transition-all hover:scale-105 ${className}`}
13
+ aria-label="Play video"
14
+ >
15
+ <svg
16
+ className="w-10 h-10 md:w-14 md:h-14 text-white group-hover:scale-110 transition-transform ml-1"
17
+ fill="currentColor"
18
+ viewBox="0 0 24 24"
19
+ >
20
+ <path d="M8 5v14l11-7z" />
21
+ </svg>
22
+ </button>
23
+ );
24
+ }
25
+
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
 
3
- import { Fragment } from "react";
3
+ import { Fragment, useState } from "react";
4
4
  import React from "react";
5
- import { PhotoWithFallback } from '../elements';
5
+ import { PhotoWithFallback, VideoModal, VideoPlayButton } from '../elements';
6
6
  import type { WebsitePhotos } from '../../types/api/website-photos';
7
7
 
8
8
  interface HeroHomeProps {
@@ -13,6 +13,7 @@ interface HeroHomeProps {
13
13
  ctaHref?: string;
14
14
  reviews?: { rating: number; count: number };
15
15
  onEmailSubmit?: (email: string) => void;
16
+ videoUrl?: string;
16
17
  }
17
18
 
18
19
  export const HeroHome = ({
@@ -21,7 +22,9 @@ export const HeroHome = ({
21
22
  subhead = "",
22
23
  ctaText = "",
23
24
  ctaHref = "/contact",
25
+ videoUrl,
24
26
  }: HeroHomeProps) => {
27
+ const [showVideo, setShowVideo] = useState(false);
25
28
 
26
29
  const heroImage = {
27
30
  url: websitePhotos?.hero?.url || (websitePhotos as any)?.brand?.url || '',
@@ -54,16 +57,32 @@ export const HeroHome = ({
54
57
 
55
58
  <section>
56
59
  <div className="mx-auto max-w-container px-4 md:px-8">
57
- <div className="w-full h-[400px] md:h-[500px] lg:h-[600px]">
60
+ <div className="relative w-full h-[400px] md:h-[500px] lg:h-[600px]">
58
61
  <PhotoWithFallback
59
62
  photoUrl={heroImage.url}
60
63
  photoAlt={heroImage.alt}
61
64
  fallbackId="hero-home-brand"
62
65
  className="w-full h-full object-cover"
63
66
  />
67
+
68
+ {/* Video play button overlay */}
69
+ {videoUrl && (
70
+ <div className="absolute inset-0 flex items-center justify-center">
71
+ <VideoPlayButton onClick={() => setShowVideo(true)} />
72
+ </div>
73
+ )}
64
74
  </div>
65
75
  </div>
66
76
  </section>
77
+
78
+ {/* Video modal */}
79
+ {videoUrl && (
80
+ <VideoModal
81
+ isOpen={showVideo}
82
+ onClose={() => setShowVideo(false)}
83
+ videoUrl={videoUrl}
84
+ />
85
+ )}
67
86
  </Fragment>
68
87
  );
69
88
  };
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
 
3
- import { Fragment, useMemo } from "react";
3
+ import { Fragment, useMemo, useState } from "react";
4
4
  import React from "react";
5
- import { Button, Form, Input, PhotoWithFallback } from '../elements';
5
+ import { Button, Form, Input, PhotoWithFallback, VideoModal, VideoPlayButton } from '../elements';
6
6
  import { cx } from '../../utils/cx';
7
7
  import { mapIcon } from '../utils/icon-mapping';
8
8
  import type { FC } from "react";
@@ -33,6 +33,7 @@ interface HeroHomeProps {
33
33
  statistics?: Statistic[];
34
34
  email_signup?: EmailSignup;
35
35
  onEmailSubmit?: (email: string) => void;
36
+ videoUrl?: string;
36
37
  }
37
38
 
38
39
  const AvatarsWithReview = ({
@@ -140,7 +141,9 @@ export const HeroHome = ({
140
141
  statistics = [],
141
142
  email_signup,
142
143
  onEmailSubmit,
144
+ videoUrl,
143
145
  }: HeroHomeProps) => {
146
+ const [showVideo, setShowVideo] = useState(false);
144
147
 
145
148
  // Get hero image from props
146
149
  const heroImage = {
@@ -241,9 +244,25 @@ export const HeroHome = ({
241
244
  fallbackId="hero-home-image"
242
245
  className="inset-0 h-70 w-full object-cover md:h-110 lg:absolute lg:h-full"
243
246
  />
247
+
248
+ {/* Video play button overlay */}
249
+ {videoUrl && (
250
+ <div className="absolute inset-0 flex items-center justify-center">
251
+ <VideoPlayButton onClick={() => setShowVideo(true)} />
252
+ </div>
253
+ )}
244
254
  </div>
245
255
  </div>
246
256
  </section>
257
+
258
+ {/* Video modal */}
259
+ {videoUrl && (
260
+ <VideoModal
261
+ isOpen={showVideo}
262
+ onClose={() => setShowVideo(false)}
263
+ videoUrl={videoUrl}
264
+ />
265
+ )}
247
266
  </Fragment>
248
267
  );
249
268
  };