keystone-design-bootstrap 1.0.50 → 1.0.53

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 (32) hide show
  1. package/dist/{blog-post-D7HFCDp1.d.ts → blog-post-DGjaJ3wf.d.ts} +2 -2
  2. package/dist/design_system/sections/index.d.ts +7 -25
  3. package/dist/design_system/sections/index.js +257 -405
  4. package/dist/design_system/sections/index.js.map +1 -1
  5. package/dist/{form-BLZuTGkr.d.ts → form-CpsCONG5.d.ts} +16 -2
  6. package/dist/index.d.ts +4 -5
  7. package/dist/index.js +275 -423
  8. package/dist/index.js.map +1 -1
  9. package/dist/lib/server-api.d.ts +3 -46
  10. package/dist/lib/server-api.js +0 -9
  11. package/dist/lib/server-api.js.map +1 -1
  12. package/dist/types/index.d.ts +4 -4
  13. package/dist/utils/photo-helpers.d.ts +1 -1
  14. package/package.json +1 -1
  15. package/src/design_system/sections/email-signup-section.tsx +115 -0
  16. package/src/design_system/sections/header-navigation.aman.tsx +8 -3
  17. package/src/design_system/sections/header-navigation.balance.tsx +2 -0
  18. package/src/design_system/sections/header-navigation.barelux.tsx +4 -1
  19. package/src/design_system/sections/index.tsx +5 -16
  20. package/src/design_system/sections/service-menu-section.tsx +58 -146
  21. package/src/lib/server-api.ts +0 -54
  22. package/src/next/contexts/form-definitions.tsx +5 -2
  23. package/src/next/layouts/root-layout.tsx +3 -0
  24. package/src/types/api/form.ts +1 -1
  25. package/src/types/api/offer.ts +13 -0
  26. package/src/types/api/service.ts +2 -0
  27. package/src/types/index.ts +1 -0
  28. package/src/design_system/sections/offer-detail.tsx +0 -46
  29. package/src/design_system/sections/offers-gallery.tsx +0 -40
  30. package/src/design_system/sections/offers-grid.tsx +0 -108
  31. package/src/design_system/sections/offers-section.tsx +0 -90
  32. package/dist/{photos-8jMeetqV.d.ts → website-photos-Bm-CBK9g.d.ts} +20 -20
@@ -1,5 +1,5 @@
1
- import { C as CompanyInformation, S as Service, F as FormDefinition } from '../form-BLZuTGkr.js';
2
- import { P as PhotoAttachment, W as WebsitePhotos } from '../photos-8jMeetqV.js';
1
+ import { C as CompanyInformation, F as FormDefinition, S as Service } from '../form-CpsCONG5.js';
2
+ import { W as WebsitePhotos } from '../website-photos-Bm-CBK9g.js';
3
3
 
4
4
  interface FetchOptions {
5
5
  cache?: RequestCache;
@@ -39,48 +39,5 @@ declare function getPackage(slug: string): Promise<unknown>;
39
39
  declare function getTestimonials(): Promise<unknown>;
40
40
  /** Form definition for dynamic form rendering (fields may include optional placeholder). */
41
41
  declare function getForm(formType: string): Promise<FormDefinition | null>;
42
- /** Minimal service for offer detail (from GET /public/offers/:id). */
43
- interface OfferServiceSummary {
44
- id: number;
45
- name: string;
46
- slug: string;
47
- summary?: string | null;
48
- }
49
- /** Package summary (nested on each offer in list and single-offer responses). */
50
- interface OfferPackageSummary {
51
- id: number;
52
- name: string;
53
- slug: string;
54
- summary?: string | null;
55
- description_markdown?: string | null;
56
- }
57
- /** Offer from public API. */
58
- interface OfferPublic {
59
- id: number;
60
- name: string;
61
- description: string | null;
62
- value_terms: string | null;
63
- /** Optional; when absent or null, offer has no expiration. */
64
- expires_at?: string | null;
65
- expired?: boolean;
66
- /** Service IDs (used for category_names fallback when not provided by API). */
67
- service_ids?: number[];
68
- /** Package IDs (included in list response; nested services/packages are the source of truth). */
69
- package_ids?: number[];
70
- /** Category names from API (services + service_items + packages). */
71
- category_names?: string[];
72
- /** Photo attachments from related services (same shape as Service.photo_attachments). */
73
- photo_attachments?: PhotoAttachment[];
74
- /** Present when fetching single offer (GET /public/offers/:id). */
75
- services?: OfferServiceSummary[];
76
- /** Specific menu items this offer applies to (single offer only). */
77
- service_items?: OfferServiceSummary[];
78
- /** Packages this offer applies to (single offer only). */
79
- packages?: OfferPackageSummary[];
80
- }
81
- /** List offers for the current account (API key scoped). */
82
- declare function getOffers(): Promise<OfferPublic[] | null>;
83
- /** Single offer with services for detail page. */
84
- declare function getOffer(id: number | string): Promise<OfferPublic | null>;
85
42
 
86
- export { type OfferPackageSummary, type OfferPublic, type OfferServiceSummary, getAdsConfig, getBlogPost, getBlogPosts, getCompanyInformation, getFAQs, getForm, getJobPosting, getJobPostings, getLocation, getLocations, getMetaPixelId, getOffer, getOffers, getPackage, getPackages, getReviews, getService, getServices, getSocialPosts, getTeamMembers, getTestimonials, getWebsitePhotos, serverApi };
43
+ export { getAdsConfig, getBlogPost, getBlogPosts, getCompanyInformation, getFAQs, getForm, getJobPosting, getJobPostings, getLocation, getLocations, getMetaPixelId, getPackage, getPackages, getReviews, getService, getServices, getSocialPosts, getTeamMembers, getTestimonials, getWebsitePhotos, serverApi };
@@ -96,13 +96,6 @@ async function getTestimonials() {
96
96
  async function getForm(formType) {
97
97
  return serverFetch(`/public/forms/${encodeURIComponent(formType)}`, defaultOptions);
98
98
  }
99
- async function getOffers() {
100
- const data = await serverFetch("/public/offers", defaultOptions);
101
- return Array.isArray(data) ? data : null;
102
- }
103
- async function getOffer(id) {
104
- return serverFetch(`/public/offers/${id}`, defaultOptions);
105
- }
106
99
  export {
107
100
  getAdsConfig,
108
101
  getBlogPost,
@@ -115,8 +108,6 @@ export {
115
108
  getLocation,
116
109
  getLocations,
117
110
  getMetaPixelId,
118
- getOffer,
119
- getOffers,
120
111
  getPackage,
121
112
  getPackages,
122
113
  getReviews,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/server-api.ts"],"sourcesContent":["/**\n * Server-side API client for SSR\n * API key is stored securely on the server - never exposed to browser\n */\n\nimport type { CompanyInformation } from '../types/api/company-information';\nimport type { PhotoAttachment } from '../types/api/photos';\nimport type { Service } from '../types/api/service';\nimport type { WebsitePhotos } from '../types/api/website-photos';\n\nconst API_URL = process.env.API_URL || 'http://localhost:3000/api/v1';\nconst API_KEY = process.env.API_KEY || '';\n\ninterface FetchOptions {\n cache?: RequestCache;\n revalidate?: number;\n}\n\nasync function serverFetch<T>(endpoint: string, options: FetchOptions = {}): Promise<T | null> {\n const url = `${API_URL}${endpoint}`;\n \n try {\n const fetchOptions: RequestInit & { next?: { revalidate?: number } } = {\n headers: {\n 'X-API-Key': API_KEY,\n 'Content-Type': 'application/json',\n },\n cache: options.cache,\n };\n \n if (options.revalidate) {\n fetchOptions.next = { revalidate: options.revalidate };\n }\n \n const response = await fetch(url, fetchOptions);\n\n if (!response.ok) {\n console.error(`[Server API] Error ${response.status} for ${endpoint}`);\n return null;\n }\n\n const json = await response.json();\n \n // Rails API returns { data: [...], meta: {...} }\n return json.data ?? json;\n } catch (error) {\n console.error(`[Server API] Failed to fetch ${endpoint}:`, error);\n return null;\n }\n}\n\n/**\n * Generic serverApi object for flexible endpoint access\n */\nexport const serverApi = {\n get: <T = unknown>(endpoint: string, options?: FetchOptions): Promise<T | null> => {\n return serverFetch<T>(endpoint, options || { revalidate: 60 });\n }\n};\n\n// Revalidate data every 60 seconds (ISR)\nconst defaultOptions: FetchOptions = { revalidate: 60 };\n\nexport async function getCompanyInformation(): Promise<CompanyInformation | null> {\n return serverFetch<CompanyInformation>('/public/company_information', defaultOptions);\n}\n\n/** Ads config (e.g. Meta Pixel). Returns { meta_pixel_id?: string } or {}. Only present when account has connected Meta Ads. */\nexport async function getAdsConfig(): Promise<{ meta_pixel_id?: string } | null> {\n const data = await serverFetch<{ meta_pixel_id?: string }>('/public/ads_config', defaultOptions);\n return data ?? null;\n}\n\n/** Extract Meta Pixel ID from ads config for use with <MetaPixel pixelId={...} />. Only returns a value when present and valid (numeric). */\nexport function getMetaPixelId(adsConfig: { meta_pixel_id?: string } | null | undefined): string | null {\n const id = adsConfig && typeof adsConfig === 'object' && 'meta_pixel_id' in adsConfig && adsConfig.meta_pixel_id;\n const str = id != null && id !== '' ? String(id).trim() : '';\n return str !== '' && str !== 'null' && /^\\d+$/.test(str) ? str : null;\n}\n\nexport async function getServices() {\n return serverFetch('/public/services', defaultOptions);\n}\n\nexport async function getService(slug: string): Promise<Service | null> {\n return serverFetch<Service>(`/public/services/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getLocations() {\n return serverFetch('/public/locations', defaultOptions);\n}\n\nexport async function getLocation(slug: string) {\n return serverFetch(`/public/locations/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getReviews() {\n return serverFetch('/public/reviews', defaultOptions);\n}\n\nexport async function getFAQs() {\n return serverFetch('/public/faq_questions', defaultOptions);\n}\n\nexport async function getBlogPosts() {\n return serverFetch('/public/blog_posts', defaultOptions);\n}\n\nexport async function getBlogPost(slug: string) {\n return serverFetch(`/public/blog_posts/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getTeamMembers() {\n return serverFetch('/public/team_members', defaultOptions);\n}\n\nexport async function getWebsitePhotos(): Promise<WebsitePhotos | null> {\n return serverFetch<WebsitePhotos>('/public/website_photos', defaultOptions);\n}\n\nexport async function getJobPostings() {\n return serverFetch('/public/job_postings', defaultOptions);\n}\n\nexport async function getJobPosting(slug: string) {\n return serverFetch(`/public/job_postings/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getSocialPosts() {\n return serverFetch('/public/social_posts', defaultOptions);\n}\n\n/** Packages (bundles of service items). */\nexport async function getPackages() {\n return serverFetch('/public/packages', defaultOptions);\n}\n\nexport async function getPackage(slug: string) {\n return serverFetch(`/public/packages/by_slug/${encodeURIComponent(slug)}`, defaultOptions);\n}\n\n// Alias for testimonials (API uses \"reviews\")\nexport async function getTestimonials() {\n return getReviews();\n}\n\n/** Form definition for dynamic form rendering (fields may include optional placeholder). */\nexport async function getForm(formType: string) {\n type FormDefinition = import('../types/api/form').FormDefinition;\n return serverFetch<FormDefinition>(`/public/forms/${encodeURIComponent(formType)}`, defaultOptions);\n}\n\n/** Minimal service for offer detail (from GET /public/offers/:id). */\nexport interface OfferServiceSummary {\n id: number;\n name: string;\n slug: string;\n summary?: string | null;\n}\n\n/** Package summary (nested on each offer in list and single-offer responses). */\nexport interface OfferPackageSummary {\n id: number;\n name: string;\n slug: string;\n summary?: string | null;\n description_markdown?: string | null;\n}\n\n/** Offer from public API. */\nexport interface OfferPublic {\n id: number;\n name: string;\n description: string | null;\n value_terms: string | null;\n /** Optional; when absent or null, offer has no expiration. */\n expires_at?: string | null;\n expired?: boolean;\n /** Service IDs (used for category_names fallback when not provided by API). */\n service_ids?: number[];\n /** Package IDs (included in list response; nested services/packages are the source of truth). */\n package_ids?: number[];\n /** Category names from API (services + service_items + packages). */\n category_names?: string[];\n /** Photo attachments from related services (same shape as Service.photo_attachments). */\n photo_attachments?: PhotoAttachment[];\n /** Present when fetching single offer (GET /public/offers/:id). */\n services?: OfferServiceSummary[];\n /** Specific menu items this offer applies to (single offer only). */\n service_items?: OfferServiceSummary[];\n /** Packages this offer applies to (single offer only). */\n packages?: OfferPackageSummary[];\n}\n\n/** List offers for the current account (API key scoped). */\nexport async function getOffers(): Promise<OfferPublic[] | null> {\n const data = await serverFetch<OfferPublic[]>('/public/offers', defaultOptions);\n return Array.isArray(data) ? data : null;\n}\n\n/** Single offer with services for detail page. */\nexport async function getOffer(id: number | string): Promise<OfferPublic | null> {\n return serverFetch<OfferPublic>(`/public/offers/${id}`, defaultOptions);\n}\n\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/lib/server-api.ts"],"sourcesContent":["/**\n * Server-side API client for SSR\n * API key is stored securely on the server - never exposed to browser\n */\n\nimport type { CompanyInformation } from '../types/api/company-information';\nimport type { Service } from '../types/api/service';\nimport type { WebsitePhotos } from '../types/api/website-photos';\n\nconst API_URL = process.env.API_URL || 'http://localhost:3000/api/v1';\nconst API_KEY = process.env.API_KEY || '';\n\ninterface FetchOptions {\n cache?: RequestCache;\n revalidate?: number;\n}\n\nasync function serverFetch<T>(endpoint: string, options: FetchOptions = {}): Promise<T | null> {\n const url = `${API_URL}${endpoint}`;\n \n try {\n const fetchOptions: RequestInit & { next?: { revalidate?: number } } = {\n headers: {\n 'X-API-Key': API_KEY,\n 'Content-Type': 'application/json',\n },\n cache: options.cache,\n };\n \n if (options.revalidate) {\n fetchOptions.next = { revalidate: options.revalidate };\n }\n \n const response = await fetch(url, fetchOptions);\n\n if (!response.ok) {\n console.error(`[Server API] Error ${response.status} for ${endpoint}`);\n return null;\n }\n\n const json = await response.json();\n \n // Rails API returns { data: [...], meta: {...} }\n return json.data ?? json;\n } catch (error) {\n console.error(`[Server API] Failed to fetch ${endpoint}:`, error);\n return null;\n }\n}\n\n/**\n * Generic serverApi object for flexible endpoint access\n */\nexport const serverApi = {\n get: <T = unknown>(endpoint: string, options?: FetchOptions): Promise<T | null> => {\n return serverFetch<T>(endpoint, options || { revalidate: 60 });\n }\n};\n\n// Revalidate data every 60 seconds (ISR)\nconst defaultOptions: FetchOptions = { revalidate: 60 };\n\nexport async function getCompanyInformation(): Promise<CompanyInformation | null> {\n return serverFetch<CompanyInformation>('/public/company_information', defaultOptions);\n}\n\n/** Ads config (e.g. Meta Pixel). Returns { meta_pixel_id?: string } or {}. Only present when account has connected Meta Ads. */\nexport async function getAdsConfig(): Promise<{ meta_pixel_id?: string } | null> {\n const data = await serverFetch<{ meta_pixel_id?: string }>('/public/ads_config', defaultOptions);\n return data ?? null;\n}\n\n/** Extract Meta Pixel ID from ads config for use with <MetaPixel pixelId={...} />. Only returns a value when present and valid (numeric). */\nexport function getMetaPixelId(adsConfig: { meta_pixel_id?: string } | null | undefined): string | null {\n const id = adsConfig && typeof adsConfig === 'object' && 'meta_pixel_id' in adsConfig && adsConfig.meta_pixel_id;\n const str = id != null && id !== '' ? String(id).trim() : '';\n return str !== '' && str !== 'null' && /^\\d+$/.test(str) ? str : null;\n}\n\nexport async function getServices() {\n return serverFetch('/public/services', defaultOptions);\n}\n\nexport async function getService(slug: string): Promise<Service | null> {\n return serverFetch<Service>(`/public/services/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getLocations() {\n return serverFetch('/public/locations', defaultOptions);\n}\n\nexport async function getLocation(slug: string) {\n return serverFetch(`/public/locations/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getReviews() {\n return serverFetch('/public/reviews', defaultOptions);\n}\n\nexport async function getFAQs() {\n return serverFetch('/public/faq_questions', defaultOptions);\n}\n\nexport async function getBlogPosts() {\n return serverFetch('/public/blog_posts', defaultOptions);\n}\n\nexport async function getBlogPost(slug: string) {\n return serverFetch(`/public/blog_posts/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getTeamMembers() {\n return serverFetch('/public/team_members', defaultOptions);\n}\n\nexport async function getWebsitePhotos(): Promise<WebsitePhotos | null> {\n return serverFetch<WebsitePhotos>('/public/website_photos', defaultOptions);\n}\n\nexport async function getJobPostings() {\n return serverFetch('/public/job_postings', defaultOptions);\n}\n\nexport async function getJobPosting(slug: string) {\n return serverFetch(`/public/job_postings/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getSocialPosts() {\n return serverFetch('/public/social_posts', defaultOptions);\n}\n\n/** Packages (bundles of service items). */\nexport async function getPackages() {\n return serverFetch('/public/packages', defaultOptions);\n}\n\nexport async function getPackage(slug: string) {\n return serverFetch(`/public/packages/by_slug/${encodeURIComponent(slug)}`, defaultOptions);\n}\n\n// Alias for testimonials (API uses \"reviews\")\nexport async function getTestimonials() {\n return getReviews();\n}\n\n/** Form definition for dynamic form rendering (fields may include optional placeholder). */\nexport async function getForm(formType: string) {\n type FormDefinition = import('../types/api/form').FormDefinition;\n return serverFetch<FormDefinition>(`/public/forms/${encodeURIComponent(formType)}`, defaultOptions);\n}\n\n"],"mappings":";AASA,IAAM,UAAU,QAAQ,IAAI,WAAW;AACvC,IAAM,UAAU,QAAQ,IAAI,WAAW;AAOvC,eAAe,YAAe,UAAkB,UAAwB,CAAC,GAAsB;AAjB/F;AAkBE,QAAM,MAAM,GAAG,OAAO,GAAG,QAAQ;AAEjC,MAAI;AACF,UAAM,eAAiE;AAAA,MACrE,SAAS;AAAA,QACP,aAAa;AAAA,QACb,gBAAgB;AAAA,MAClB;AAAA,MACA,OAAO,QAAQ;AAAA,IACjB;AAEA,QAAI,QAAQ,YAAY;AACtB,mBAAa,OAAO,EAAE,YAAY,QAAQ,WAAW;AAAA,IACvD;AAEA,UAAM,WAAW,MAAM,MAAM,KAAK,YAAY;AAE9C,QAAI,CAAC,SAAS,IAAI;AAChB,cAAQ,MAAM,sBAAsB,SAAS,MAAM,QAAQ,QAAQ,EAAE;AACrE,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,MAAM,SAAS,KAAK;AAGjC,YAAO,UAAK,SAAL,YAAa;AAAA,EACtB,SAAS,OAAO;AACd,YAAQ,MAAM,gCAAgC,QAAQ,KAAK,KAAK;AAChE,WAAO;AAAA,EACT;AACF;AAKO,IAAM,YAAY;AAAA,EACvB,KAAK,CAAc,UAAkB,YAA8C;AACjF,WAAO,YAAe,UAAU,WAAW,EAAE,YAAY,GAAG,CAAC;AAAA,EAC/D;AACF;AAGA,IAAM,iBAA+B,EAAE,YAAY,GAAG;AAEtD,eAAsB,wBAA4D;AAChF,SAAO,YAAgC,+BAA+B,cAAc;AACtF;AAGA,eAAsB,eAA2D;AAC/E,QAAM,OAAO,MAAM,YAAwC,sBAAsB,cAAc;AAC/F,SAAO,sBAAQ;AACjB;AAGO,SAAS,eAAe,WAAyE;AACtG,QAAM,KAAK,aAAa,OAAO,cAAc,YAAY,mBAAmB,aAAa,UAAU;AACnG,QAAM,MAAM,MAAM,QAAQ,OAAO,KAAK,OAAO,EAAE,EAAE,KAAK,IAAI;AAC1D,SAAO,QAAQ,MAAM,QAAQ,UAAU,QAAQ,KAAK,GAAG,IAAI,MAAM;AACnE;AAEA,eAAsB,cAAc;AAClC,SAAO,YAAY,oBAAoB,cAAc;AACvD;AAEA,eAAsB,WAAW,MAAuC;AACtE,SAAO,YAAqB,4BAA4B,IAAI,IAAI,cAAc;AAChF;AAEA,eAAsB,eAAe;AACnC,SAAO,YAAY,qBAAqB,cAAc;AACxD;AAEA,eAAsB,YAAY,MAAc;AAC9C,SAAO,YAAY,6BAA6B,IAAI,IAAI,cAAc;AACxE;AAEA,eAAsB,aAAa;AACjC,SAAO,YAAY,mBAAmB,cAAc;AACtD;AAEA,eAAsB,UAAU;AAC9B,SAAO,YAAY,yBAAyB,cAAc;AAC5D;AAEA,eAAsB,eAAe;AACnC,SAAO,YAAY,sBAAsB,cAAc;AACzD;AAEA,eAAsB,YAAY,MAAc;AAC9C,SAAO,YAAY,8BAA8B,IAAI,IAAI,cAAc;AACzE;AAEA,eAAsB,iBAAiB;AACrC,SAAO,YAAY,wBAAwB,cAAc;AAC3D;AAEA,eAAsB,mBAAkD;AACtE,SAAO,YAA2B,0BAA0B,cAAc;AAC5E;AAEA,eAAsB,iBAAiB;AACrC,SAAO,YAAY,wBAAwB,cAAc;AAC3D;AAEA,eAAsB,cAAc,MAAc;AAChD,SAAO,YAAY,gCAAgC,IAAI,IAAI,cAAc;AAC3E;AAEA,eAAsB,iBAAiB;AACrC,SAAO,YAAY,wBAAwB,cAAc;AAC3D;AAGA,eAAsB,cAAc;AAClC,SAAO,YAAY,oBAAoB,cAAc;AACvD;AAEA,eAAsB,WAAW,MAAc;AAC7C,SAAO,YAAY,4BAA4B,mBAAmB,IAAI,CAAC,IAAI,cAAc;AAC3F;AAGA,eAAsB,kBAAkB;AACtC,SAAO,WAAW;AACpB;AAGA,eAAsB,QAAQ,UAAkB;AAE9C,SAAO,YAA4B,iBAAiB,mBAAmB,QAAQ,CAAC,IAAI,cAAc;AACpG;","names":[]}
@@ -1,8 +1,8 @@
1
1
  import { Theme } from '../themes/index.js';
2
- export { B as BlogPost, c as BlogPostAuthor, a as BlogPostParams, b as BlogPostResponse, d as BlogPostTag } from '../blog-post-D7HFCDp1.js';
3
- export { C as CompanyInformation, a as CompanyInformationResponse, F as FormDefinition, b as FormFieldDefinition, c as FormFieldItem, d as FormType, S as Service, e as ServiceItem, f as ServiceParams, g as ServiceResponse } from '../form-BLZuTGkr.js';
4
- import { P as PhotoAttachment } from '../photos-8jMeetqV.js';
5
- export { a as Photo, b as WebsitePhoto, W as WebsitePhotos, c as WebsitePhotosResponse } from '../photos-8jMeetqV.js';
2
+ export { B as BlogPost, a as BlogPostAuthor, b as BlogPostParams, c as BlogPostResponse, d as BlogPostTag } from '../blog-post-DGjaJ3wf.js';
3
+ export { C as CompanyInformation, a as CompanyInformationResponse, F as FormDefinition, b as FormFieldDefinition, c as FormFieldItem, d as FormType, O as OfferPublic, S as Service, e as ServiceItem, f as ServiceParams, g as ServiceResponse } from '../form-CpsCONG5.js';
4
+ import { P as PhotoAttachment } from '../website-photos-Bm-CBK9g.js';
5
+ export { a as Photo, b as WebsitePhoto, W as WebsitePhotos, c as WebsitePhotosResponse } from '../website-photos-Bm-CBK9g.js';
6
6
 
7
7
  interface NavItem {
8
8
  label: string;
@@ -1,4 +1,4 @@
1
- import { P as PhotoAttachment, W as WebsitePhotos } from '../photos-8jMeetqV.js';
1
+ import { P as PhotoAttachment, W as WebsitePhotos } from '../website-photos-Bm-CBK9g.js';
2
2
 
3
3
  /**
4
4
  * Helper functions for extracting photo URLs from photo associations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keystone-design-bootstrap",
3
- "version": "1.0.50",
3
+ "version": "1.0.53",
4
4
  "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -0,0 +1,115 @@
1
+ "use client";
2
+
3
+ import React, { useRef, useState } from 'react';
4
+ import { Form, Button } from '../elements';
5
+ import { DynamicFormFields } from '../components/DynamicFormFields';
6
+ import type { FormDefinition } from '../../types/api/form';
7
+ import { useFormDefinitions } from '../../next/contexts/form-definitions';
8
+
9
+ export interface EmailSignupSectionProps {
10
+ title?: string;
11
+ subtitle?: string;
12
+ buttonText?: string;
13
+ successMessage?: string;
14
+ /** Override the form definition (falls back to context). */
15
+ formDefinition?: FormDefinition | null;
16
+ }
17
+
18
+ export const EmailSignupSection = ({
19
+ title = "Stay in the loop",
20
+ subtitle = "Subscribe to our newsletter for updates, tips, and exclusive offers.",
21
+ buttonText = "Subscribe",
22
+ successMessage = "You're subscribed! Thank you.",
23
+ formDefinition,
24
+ }: EmailSignupSectionProps) => {
25
+ const { marketingListSignupFormDefinition } = useFormDefinitions();
26
+ const resolvedFormDefinition = formDefinition ?? marketingListSignupFormDefinition;
27
+ const [isSubmitting, setIsSubmitting] = useState(false);
28
+ const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
29
+ const [statusMessage, setStatusMessage] = useState('');
30
+ const formRef = useRef<HTMLFormElement>(null);
31
+
32
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
33
+ e.preventDefault();
34
+ setIsSubmitting(true);
35
+ setSubmitStatus('idle');
36
+ setStatusMessage('');
37
+
38
+ const formData = new FormData(e.currentTarget);
39
+ const data: Record<string, string> = { formType: 'marketing_list_signup' };
40
+ formData.forEach((value, key) => {
41
+ if (key.endsWith('_prefix')) return;
42
+ if (typeof value === 'string') data[key] = value;
43
+ });
44
+
45
+ try {
46
+ const response = await fetch('/api/form/', {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify(data),
50
+ });
51
+ const result = await response.json();
52
+
53
+ if (result.success) {
54
+ setSubmitStatus('success');
55
+ setStatusMessage(result.message || successMessage);
56
+ formRef.current?.reset();
57
+ setTimeout(() => setSubmitStatus('idle'), 6000);
58
+ } else {
59
+ setSubmitStatus('error');
60
+ setStatusMessage(result.error || 'Something went wrong. Please try again.');
61
+ }
62
+ } catch {
63
+ setSubmitStatus('error');
64
+ setStatusMessage('Network error. Please try again later.');
65
+ }
66
+
67
+ setIsSubmitting(false);
68
+ };
69
+
70
+ if (!resolvedFormDefinition) return null;
71
+
72
+ return (
73
+ <section className="bg-secondary py-16 md:py-20">
74
+ <div className="mx-auto max-w-container px-4 md:px-8">
75
+ <div className="mx-auto max-w-xl text-center">
76
+ <h2 className="font-display text-display-sm font-semibold text-primary md:text-display-md">
77
+ {title}
78
+ </h2>
79
+ {subtitle && (
80
+ <p className="mt-4 font-body text-lg text-tertiary">
81
+ {subtitle}
82
+ </p>
83
+ )}
84
+ <Form
85
+ ref={formRef}
86
+ onSubmit={handleSubmit}
87
+ className="mt-8 flex flex-col gap-6 text-left"
88
+ >
89
+ <DynamicFormFields form={resolvedFormDefinition} />
90
+ {submitStatus === 'success' && (
91
+ <div className="rounded-lg bg-success-50 p-4 text-success-700">
92
+ {statusMessage || successMessage}
93
+ </div>
94
+ )}
95
+ {submitStatus === 'error' && (
96
+ <div className="rounded-lg bg-error-50 p-4 text-error-700">
97
+ {statusMessage}
98
+ </div>
99
+ )}
100
+ <Button
101
+ type="submit"
102
+ color="primary"
103
+ size="xl"
104
+ isDisabled={isSubmitting}
105
+ isLoading={isSubmitting}
106
+ className="w-full"
107
+ >
108
+ {isSubmitting ? 'Subscribing...' : buttonText}
109
+ </Button>
110
+ </Form>
111
+ </div>
112
+ </div>
113
+ </section>
114
+ );
115
+ };
@@ -30,13 +30,15 @@ export function HeaderNavigation({
30
30
  // Timeout ref for delayed dropdown closing
31
31
  const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
32
32
 
33
- // Track scroll position for header animation
33
+ // Track scroll position for header animation; close dropdown on scroll to
34
+ // avoid a stale fixed `top` value creating a gap between the nav and dropdown.
34
35
  React.useEffect(() => {
35
36
  const handleScroll = () => {
36
37
  setIsScrolled(window.scrollY > 10);
38
+ setActiveDropdown(null);
37
39
  };
38
40
 
39
- window.addEventListener('scroll', handleScroll);
41
+ window.addEventListener('scroll', handleScroll, { passive: true });
40
42
  return () => window.removeEventListener('scroll', handleScroll);
41
43
  }, []);
42
44
 
@@ -67,7 +69,8 @@ export function HeaderNavigation({
67
69
  }
68
70
  }, []);
69
71
 
70
- // Open dropdown immediately
72
+ // Open dropdown immediately; clear it when hovering a childless item so the
73
+ // previous dropdown doesn't linger while a different top-level item is active.
71
74
  const handleMouseEnter = useCallback((item: NavItem, e: React.MouseEvent<HTMLDivElement>) => {
72
75
  cancelCloseTimeout();
73
76
  if (item.children && item.children.length > 0) {
@@ -77,6 +80,8 @@ export function HeaderNavigation({
77
80
  setDropdownTop(rect.bottom);
78
81
  }
79
82
  setActiveDropdown(item.label);
83
+ } else {
84
+ setActiveDropdown(null);
80
85
  }
81
86
  }, [cancelCloseTimeout]);
82
87
 
@@ -51,6 +51,8 @@ export function HeaderNavigation({
51
51
  cancelCloseTimeout();
52
52
  if (item.children && item.children.length > 0) {
53
53
  setActiveDropdown(item.label);
54
+ } else {
55
+ setActiveDropdown(null);
54
56
  }
55
57
  }, [cancelCloseTimeout]);
56
58
 
@@ -54,11 +54,14 @@ export function HeaderNavigation({
54
54
  }
55
55
  }, []);
56
56
 
57
- // Open dropdown immediately
57
+ // Open dropdown immediately; clear it when hovering a childless item so the
58
+ // previous dropdown doesn't linger while a different top-level item is active.
58
59
  const handleMouseEnter = useCallback((item: NavItem) => {
59
60
  cancelCloseTimeout();
60
61
  if (item.children && item.children.length > 0) {
61
62
  setActiveDropdown(item.label);
63
+ } else {
64
+ setActiveDropdown(null);
62
65
  }
63
66
  }, [cancelCloseTimeout]);
64
67
 
@@ -46,9 +46,6 @@ import { HomeHeroComponent as BaseHomeHeroComponent } from './home-hero-componen
46
46
  import { JobDetailHero as BaseJobDetailHero } from './hero-job-detail';
47
47
  import { JobDetailSection as BaseJobDetailSection } from './job-detail-section';
48
48
  import { PolicyDocumentSection as BasePolicyDocumentSection } from './policy-document-section';
49
- import { OffersSection as BaseOffersSection } from './offers-section';
50
- import { OffersGallery as BaseOffersGallery } from './offers-gallery';
51
- import { OfferDetailSection as BaseOfferDetailSection } from './offer-detail';
52
49
  // Import variant files to trigger their registration
53
50
  // Aman theme variants
54
51
  import './hero-home.aman';
@@ -94,7 +91,7 @@ import './social-media-grid.barelux';
94
91
  import './contact-section.barelux';
95
92
  import './footer-home.barelux';
96
93
 
97
- // Balance theme variants (offers-section uses base for balance)
94
+ // Balance theme variants
98
95
  import './hero-home.balance';
99
96
  import './header-navigation.balance';
100
97
  import './footer-home.balance';
@@ -181,19 +178,11 @@ export const HomeHeroComponent = createThemedExport('home-hero-component', BaseH
181
178
  // Re-export application form (client component, no theme variants)
182
179
  export { JobApplicationForm } from './job-application-form';
183
180
 
184
- // Offers section (themed; embeddable on multiple pages)
185
- export const OffersSection = createThemedExport('offers-section', BaseOffersSection as unknown as React.ComponentType<Record<string, unknown>>);
181
+ // Email / newsletter signup section (client component, no theme variants needed)
182
+ export { EmailSignupSection } from './email-signup-section';
183
+ export type { EmailSignupSectionProps } from './email-signup-section';
186
184
 
187
- // Full offers page gallery (themed; use on /offers page, Aman = blog-style)
188
- export const OffersGallery = createThemedExport('offers-gallery', BaseOffersGallery as unknown as React.ComponentType<Record<string, unknown>>);
189
-
190
- // Single offer detail (themed; use on /offers/[id] page)
191
- export const OfferDetailSection = createThemedExport('offer-detail', BaseOfferDetailSection as unknown as React.ComponentType<Record<string, unknown>>);
192
-
193
- // Full offers page grid (no theme variant; fallback when gallery not used)
194
- export { OffersGrid } from './offers-grid';
195
-
196
- // Service Menu: up to 3 carousel rows (Offers, Packages, Services) with smaller cards
185
+ // Service Menu: packages + treatments; nested specials as badges / modal callouts
197
186
  export { ServiceMenuSection } from './service-menu-section';
198
187
  export type { ServiceMenuSectionProps, PackagePublic } from './service-menu-section';
199
188