keystone-design-bootstrap 1.0.44 → 1.0.46

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.44",
3
+ "version": "1.0.46",
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,14 +1,20 @@
1
1
  import { PhotoWithFallback } from '../elements';
2
+ import { isVideoUrl } from '../../utils/photo-helpers';
2
3
  import type { SocialPost } from '../../types/api/social-post';
3
4
 
4
- /** Get display image URLs from post: image_urls (API) or photo_attachments (ordered) */
5
+ /** Get display image URLs from post (excludes video URLs so img/PhotoWithFallback don't break) */
5
6
  function getPostImageUrls(post: SocialPost): string[] {
6
- if (post.image_urls?.length) return post.image_urls;
7
+ const videoSet = post.video_urls?.length ? new Set(post.video_urls) : null;
8
+ const isVideo = (url: string) => isVideoUrl(url) || (videoSet !== null && videoSet.has(url));
9
+ if (post.image_urls?.length) {
10
+ return post.image_urls.filter((url) => !isVideo(url));
11
+ }
7
12
  const attachments = post.photo_attachments || [];
8
13
  const sorted = [...attachments].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
9
14
  return sorted
10
15
  .map((pa) => pa.photo?.large_url || pa.photo?.original_url || pa.photo?.medium_url || pa.photo?.thumbnail_url)
11
- .filter((url): url is string => Boolean(url));
16
+ .filter((url): url is string => Boolean(url))
17
+ .filter((url) => !isVideo(url));
12
18
  }
13
19
 
14
20
  interface SocialMediaGridProps {
@@ -1,14 +1,20 @@
1
1
  import { PhotoWithFallback } from '../elements';
2
+ import { isVideoUrl } from '../../utils/photo-helpers';
2
3
  import type { SocialPost } from '../../types/api/social-post';
3
4
 
4
- /** Get display image URLs from post: image_urls (API) or photo_attachments (ordered) */
5
+ /** Get display image URLs from post (excludes video URLs so img/PhotoWithFallback don't break) */
5
6
  function getPostImageUrls(post: SocialPost): string[] {
6
- if (post.image_urls?.length) return post.image_urls;
7
+ const videoSet = post.video_urls?.length ? new Set(post.video_urls) : null;
8
+ const isVideo = (url: string) => isVideoUrl(url) || (videoSet !== null && videoSet.has(url));
9
+ if (post.image_urls?.length) {
10
+ return post.image_urls.filter((url) => !isVideo(url));
11
+ }
7
12
  const attachments = post.photo_attachments || [];
8
13
  const sorted = [...attachments].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
9
14
  return sorted
10
15
  .map((pa) => pa.photo?.large_url || pa.photo?.original_url || pa.photo?.medium_url || pa.photo?.thumbnail_url)
11
- .filter((url): url is string => Boolean(url));
16
+ .filter((url): url is string => Boolean(url))
17
+ .filter((url) => !isVideo(url));
12
18
  }
13
19
 
14
20
  interface SocialMediaGridProps {
@@ -6,16 +6,22 @@ import { ArrowLeft, ArrowRight } from '@untitledui/icons';
6
6
  import { Carousel } from '../elements/carousel/carousel-base';
7
7
  import { Button, PaginationPageMinimalCenter, PhotoWithFallback, RoundButton } from '../elements';
8
8
  import { cx } from '../../utils/cx';
9
+ import { isVideoUrl } from '../../utils/photo-helpers';
9
10
  import type { SocialPost } from '../../types/api/social-post';
10
11
 
11
- /** Get display image URLs from post: image_urls (API) or photo_attachments (ordered) */
12
+ /** Get display image URLs from post (excludes video URLs so img/PhotoWithFallback don't break) */
12
13
  function getPostImageUrls(post: SocialPost): string[] {
13
- if (post.image_urls?.length) return post.image_urls;
14
+ const videoSet = post.video_urls?.length ? new Set(post.video_urls) : null;
15
+ const isVideo = (url: string) => isVideoUrl(url) || (videoSet !== null && videoSet.has(url));
16
+ if (post.image_urls?.length) {
17
+ return post.image_urls.filter((url) => !isVideo(url));
18
+ }
14
19
  const attachments = post.photo_attachments || [];
15
20
  const sorted = [...attachments].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
16
21
  return sorted
17
22
  .map((pa) => pa.photo?.large_url || pa.photo?.original_url || pa.photo?.medium_url || pa.photo?.thumbnail_url)
18
- .filter((url): url is string => Boolean(url));
23
+ .filter((url): url is string => Boolean(url))
24
+ .filter((url) => !isVideo(url));
19
25
  }
20
26
 
21
27
  interface SocialMediaGridProps {
@@ -22,6 +22,8 @@ import type { CompanyInformation, Location, NavItem, Service, SiteConfig } from
22
22
 
23
23
  export type KeystoneRootLayoutHeaderOverrides = {
24
24
  logoHref?: string;
25
+ /** Overrides the title/company name shown in the center of the nav bar (otherwise from business_info) */
26
+ logoText?: string;
25
27
  ctaLabel?: string;
26
28
  ctaHref?: string;
27
29
  secondaryLabel?: string;
@@ -138,7 +140,10 @@ export async function KeystoneRootLayout(props: {
138
140
 
139
141
  const headerOverrides = options?.headerOverrides;
140
142
  const headerProps = {
141
- logo: { href: headerOverrides?.logoHref || '/' },
143
+ logo: {
144
+ href: headerOverrides?.logoHref || '/',
145
+ ...(headerOverrides?.logoText != null && { text: headerOverrides.logoText }),
146
+ },
142
147
  cta_button: {
143
148
  label: headerOverrides?.ctaLabel || 'Contact Us',
144
149
  href: headerOverrides?.ctaHref || '/contact',
@@ -11,6 +11,8 @@ export interface SocialPost {
11
11
  updated_at: string;
12
12
  /** Image URLs from photo_attachments (preferred for display) */
13
13
  image_urls?: string[];
14
+ /** Video URLs from photo_attachments (exclude these when choosing img src) */
15
+ video_urls?: string[];
14
16
  /** Photo attachments (same pattern as blog post, team member, etc.) */
15
17
  photo_attachments?: PhotoAttachment[];
16
18
  /** Legacy; prefer image_urls / photo_attachments */
@@ -4,6 +4,19 @@
4
4
 
5
5
  import type { WebsitePhotos } from '../types/api/website-photos';
6
6
 
7
+ /** Video file extensions treated as video (match backend ExternalPhotoService.video_url?) */
8
+ const VIDEO_EXTENSIONS = ['.mp4', '.mov', '.webm', '.m4v', '.avi', '.mkv', '.flv', '.wmv'];
9
+
10
+ /**
11
+ * True if the URL looks like a video by path extension. Used to avoid using video URLs in img src.
12
+ */
13
+ export function isVideoUrl(url: string | null | undefined): boolean {
14
+ if (!url || typeof url !== 'string') return false;
15
+ const path = url.split('?')[0] ?? '';
16
+ const ext = path.slice(path.lastIndexOf('.')).toLowerCase();
17
+ return VIDEO_EXTENSIONS.includes(ext);
18
+ }
19
+
7
20
  export interface PhotoAttachment {
8
21
  id: number;
9
22
  photo: {