@salesmind-ai/design-system 1.2.0 → 1.4.0

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.
@@ -132,7 +132,8 @@ function createSchemaGenerators(brand, supportedLanguages = ["en", "fr", "es"])
132
132
  };
133
133
  },
134
134
  /** SoftwareApplication schema — Platform Hub, Platform Feature, Pricing. */
135
- softwareApplication(name, description, url, locale = "en") {
135
+ softwareApplication(name, description, url, locale = "en", options = {}) {
136
+ const aggregateRating = options.aggregateRating ?? brand.aggregateRating;
136
137
  return {
137
138
  "@context": "https://schema.org",
138
139
  "@type": "SoftwareApplication",
@@ -144,17 +145,15 @@ function createSchemaGenerators(brand, supportedLanguages = ["en", "fr", "es"])
144
145
  operatingSystem: "Web",
145
146
  inLanguage: locale,
146
147
  availableLanguage: [...supportedLanguages],
147
- offers: {
148
- "@type": "AggregateOffer",
149
- priceCurrency: "USD",
150
- lowPrice: "0",
151
- highPrice: "0",
152
- offerCount: "2",
153
- priceSpecification: {
154
- "@type": "PriceSpecification",
155
- description: "Custom pricing \u2014 contact sales for a tailored quote"
148
+ ...aggregateRating ? {
149
+ aggregateRating: {
150
+ "@type": "AggregateRating",
151
+ ratingValue: String(aggregateRating.ratingValue),
152
+ ratingCount: String(aggregateRating.reviewCount),
153
+ bestRating: String(aggregateRating.bestRating ?? 5),
154
+ worstRating: String(aggregateRating.worstRating ?? 1)
156
155
  }
157
- },
156
+ } : {},
158
157
  author: {
159
158
  "@type": "Organization",
160
159
  "@id": ENTITY_IDS.organization,
@@ -563,4 +562,4 @@ function JsonLd({ data, nonce }) {
563
562
 
564
563
  export { JsonLd, aggregateRatingFromTestimonials, buildPageGraph, canonicalUrl, createEntityIds, createSchemaGenerators };
565
564
  //# sourceMappingURL=out.js.map
566
- //# sourceMappingURL=chunk-NB3BDIQO.js.map
565
+ //# sourceMappingURL=chunk-25TUCNN6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/web/seo/schema-generators.ts","../src/web/seo/JsonLd.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * Schema.org Generator System\n *\n * Pure functions that generate structured data objects for SEO.\n * Each generator accepts a `BrandConfig` and optional `locale` for `inLanguage` injection.\n *\n * Architecture:\n * - Each generator returns an object with its own `@context` (standalone-safe).\n * - `buildPageGraph()` strips individual `@context` and wraps all schemas\n * in a single `{\"@context\": \"...\", \"@graph\": [...]}` for unified output.\n * - Use `JsonLd` React component to render the @graph into a single <script> tag.\n *\n * @example\n * ```ts\n * import { createSchemaGenerators, buildPageGraph } from '@salesmind-ai/design-system/web';\n *\n * const seo = createSchemaGenerators({\n * name: 'SalesMind AI',\n * url: 'https://sales-mind.ai',\n * description: 'AI sales automation',\n * tagline: 'AI-powered sales',\n * founder: 'Julien Gadea',\n * });\n *\n * const graph = buildPageGraph(\n * seo.organization(),\n * seo.website(),\n * seo.webPage('/pricing', 'Pricing', '...'),\n * );\n * ```\n */\n\nimport type {\n BrandConfig,\n ArticleMeta,\n BreadcrumbItem,\n CareerForSchema,\n CustomerOrganizationMeta,\n FAQItem,\n GlossaryTermForSchema,\n HowToMeta,\n HowToStep,\n ItemForList,\n PersonMeta,\n PricingPlan,\n ServiceMotion,\n SoftwareApplicationSchemaOptions,\n TestimonialForSchema,\n TranscriptForSchema,\n VideoObjectMeta,\n} from './types';\n\n// ─── Internal Helpers ────────────────────────────────────────────────────────\n\n/** Convert \"MM:SS\" or \"HH:MM:SS\" to ISO 8601 duration \"PT…\" */\nfunction toISO8601Duration(duration: string): string {\n if (duration.startsWith('PT')) return duration;\n const parts = duration.split(':').map(Number);\n if (parts.some(isNaN)) return duration;\n if (parts.length === 3) return `PT${parts[0]}H${parts[1]}M${parts[2]}S`;\n if (parts.length === 2) return `PT${parts[0]}M${parts[1]}S`;\n return `PT${parts[0]}S`;\n}\n\n/** Convert YouTube watch URL to embed URL */\nfunction toEmbedUrl(url: string): string | undefined {\n try {\n const parsed = new URL(url);\n if (parsed.hostname.includes('youtube.com') && parsed.searchParams.has('v')) {\n return `https://www.youtube.com/embed/${parsed.searchParams.get('v')}`;\n }\n if (parsed.hostname === 'youtu.be') {\n return `https://www.youtube.com/embed${parsed.pathname}`;\n }\n return url;\n } catch {\n return undefined;\n }\n}\n\n/** Map career type string to Schema.org employmentType */\nfunction mapEmploymentType(type?: string | null): string {\n if (!type) return 'FULL_TIME';\n const lower = type.toLowerCase();\n if (lower.includes('part')) return 'PART_TIME';\n if (lower.includes('contract')) return 'CONTRACTOR';\n if (lower.includes('intern')) return 'INTERN';\n if (lower.includes('temporary') || lower.includes('temp')) return 'TEMPORARY';\n return 'FULL_TIME';\n}\n\n// ─── Entity ID Factory ──────────────────────────────────────────────────────\n\n/**\n * Create stable entity ID anchors for @graph cross-referencing.\n * Every entity in the knowledge graph gets a permanent @id.\n * Google resolves @id references across the entire site.\n */\n// Returns either `pathOrUrl` as-is (when it is an absolute http(s) URL) or\n// concatenated to `brand.url`. Defensive against callers that pass an absolute\n// URL where a relative path is expected — prevents the doubled-prefix bug\n// `https://brand.aihttps://brand.ai/page#anchor`.\nfunction normalizeEntityPath(brandUrl: string, pathOrUrl: string): string {\n if (/^https?:\\/\\//i.test(pathOrUrl)) return pathOrUrl;\n return `${brandUrl}${pathOrUrl}`;\n}\n\nexport function createEntityIds(brand: BrandConfig) {\n return {\n organization: `${brand.url}/#organization`,\n website: `${brand.url}/#website`,\n software: `${brand.url}/#software`,\n product: `${brand.url}/pricing#product`,\n webpage: (path: string) => `${normalizeEntityPath(brand.url, path)}#webpage`,\n article: (path: string) => `${normalizeEntityPath(brand.url, path)}#article`,\n faq: (path: string) => `${normalizeEntityPath(brand.url, path)}#faq`,\n breadcrumb: (path: string) => `${normalizeEntityPath(brand.url, path)}#breadcrumb`,\n person: (slug: string) => `${brand.url}/company/team#person-${slug}`,\n video: (path: string) => `${normalizeEntityPath(brand.url, path)}#video`,\n review: (id: string) => `${brand.url}/#review-${id}`,\n service: (slug: string) => `${brand.url}/#service-${slug}`,\n customerOrganization: (slug: string) => `${brand.url}/#customer-org-${slug}`,\n customerPerson: (slug: string) => `${brand.url}/#customer-person-${slug}`,\n } as const;\n}\n\n// ─── Schema Generator Factory ───────────────────────────────────────────────\n\n/**\n * Create all schema generators bound to a specific brand configuration.\n * Returns an object with methods for each schema type.\n */\nexport function createSchemaGenerators(\n brand: BrandConfig,\n supportedLanguages: readonly string[] = ['en', 'fr', 'es'],\n) {\n const ENTITY_IDS = createEntityIds(brand);\n\n return {\n /** Organization schema — Homepage + About page. */\n organization(locale: string = 'en') {\n return {\n '@context': 'https://schema.org',\n '@type': 'Organization',\n '@id': ENTITY_IDS.organization,\n name: brand.name,\n url: brand.url,\n logo: {\n '@type': 'ImageObject',\n url: `${brand.url}/logo.svg`,\n },\n description: brand.description,\n inLanguage: locale,\n availableLanguage: [...supportedLanguages],\n founder: {\n '@type': 'Person',\n name: brand.founder,\n },\n ...(brand.sameAs && brand.sameAs.length > 0 ? { sameAs: brand.sameAs } : {}),\n contactPoint: [] as Record<string, unknown>[],\n areaServed: 'Worldwide',\n };\n },\n\n /** WebSite schema — Homepage only (with SearchAction). */\n website(locale: string = 'en') {\n return {\n '@context': 'https://schema.org',\n '@type': 'WebSite',\n '@id': ENTITY_IDS.website,\n name: brand.name,\n url: brand.url,\n description: brand.tagline,\n inLanguage: locale,\n publisher: {\n '@type': 'Organization',\n '@id': ENTITY_IDS.organization,\n name: brand.name,\n },\n potentialAction: [\n {\n '@type': 'SearchAction',\n target: {\n '@type': 'EntryPoint',\n urlTemplate: `${brand.url}/blog?q={search_term_string}`,\n },\n 'query-input': 'required name=search_term_string',\n },\n {\n '@type': 'ReadAction',\n target: brand.url,\n },\n ],\n };\n },\n\n /** WebPage schema — Every public page. Links to WebSite + Breadcrumb via @id. */\n webPage(path: string, title: string, description: string, locale: string = 'en', speakable?: Record<string, unknown>) {\n return {\n '@context': 'https://schema.org',\n '@type': 'WebPage',\n '@id': ENTITY_IDS.webpage(path),\n url: `${brand.url}${path}`,\n name: title,\n description,\n inLanguage: locale,\n isPartOf: { '@type': 'WebSite', '@id': ENTITY_IDS.website },\n breadcrumb: { '@id': ENTITY_IDS.breadcrumb(path) },\n potentialAction: {\n '@type': 'ReadAction',\n target: `${brand.url}${path}`,\n },\n ...(speakable ? { speakable } : {}),\n };\n },\n\n /** SoftwareApplication schema — Platform Hub, Platform Feature, Pricing. */\n softwareApplication(\n name?: string,\n description?: string,\n url?: string,\n locale: string = 'en',\n options: SoftwareApplicationSchemaOptions = {},\n ) {\n const aggregateRating = options.aggregateRating ?? brand.aggregateRating;\n\n return {\n '@context': 'https://schema.org',\n '@type': 'SoftwareApplication',\n '@id': ENTITY_IDS.software,\n name: name || brand.name,\n description: description || brand.description,\n url: url || brand.url,\n applicationCategory: 'BusinessApplication',\n operatingSystem: 'Web',\n inLanguage: locale,\n availableLanguage: [...supportedLanguages],\n ...(aggregateRating\n ? {\n aggregateRating: {\n '@type': 'AggregateRating',\n ratingValue: String(aggregateRating.ratingValue),\n ratingCount: String(aggregateRating.reviewCount),\n bestRating: String(aggregateRating.bestRating ?? 5),\n worstRating: String(aggregateRating.worstRating ?? 1),\n },\n }\n : {}),\n author: {\n '@type': 'Organization',\n '@id': ENTITY_IDS.organization,\n name: brand.name,\n },\n };\n },\n\n /** Article schema — Blog posts, Comparison pages. */\n article(meta: ArticleMeta, locale: string = 'en') {\n let path: string;\n try {\n path = new URL(meta.url).pathname;\n } catch {\n path = meta.url.startsWith('/') ? meta.url : `/${meta.url}`;\n }\n\n return {\n '@context': 'https://schema.org',\n '@type': 'Article',\n '@id': ENTITY_IDS.article(path),\n headline: meta.title,\n description: meta.description,\n url: meta.url.startsWith('http') ? meta.url : `${brand.url}${meta.url}`,\n image: meta.image || `${brand.url}/og-default.png`,\n datePublished: meta.publishedTime,\n dateModified: meta.modifiedTime,\n inLanguage: locale,\n author: meta.authorSlug\n ? { '@id': ENTITY_IDS.person(meta.authorSlug) }\n : {\n '@type': 'Person',\n name: meta.authorName,\n url: meta.authorUrl || `${brand.url}/company/team`,\n },\n publisher: {\n '@type': 'Organization',\n '@id': ENTITY_IDS.organization,\n name: brand.name,\n logo: {\n '@type': 'ImageObject',\n url: `${brand.url}/logo.svg`,\n },\n },\n isPartOf: { '@type': 'WebSite', '@id': ENTITY_IDS.website },\n mainEntityOfPage: { '@id': ENTITY_IDS.webpage(path) },\n articleSection: meta.section,\n keywords: meta.tags?.join(', '),\n speakable: {\n '@type': 'SpeakableSpecification',\n cssSelector: meta.speakableCssSelectors || ['article h1', 'article .summary', 'article .lead'],\n },\n ...(meta.about ? { about: meta.about } : {}),\n ...(meta.video ? { video: meta.video } : {}),\n };\n },\n\n /** FAQPage schema — FAQ hub, Solutions, Platform Features, Comparisons, Integrations. */\n faqPage(items: FAQItem[], pageUrl: string, locale: string = 'en') {\n return {\n '@context': 'https://schema.org',\n '@type': 'FAQPage',\n '@id': ENTITY_IDS.faq(pageUrl),\n inLanguage: locale,\n mainEntity: items.map((item) => ({\n '@type': 'Question',\n name: item.question,\n acceptedAnswer: {\n '@type': 'Answer',\n text: item.answer,\n inLanguage: locale,\n },\n })),\n };\n },\n\n /** BreadcrumbList schema — Every public page. */\n breadcrumb(items: BreadcrumbItem[], locale: string = 'en') {\n const lastUrl = items.length > 0 ? items[items.length - 1].url : '/';\n const path = lastUrl.startsWith('http') ? new URL(lastUrl).pathname : lastUrl;\n\n return {\n '@context': 'https://schema.org',\n '@type': 'BreadcrumbList',\n '@id': ENTITY_IDS.breadcrumb(path),\n inLanguage: locale,\n itemListElement: items.map((item, index) => ({\n '@type': 'ListItem',\n position: index + 1,\n name: item.name,\n item: item.url.startsWith('http') ? item.url : `${brand.url}${item.url}`,\n })),\n };\n },\n\n /** Person schema — Team page, Blog author, Comparison author. */\n person(person: PersonMeta, locale: string = 'en') {\n const slug = person.slug || person.name.toLowerCase().replace(/\\s+/g, '-');\n\n return {\n '@context': 'https://schema.org',\n '@type': 'Person',\n '@id': person.entityIdOverride || ENTITY_IDS.person(slug),\n name: person.name,\n jobTitle: person.jobTitle,\n url: person.url,\n image: person.image,\n inLanguage: locale,\n worksFor: person.worksFor || {\n '@type': 'Organization',\n '@id': ENTITY_IDS.organization,\n name: brand.name,\n },\n sameAs: person.sameAs || [],\n };\n },\n\n /** Review schema — Generated from testimonials. */\n review(testimonial: TestimonialForSchema) {\n return {\n '@context': 'https://schema.org',\n '@type': 'Review',\n '@id': ENTITY_IDS.review(testimonial.id),\n author: {\n '@type': 'Person',\n ...(testimonial.authorEntityId ? { '@id': testimonial.authorEntityId } : {}),\n name: testimonial.customer_name,\n ...(testimonial.occupation ? { jobTitle: testimonial.occupation } : {}),\n },\n reviewBody: testimonial.quote || '',\n ...(testimonial.star_rating\n ? {\n reviewRating: {\n '@type': 'Rating',\n ratingValue: String(testimonial.star_rating),\n bestRating: '5',\n worstRating: '1',\n },\n }\n : {}),\n ...(testimonial.review_date ? { datePublished: testimonial.review_date } : {}),\n itemReviewed: {\n '@type': 'SoftwareApplication',\n '@id': ENTITY_IDS.software,\n name: brand.name,\n },\n ...(testimonial.review_source\n ? {\n publisher: {\n '@type': 'Organization',\n name: testimonial.review_source,\n },\n }\n : {}),\n };\n },\n\n /** Product + multiple Offers schema — Pricing page. */\n productWithOffers(\n plans: PricingPlan[],\n testimonials: TestimonialForSchema[] = [],\n locale: string = 'en',\n ) {\n return {\n '@context': 'https://schema.org',\n '@type': 'Product',\n '@id': ENTITY_IDS.product,\n name: brand.name,\n description: brand.description,\n brand: {\n '@type': 'Organization',\n '@id': ENTITY_IDS.organization,\n name: brand.name,\n },\n inLanguage: locale,\n ...(testimonials.length > 0\n ? { aggregateRating: aggregateRatingFromTestimonials(testimonials) }\n : {}),\n offers: plans.map((plan) => ({\n '@type': 'Offer',\n name: plan.name,\n description: plan.description,\n price: plan.price.replace(/[^0-9.]/g, '') || '0',\n priceCurrency: plan.priceCurrency || 'USD',\n availability: 'https://schema.org/InStock',\n url: `${brand.url}/pricing`,\n ...(plan.billingPeriod\n ? {\n priceSpecification: {\n '@type': 'UnitPriceSpecification',\n price: plan.price.replace(/[^0-9.]/g, '') || '0',\n priceCurrency: plan.priceCurrency || 'USD',\n billingDuration: plan.billingPeriod,\n },\n }\n : {}),\n })),\n };\n },\n\n /** VideoObject schema — Transcript detail pages. */\n videoObject(transcript: TranscriptForSchema, locale: string = 'en') {\n const path = transcript.path || `/resources/transcripts/${transcript.slug}`;\n\n return {\n '@context': 'https://schema.org',\n '@type': 'VideoObject',\n '@id': ENTITY_IDS.video(path),\n name: transcript.title,\n description: transcript.summary || transcript.title,\n ...(transcript.video_link ? { contentUrl: transcript.video_link } : {}),\n ...(transcript.video_link ? { embedUrl: toEmbedUrl(transcript.video_link) } : {}),\n ...(transcript.duration\n ? { duration: toISO8601Duration(transcript.duration) }\n : {}),\n ...(transcript.published_at ? { uploadDate: transcript.published_at } : {}),\n ...(transcript.thumbnailUrl ? { thumbnailUrl: transcript.thumbnailUrl } : {}),\n inLanguage: locale,\n publisher: {\n '@type': 'Organization',\n '@id': ENTITY_IDS.organization,\n name: brand.name,\n },\n ...(transcript.transcript_text ? { transcript: transcript.transcript_text } : {}),\n };\n },\n\n /** CustomerOrganization schema — Customer's company for Success Story pages. */\n customerOrganization(meta: CustomerOrganizationMeta) {\n return {\n '@context': 'https://schema.org',\n '@type': 'Organization',\n '@id': ENTITY_IDS.customerOrganization(meta.slug),\n name: meta.name,\n ...(meta.url ? { url: meta.url } : {}),\n ...(meta.logo ? { logo: meta.logo } : {}),\n ...(meta.description ? { description: meta.description } : {}),\n ...(meta.industry ? { industry: meta.industry } : {}),\n ...(meta.sameAs && meta.sameAs.length > 0 ? { sameAs: meta.sameAs } : {}),\n };\n },\n\n /** Generic VideoObject — Success story main video and shorts. */\n videoObjectGeneric(meta: VideoObjectMeta, locale: string = 'en') {\n const videoId = meta.fragment\n ? `${brand.url}${meta.path}#${meta.fragment}`\n : ENTITY_IDS.video(meta.path);\n return {\n '@context': 'https://schema.org',\n '@type': 'VideoObject',\n '@id': videoId,\n name: meta.title,\n ...(meta.description ? { description: meta.description } : {}),\n ...(meta.videoUrl ? { contentUrl: meta.videoUrl } : {}),\n ...(meta.embedUrl ? { embedUrl: meta.embedUrl } : {}),\n ...(meta.thumbnailUrl ? { thumbnailUrl: meta.thumbnailUrl } : {}),\n ...(meta.duration ? { duration: meta.duration } : {}),\n ...(meta.uploadDate ? { uploadDate: meta.uploadDate } : {}),\n ...(meta.transcript ? { transcript: meta.transcript } : {}),\n inLanguage: locale,\n publisher: { '@id': ENTITY_IDS.organization },\n };\n },\n\n /** HowTo schema — Solution pages, tutorial blog posts. */\n howTo(steps: HowToStep[], meta: HowToMeta, locale: string = 'en') {\n return {\n '@context': 'https://schema.org',\n '@type': 'HowTo',\n name: meta.name,\n description: meta.description,\n inLanguage: locale,\n ...(meta.totalTime ? { totalTime: meta.totalTime } : {}),\n ...(meta.image ? { image: meta.image } : {}),\n step: steps.map((step, index) => ({\n '@type': 'HowToStep',\n position: index + 1,\n name: step.name,\n text: step.text,\n ...(step.url ? { url: step.url } : {}),\n ...(step.image ? { image: step.image } : {}),\n })),\n };\n },\n\n /** Service schema — Growth motion service offerings. */\n service(motion: ServiceMotion, locale: string = 'en') {\n return {\n '@context': 'https://schema.org',\n '@type': 'Service',\n '@id': ENTITY_IDS.service(motion.slug),\n name: motion.name,\n description: motion.description,\n url: motion.url.startsWith('http') ? motion.url : `${brand.url}${motion.url}`,\n provider: {\n '@type': 'Organization',\n '@id': ENTITY_IDS.organization,\n name: brand.name,\n },\n inLanguage: locale,\n availableLanguage: [...supportedLanguages],\n areaServed: 'Worldwide',\n ...(motion.price\n ? {\n offers: {\n '@type': 'Offer',\n price: motion.price.replace(/[^0-9.]/g, '') || '0',\n priceCurrency: motion.priceCurrency || 'USD',\n availability: 'https://schema.org/InStock',\n },\n }\n : {}),\n termsOfService: `${brand.url}/legal/terms`,\n };\n },\n\n /** JobPosting schema — Career detail pages. */\n jobPosting(job: CareerForSchema, locale: string = 'en') {\n return {\n '@context': 'https://schema.org',\n '@type': 'JobPosting',\n '@id': `${brand.url}/careers/${job.slug}#jobposting`,\n title: job.title,\n description: job.description || '',\n datePosted: job.published_at || job.created_at,\n employmentType: mapEmploymentType(job.type),\n hiringOrganization: {\n '@type': 'Organization',\n '@id': ENTITY_IDS.organization,\n name: brand.name,\n sameAs: brand.url,\n },\n ...(job.location\n ? {\n jobLocation: {\n '@type': 'Place',\n address: {\n '@type': 'PostalAddress',\n addressLocality: job.location,\n },\n },\n }\n : {\n jobLocationType: 'TELECOMMUTE',\n }),\n inLanguage: locale,\n };\n },\n\n /** ItemList schema — Hub/listing pages. */\n itemList(items: ItemForList[], listName: string) {\n return {\n '@context': 'https://schema.org',\n '@type': 'ItemList',\n name: listName,\n numberOfItems: items.length,\n itemListElement: items.map((item, index) => ({\n '@type': 'ListItem',\n position: item.position ?? index + 1,\n name: item.name,\n url: item.url.startsWith('http') ? item.url : `${brand.url}${item.url}`,\n ...(item.description ? { description: item.description } : {}),\n ...(item.image ? { image: item.image } : {}),\n })),\n };\n },\n\n /** DefinedTermSet schema — Glossary page. */\n definedTermSet(terms: GlossaryTermForSchema[], locale: string = 'en') {\n return {\n '@context': 'https://schema.org',\n '@type': 'DefinedTermSet',\n '@id': `${brand.url}/resources/glossary#termset`,\n name: 'AI Sales Glossary',\n inLanguage: locale,\n hasDefinedTerm: terms.map((term) => ({\n '@type': 'DefinedTerm',\n name: term.term,\n description: term.definition,\n url: term.url || `${brand.url}/resources/glossary#${term.slug}`,\n })),\n };\n },\n\n /** Speakable — marks content sections suitable for voice assistant reading. */\n speakable(cssSelectors: string[]): Record<string, unknown> {\n return {\n '@type': 'SpeakableSpecification',\n cssSelector: cssSelectors,\n };\n },\n\n /** Access the entity IDs for cross-referencing */\n entityIds: ENTITY_IDS,\n };\n}\n\n// ─── Standalone Utilities ───────────────────────────────────────────────────\n\n/**\n * Compute AggregateRating from real testimonial data.\n * Falls back to hardcoded 4.7/150 when no rated testimonials exist.\n * Returns a fragment (no @context) — meant to be embedded in Product/SoftwareApp.\n */\nexport function aggregateRatingFromTestimonials(testimonials: TestimonialForSchema[]) {\n const rated = testimonials.filter((t) => t.star_rating != null && t.star_rating > 0);\n\n if (rated.length === 0) {\n return {\n '@type': 'AggregateRating',\n ratingValue: '4.7',\n ratingCount: '150',\n bestRating: '5',\n worstRating: '1',\n };\n }\n\n const sum = rated.reduce((acc, t) => acc + (t.star_rating || 0), 0);\n const avg = (sum / rated.length).toFixed(1);\n\n return {\n '@type': 'AggregateRating',\n ratingValue: avg,\n ratingCount: String(rated.length),\n bestRating: '5',\n worstRating: '1',\n };\n}\n\n/**\n * Assemble multiple schema entities into a single @graph array.\n * Strips individual `@context` from each schema — the wrapper provides it once.\n *\n * @example\n * ```ts\n * const graph = buildPageGraph(\n * seo.organization(),\n * seo.website(),\n * seo.webPage('/pricing', 'Pricing', '...'),\n * seo.breadcrumb(crumbs),\n * );\n * ```\n */\nexport function buildPageGraph(\n ...schemas: Record<string, unknown>[]\n): Record<string, unknown> {\n const graph = schemas.map((schema) => {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { '@context': _, ...rest } = schema;\n return rest;\n });\n\n return {\n '@context': 'https://schema.org',\n '@graph': graph,\n };\n}\n\n/**\n * Build canonical URL from pathname.\n * Removes trailing slash (except root) and query params.\n *\n * @param baseUrl - The site's base URL (e.g. \"https://sales-mind.ai\")\n * @param pathname - The route path (e.g. \"/pricing\", \"/blog/post/foo\")\n * @param locale - BCP-47 locale code. 'en' = no prefix (default). Others get /{locale}/ prefix.\n */\nexport function canonicalUrl(baseUrl: string, pathname: string, locale?: string): string {\n const clean = pathname === '/' ? '/' : pathname.replace(/\\/+$/, '');\n\n if (locale && locale !== 'en') {\n const stripped = clean.replace(new RegExp(`^/${locale}(/|$)`), '$1') || '/';\n const path = stripped === '/' ? '' : stripped;\n return `${baseUrl}/${locale}${path}`;\n }\n\n return `${baseUrl}${clean}`;\n}\n","/**\n * JsonLd — Renders structured data as a `<script type=\"application/ld+json\">` tag.\n *\n * Framework-agnostic: works with any React-based framework (Next.js, Vite, Remix, etc.).\n * Safely serializes the data object and escapes `</script>` sequences.\n *\n * @example\n * ```tsx\n * import { JsonLd, buildPageGraph } from '@salesmind-ai/design-system/web';\n *\n * <JsonLd data={buildPageGraph(seo.organization(), seo.website())} />\n * ```\n */\nexport interface JsonLdProps {\n /** The structured data object (schema.org format) */\n data: Record<string, unknown>;\n /** Optional nonce for CSP headers */\n nonce?: string;\n}\n\nexport function JsonLd({ data, nonce }: JsonLdProps) {\n const json = JSON.stringify(data).replace(/<\\/script>/gi, '<\\\\/script>');\n\n return (\n <script\n type=\"application/ld+json\"\n nonce={nonce}\n dangerouslySetInnerHTML={{ __html: json }}\n />\n );\n}\n"]}
@@ -46,6 +46,48 @@ var loadClarity = createAnalyticsLoader({
46
46
  }
47
47
  }
48
48
  });
49
+
50
+ // src/web/analytics/region.ts
51
+ var NON_EEA_EUROPE_TIMEZONES = /* @__PURE__ */ new Set([
52
+ // Russia
53
+ "Europe/Moscow",
54
+ "Europe/Volgograd",
55
+ "Europe/Samara",
56
+ "Europe/Saratov",
57
+ "Europe/Ulyanovsk",
58
+ "Europe/Kaliningrad",
59
+ "Europe/Astrakhan",
60
+ "Europe/Kirov",
61
+ "Europe/Simferopol",
62
+ // Turkey
63
+ "Europe/Istanbul",
64
+ // Belarus
65
+ "Europe/Minsk",
66
+ // Western Balkans (non-EU)
67
+ "Europe/Belgrade",
68
+ "Europe/Sarajevo",
69
+ "Europe/Skopje",
70
+ "Europe/Tirane",
71
+ "Europe/Podgorica",
72
+ "Europe/Pristina",
73
+ // Moldova
74
+ "Europe/Chisinau",
75
+ // Ukraine
76
+ "Europe/Kyiv",
77
+ "Europe/Uzhgorod",
78
+ "Europe/Zaporozhye"
79
+ ]);
80
+ function isEEAUser() {
81
+ if (typeof window === "undefined") return true;
82
+ try {
83
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
84
+ if (!tz) return true;
85
+ if (!tz.startsWith("Europe/")) return false;
86
+ return !NON_EEA_EUROPE_TIMEZONES.has(tz);
87
+ } catch {
88
+ return true;
89
+ }
90
+ }
49
91
  var COOKIE_CONSENT_EVENT = "cookie_consent_granted";
50
92
  var COOKIE_CONSENT_KEY = "cookie_consent";
51
93
  function CookieConsent({
@@ -59,12 +101,21 @@ function CookieConsent({
59
101
  const [state, setState] = react.useState("idle");
60
102
  react.useEffect(() => {
61
103
  const consent = localStorage.getItem(COOKIE_CONSENT_KEY);
62
- if (!consent) {
63
- const timer = setTimeout(() => setState("open"), delay);
64
- return () => clearTimeout(timer);
65
- } else {
104
+ if (consent) {
105
+ setState("closed");
106
+ return;
107
+ }
108
+ if (!isEEAUser()) {
109
+ try {
110
+ localStorage.setItem(COOKIE_CONSENT_KEY, "granted");
111
+ window.dispatchEvent(new Event(COOKIE_CONSENT_EVENT));
112
+ } catch {
113
+ }
66
114
  setState("closed");
115
+ return;
67
116
  }
117
+ const timer = setTimeout(() => setState("open"), delay);
118
+ return () => clearTimeout(timer);
68
119
  }, [delay]);
69
120
  const handleDismiss = react.useCallback(() => {
70
121
  setState("closing");
@@ -122,17 +173,29 @@ function CookieConsent({
122
173
  );
123
174
  }
124
175
  function useCookieConsent() {
125
- const [status, setStatus] = react.useState(() => {
126
- if (typeof window === "undefined") return null;
127
- const stored = localStorage.getItem(COOKIE_CONSENT_KEY);
128
- if (stored === "granted") return "granted";
129
- if (stored === "denied") return "denied";
130
- return null;
131
- });
176
+ const [status, setStatus] = react.useState(null);
132
177
  react.useEffect(() => {
133
- const handleConsent = () => setStatus("granted");
134
- window.addEventListener(COOKIE_CONSENT_EVENT, handleConsent);
135
- return () => window.removeEventListener(COOKIE_CONSENT_EVENT, handleConsent);
178
+ const stored = localStorage.getItem(COOKIE_CONSENT_KEY);
179
+ if (stored === "granted") {
180
+ setStatus("granted");
181
+ } else if (stored === "denied") {
182
+ setStatus("denied");
183
+ } else if (!isEEAUser()) {
184
+ setStatus("granted");
185
+ }
186
+ const handleGranted = () => setStatus("granted");
187
+ const handleStorage = (e) => {
188
+ if (e.key !== COOKIE_CONSENT_KEY) return;
189
+ if (e.newValue === "granted") setStatus("granted");
190
+ else if (e.newValue === "denied") setStatus("denied");
191
+ else setStatus(isEEAUser() ? null : "granted");
192
+ };
193
+ window.addEventListener(COOKIE_CONSENT_EVENT, handleGranted);
194
+ window.addEventListener("storage", handleStorage);
195
+ return () => {
196
+ window.removeEventListener(COOKIE_CONSENT_EVENT, handleGranted);
197
+ window.removeEventListener("storage", handleStorage);
198
+ };
136
199
  }, []);
137
200
  return status;
138
201
  }
@@ -155,8 +218,9 @@ exports.COOKIE_CONSENT_EVENT = COOKIE_CONSENT_EVENT;
155
218
  exports.COOKIE_CONSENT_KEY = COOKIE_CONSENT_KEY;
156
219
  exports.CookieConsent = CookieConsent;
157
220
  exports.createAnalyticsLoader = createAnalyticsLoader;
221
+ exports.isEEAUser = isEEAUser;
158
222
  exports.loadClarity = loadClarity;
159
223
  exports.loadGoogleAnalytics = loadGoogleAnalytics;
160
224
  exports.useCookieConsent = useCookieConsent;
161
225
  //# sourceMappingURL=out.js.map
162
- //# sourceMappingURL=chunk-2VVRZBUR.cjs.map
226
+ //# sourceMappingURL=chunk-DXFMZ4ME.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/web/analytics/create-analytics-loader.ts","../src/web/analytics/region.ts","../src/web/CookieConsent/CookieConsent.tsx","../src/web/analytics/use-cookie-consent.ts","../src/web/analytics/analytics-provider.tsx"],"names":["useState","useEffect","useCallback","jsx"],"mappings":"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","sourcesContent":["/**\n * Generic analytics script loader.\n *\n * Framework-agnostic: injects a `<script>` tag into `<head>` once,\n * guards against double-loading, and respects cookie consent.\n *\n * @example\n * ```ts\n * import { createAnalyticsLoader } from '@salesmind-ai/design-system/web';\n *\n * const loadGA = createAnalyticsLoader({\n * id: 'ga-script',\n * src: (gaId) => `https://www.googletagmanager.com/gtag/js?id=${gaId}`,\n * onLoad: (gaId) => {\n * window.dataLayer = window.dataLayer || [];\n * function gtag(...args: unknown[]) { window.dataLayer.push(args); }\n * gtag('js', new Date());\n * gtag('config', gaId);\n * },\n * });\n *\n * // Later, after consent:\n * loadGA('G-XXXXXX');\n * ```\n */\n\nexport interface AnalyticsLoaderConfig<TId extends string = string> {\n /** Unique DOM id for the script element (prevents double-loading) */\n id: string;\n /** Build the script src URL from the tracking ID */\n src: (trackingId: TId) => string;\n /** Async attribute on the script tag (default: true) */\n async?: boolean;\n /** Called after the script is appended to the DOM */\n onLoad?: (trackingId: TId) => void;\n /** Custom inline script content instead of an external src */\n inlineScript?: (trackingId: TId) => string;\n}\n\n/**\n * Create a reusable analytics loader function.\n * The returned function injects the script once and is safe to call multiple times.\n */\nexport function createAnalyticsLoader<TId extends string = string>(\n config: AnalyticsLoaderConfig<TId>,\n): (trackingId: TId) => void {\n return (trackingId: TId) => {\n if (typeof document === 'undefined') return;\n if (document.getElementById(config.id)) return;\n\n const script = document.createElement('script');\n script.id = config.id;\n\n if (config.inlineScript) {\n script.textContent = config.inlineScript(trackingId);\n } else {\n script.async = config.async ?? true;\n script.src = config.src(trackingId);\n }\n\n document.head.appendChild(script);\n config.onLoad?.(trackingId);\n };\n}\n\n// ─── Pre-built Loaders ──────────────────────────────────────────────────────\n\ndeclare global {\n interface Window {\n dataLayer: unknown[];\n clarity?: (...args: unknown[]) => void;\n }\n}\n\n/**\n * Load Google Analytics (gtag.js).\n * Call with your GA measurement ID after cookie consent.\n */\nexport const loadGoogleAnalytics = createAnalyticsLoader({\n id: 'ga-script',\n src: (gaId) => `https://www.googletagmanager.com/gtag/js?id=${gaId}`,\n onLoad: (gaId) => {\n window.dataLayer = window.dataLayer || [];\n // Use array push directly to avoid @ts-ignore issues with gtag signature\n window.dataLayer.push(['js', new Date()]);\n window.dataLayer.push(['config', gaId]);\n },\n});\n\n/**\n * Load Microsoft Clarity.\n * Call with your Clarity project ID after cookie consent.\n */\nexport const loadClarity = createAnalyticsLoader({\n id: 'clarity-script',\n src: (clarityId) => `https://www.clarity.ms/tag/${clarityId}`,\n onLoad: () => {\n // Clarity auto-initializes from the script tag.\n // Expose the global `clarity` queue function for custom events.\n if (!window.clarity) {\n const queue: unknown[][] = [];\n window.clarity = (...args: unknown[]) => {\n queue.push(args);\n };\n (window.clarity as unknown as { q: unknown[][] }).q = queue;\n }\n },\n});\n","\"use client\";\n\n/**\n * EEA timezone-based detection for soft-consent flows.\n *\n * Returns `true` if the user's resolved timezone is in the European\n * Economic Area / UK / Switzerland (where GDPR strict consent applies).\n * Returns `false` for everyone else.\n *\n * Server-side and timezone-resolution failures return `true` (safe\n * default — assume GDPR applies).\n *\n * @example\n * ```ts\n * if (!isEEAUser()) {\n * // Auto-grant analytics consent for non-EEA visitors\n * localStorage.setItem('cookie_consent', 'granted');\n * }\n * ```\n */\n\n/**\n * Europe/* timezones that are NOT in the EEA and where GDPR-strict consent\n * rules do not apply. Includes Russia (multi-tz), Turkey, Ukraine, Belarus,\n * Moldova, and the Western Balkans (non-EU).\n */\nconst NON_EEA_EUROPE_TIMEZONES = new Set<string>([\n // Russia\n \"Europe/Moscow\",\n \"Europe/Volgograd\",\n \"Europe/Samara\",\n \"Europe/Saratov\",\n \"Europe/Ulyanovsk\",\n \"Europe/Kaliningrad\",\n \"Europe/Astrakhan\",\n \"Europe/Kirov\",\n \"Europe/Simferopol\",\n // Turkey\n \"Europe/Istanbul\",\n // Belarus\n \"Europe/Minsk\",\n // Western Balkans (non-EU)\n \"Europe/Belgrade\",\n \"Europe/Sarajevo\",\n \"Europe/Skopje\",\n \"Europe/Tirane\",\n \"Europe/Podgorica\",\n \"Europe/Pristina\",\n // Moldova\n \"Europe/Chisinau\",\n // Ukraine\n \"Europe/Kyiv\",\n \"Europe/Uzhgorod\",\n \"Europe/Zaporozhye\",\n]);\n\nexport function isEEAUser(): boolean {\n if (typeof window === \"undefined\") return true;\n try {\n const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;\n if (!tz) return true;\n if (!tz.startsWith(\"Europe/\")) return false;\n return !NON_EEA_EUROPE_TIMEZONES.has(tz);\n } catch {\n return true; // fail safe\n }\n}\n","\"use client\";\nimport { useState, useEffect, useCallback } from 'react';\nimport { Button } from '../../components/Button/Button';\nimport { OutboundLink } from '../../components/OutboundLink/OutboundLink';\nimport { X } from 'lucide-react';\nimport './CookieConsent.css';\nimport { isEEAUser } from '../analytics/region';\n\n/** Cookie consent state */\nexport type ConsentStatus = 'granted' | 'denied' | null;\n\n/** Event name dispatched on window when consent changes */\nexport const COOKIE_CONSENT_EVENT = 'cookie_consent_granted';\n\n/** localStorage key for cookie consent */\nexport const COOKIE_CONSENT_KEY = 'cookie_consent';\n\n/** Labels for i18n support */\nexport interface CookieConsentLabels {\n title?: string;\n description?: string;\n privacyLinkText?: string;\n acceptLabel?: string;\n declineLabel?: string;\n}\n\nexport interface CookieConsentProps {\n /** Delay in ms before showing the banner (default: 1000) */\n delay?: number;\n /** URL to the privacy policy page (default: \"/legal/privacy\") */\n privacyUrl?: string;\n /** Called when the user accepts cookies */\n onAccept?: () => void;\n /** Called when the user declines cookies */\n onDecline?: () => void;\n /** Override default labels for i18n */\n labels?: CookieConsentLabels;\n /** Custom className for the container */\n className?: string;\n}\n\n/**\n * CookieConsent — GDPR-compliant cookie consent banner.\n *\n * - Animated entrance/exit via CSS transitions (no framer-motion)\n * - Respects prior consent stored in localStorage\n * - Dispatches a `cookie_consent_granted` event on the window for analytics loaders\n * - Uses DS Button component\n *\n * @example\n * ```tsx\n * import { CookieConsent } from '@salesmind-ai/design-system/web';\n *\n * <CookieConsent\n * privacyUrl=\"/legal/privacy\"\n * onAccept={() => loadGoogleAnalytics('G-XXXX')}\n * />\n * ```\n */\nexport function CookieConsent({\n delay = 1000,\n privacyUrl = '/legal/privacy',\n onAccept,\n onDecline,\n labels,\n className,\n}: CookieConsentProps) {\n // 'idle' = not yet determined, 'open' = visible, 'closing' = exit animation, 'closed' = hidden\n const [state, setState] = useState<'idle' | 'open' | 'closing' | 'closed'>('idle');\n\n useEffect(() => {\n const consent = localStorage.getItem(COOKIE_CONSENT_KEY);\n if (consent) {\n setState('closed');\n return;\n }\n // Soft consent: non-EEA visitors auto-grant analytics. Persist the\n // implicit choice so cross-tab/cross-page state stays consistent,\n // skip the banner, and notify any subscribed hooks/listeners.\n if (!isEEAUser()) {\n try {\n localStorage.setItem(COOKIE_CONSENT_KEY, 'granted');\n window.dispatchEvent(new Event(COOKIE_CONSENT_EVENT));\n } catch {\n // localStorage may be unavailable (private mode, etc.) — fall through.\n }\n setState('closed');\n return;\n }\n const timer = setTimeout(() => setState('open'), delay);\n return () => clearTimeout(timer);\n }, [delay]);\n\n const handleDismiss = useCallback(() => {\n setState('closing');\n }, []);\n\n const handleAccept = useCallback(() => {\n localStorage.setItem(COOKIE_CONSENT_KEY, 'granted');\n handleDismiss();\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new Event(COOKIE_CONSENT_EVENT));\n }\n onAccept?.();\n }, [onAccept, handleDismiss]);\n\n const handleDecline = useCallback(() => {\n localStorage.setItem(COOKIE_CONSENT_KEY, 'denied');\n handleDismiss();\n onDecline?.();\n }, [onDecline, handleDismiss]);\n\n const handleTransitionEnd = useCallback(() => {\n if (state === 'closing') {\n setState('closed');\n }\n }, [state]);\n\n // Don't render anything until we know consent status, or after fully closed\n if (state === 'idle' || state === 'closed') return null;\n\n return (\n <div\n className={`ds-cookie-consent ${className ?? ''}`}\n data-state={state === 'open' ? 'open' : 'closed'}\n onTransitionEnd={handleTransitionEnd}\n role=\"dialog\"\n aria-label={labels?.title ?? 'Cookie consent'}\n >\n <div className=\"ds-cookie-consent__inner\">\n <div className=\"ds-cookie-consent__content\">\n <h3 className=\"ds-cookie-consent__title\">\n {labels?.title ?? 'We use cookies'}\n </h3>\n <p className=\"ds-cookie-consent__description\">\n {labels?.description ??\n 'We use tracking cookies to understand how you use the product and help us improve it.'}\n <OutboundLink href={privacyUrl} context=\"cookie-consent-privacy\" className=\"ds-cookie-consent__link\">\n {labels?.privacyLinkText ?? 'Privacy Policy'}\n </OutboundLink>\n </p>\n </div>\n <div className=\"ds-cookie-consent__actions\">\n <Button variant=\"outline\" size=\"sm\" onClick={handleDecline}>\n {labels?.declineLabel ?? 'Decline'}\n </Button>\n <Button variant=\"primary\" size=\"sm\" onClick={handleAccept}>\n {labels?.acceptLabel ?? 'Accept'}\n </Button>\n </div>\n <button\n onClick={handleDecline}\n className=\"ds-cookie-consent__close\"\n aria-label=\"Close\"\n >\n <X size={16} />\n </button>\n </div>\n </div>\n );\n}\n","\"use client\";\nimport { useState, useEffect } from 'react';\nimport { COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, type ConsentStatus } from '../CookieConsent/CookieConsent';\nimport { isEEAUser } from './region';\n\n/**\n * React hook that tracks cookie consent status.\n *\n * Returns `'granted'` once the user has granted cookie consent,\n * `'denied'` if denied, `null` if not yet decided.\n *\n * SOFT CONSENT: For non-EEA users (timezone-based detection), the default\n * resolves to `'granted'` after mount so analytics fire without requiring a\n * banner interaction. EEA users retain the strict `null → granted` flow via\n * the banner.\n *\n * Server-side renders as `null` (no window). The mount effect computes the\n * real status; this avoids hydration mismatches.\n *\n * Listens for the `cookie_consent_granted` window event dispatched by the\n * CookieConsent component, plus the cross-tab `storage` event.\n *\n * @example\n * ```tsx\n * const consent = useCookieConsent();\n *\n * useEffect(() => {\n * if (consent === 'granted') loadGoogleAnalytics('G-XXXX');\n * }, [consent]);\n * ```\n */\nexport function useCookieConsent(): ConsentStatus {\n const [status, setStatus] = useState<ConsentStatus>(null);\n\n useEffect(() => {\n // Compute initial status on mount only (avoids SSR mismatch).\n const stored = localStorage.getItem(COOKIE_CONSENT_KEY);\n if (stored === 'granted') {\n setStatus('granted');\n } else if (stored === 'denied') {\n setStatus('denied');\n } else if (!isEEAUser()) {\n // Soft consent: non-EEA users auto-grant on first visit.\n setStatus('granted');\n }\n // else: leave as null until the user interacts with the banner.\n\n const handleGranted = () => setStatus('granted');\n const handleStorage = (e: StorageEvent) => {\n if (e.key !== COOKIE_CONSENT_KEY) return;\n if (e.newValue === 'granted') setStatus('granted');\n else if (e.newValue === 'denied') setStatus('denied');\n else setStatus(isEEAUser() ? null : 'granted');\n };\n\n window.addEventListener(COOKIE_CONSENT_EVENT, handleGranted);\n window.addEventListener('storage', handleStorage);\n return () => {\n window.removeEventListener(COOKIE_CONSENT_EVENT, handleGranted);\n window.removeEventListener('storage', handleStorage);\n };\n }, []);\n\n return status;\n}\n","\"use client\";\nimport { useCallback, useMemo } from 'react';\nimport type { ReactNode } from 'react';\nimport { AnalyticsContext } from './analytics-context';\nimport type { TrackFn } from './analytics-context';\n\n/* ============================================================================\n ANALYTICS PROVIDER — Event Tracking Context\n ============================================================================\n\n Provides a unified event tracking interface for all marketing/conversion\n components. Components call `useAnalytics().track(...)` without knowing\n which analytics backend is active.\n\n When no provider is present, all calls are no-ops — safe for Storybook,\n tests, and SSR.\n\n Usage:\n // In your app root:\n <AnalyticsProvider onTrack={(event, props) => gtag('event', event, props)}>\n <App />\n </AnalyticsProvider>\n\n // In any DS component:\n const { track } = useAnalytics();\n track('cta_click', { location: 'hero', label: 'Book Demo' });\n\n ============================================================================ */\n\n/** Props for the AnalyticsProvider component */\nexport interface AnalyticsProviderProps {\n /** Callback invoked on every track() call. Wire this to your analytics backend. */\n onTrack: TrackFn;\n /** Enable console logging in development (default: false) */\n debug?: boolean;\n children: ReactNode;\n}\n\n/**\n * Provides analytics event tracking to all descendant DS components.\n *\n * @example\n * ```tsx\n * <AnalyticsProvider\n * onTrack={(event, props) => {\n * window.gtag?.('event', event, props);\n * }}\n * >\n * <App />\n * </AnalyticsProvider>\n * ```\n */\nexport function AnalyticsProvider({ onTrack, debug = false, children }: AnalyticsProviderProps) {\n const track: TrackFn = useCallback(\n (event, props) => {\n if (debug && typeof console !== 'undefined') {\n // eslint-disable-next-line no-console\n console.log('[DS Analytics]', event, props);\n }\n onTrack(event, props);\n },\n [onTrack, debug],\n );\n\n const value = useMemo(() => ({ track }), [track]);\n\n return <AnalyticsContext.Provider value={value}>{children}</AnalyticsContext.Provider>;\n}\n"]}
@@ -44,6 +44,48 @@ var loadClarity = createAnalyticsLoader({
44
44
  }
45
45
  }
46
46
  });
47
+
48
+ // src/web/analytics/region.ts
49
+ var NON_EEA_EUROPE_TIMEZONES = /* @__PURE__ */ new Set([
50
+ // Russia
51
+ "Europe/Moscow",
52
+ "Europe/Volgograd",
53
+ "Europe/Samara",
54
+ "Europe/Saratov",
55
+ "Europe/Ulyanovsk",
56
+ "Europe/Kaliningrad",
57
+ "Europe/Astrakhan",
58
+ "Europe/Kirov",
59
+ "Europe/Simferopol",
60
+ // Turkey
61
+ "Europe/Istanbul",
62
+ // Belarus
63
+ "Europe/Minsk",
64
+ // Western Balkans (non-EU)
65
+ "Europe/Belgrade",
66
+ "Europe/Sarajevo",
67
+ "Europe/Skopje",
68
+ "Europe/Tirane",
69
+ "Europe/Podgorica",
70
+ "Europe/Pristina",
71
+ // Moldova
72
+ "Europe/Chisinau",
73
+ // Ukraine
74
+ "Europe/Kyiv",
75
+ "Europe/Uzhgorod",
76
+ "Europe/Zaporozhye"
77
+ ]);
78
+ function isEEAUser() {
79
+ if (typeof window === "undefined") return true;
80
+ try {
81
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
82
+ if (!tz) return true;
83
+ if (!tz.startsWith("Europe/")) return false;
84
+ return !NON_EEA_EUROPE_TIMEZONES.has(tz);
85
+ } catch {
86
+ return true;
87
+ }
88
+ }
47
89
  var COOKIE_CONSENT_EVENT = "cookie_consent_granted";
48
90
  var COOKIE_CONSENT_KEY = "cookie_consent";
49
91
  function CookieConsent({
@@ -57,12 +99,21 @@ function CookieConsent({
57
99
  const [state, setState] = useState("idle");
58
100
  useEffect(() => {
59
101
  const consent = localStorage.getItem(COOKIE_CONSENT_KEY);
60
- if (!consent) {
61
- const timer = setTimeout(() => setState("open"), delay);
62
- return () => clearTimeout(timer);
63
- } else {
102
+ if (consent) {
103
+ setState("closed");
104
+ return;
105
+ }
106
+ if (!isEEAUser()) {
107
+ try {
108
+ localStorage.setItem(COOKIE_CONSENT_KEY, "granted");
109
+ window.dispatchEvent(new Event(COOKIE_CONSENT_EVENT));
110
+ } catch {
111
+ }
64
112
  setState("closed");
113
+ return;
65
114
  }
115
+ const timer = setTimeout(() => setState("open"), delay);
116
+ return () => clearTimeout(timer);
66
117
  }, [delay]);
67
118
  const handleDismiss = useCallback(() => {
68
119
  setState("closing");
@@ -120,17 +171,29 @@ function CookieConsent({
120
171
  );
121
172
  }
122
173
  function useCookieConsent() {
123
- const [status, setStatus] = useState(() => {
124
- if (typeof window === "undefined") return null;
125
- const stored = localStorage.getItem(COOKIE_CONSENT_KEY);
126
- if (stored === "granted") return "granted";
127
- if (stored === "denied") return "denied";
128
- return null;
129
- });
174
+ const [status, setStatus] = useState(null);
130
175
  useEffect(() => {
131
- const handleConsent = () => setStatus("granted");
132
- window.addEventListener(COOKIE_CONSENT_EVENT, handleConsent);
133
- return () => window.removeEventListener(COOKIE_CONSENT_EVENT, handleConsent);
176
+ const stored = localStorage.getItem(COOKIE_CONSENT_KEY);
177
+ if (stored === "granted") {
178
+ setStatus("granted");
179
+ } else if (stored === "denied") {
180
+ setStatus("denied");
181
+ } else if (!isEEAUser()) {
182
+ setStatus("granted");
183
+ }
184
+ const handleGranted = () => setStatus("granted");
185
+ const handleStorage = (e) => {
186
+ if (e.key !== COOKIE_CONSENT_KEY) return;
187
+ if (e.newValue === "granted") setStatus("granted");
188
+ else if (e.newValue === "denied") setStatus("denied");
189
+ else setStatus(isEEAUser() ? null : "granted");
190
+ };
191
+ window.addEventListener(COOKIE_CONSENT_EVENT, handleGranted);
192
+ window.addEventListener("storage", handleStorage);
193
+ return () => {
194
+ window.removeEventListener(COOKIE_CONSENT_EVENT, handleGranted);
195
+ window.removeEventListener("storage", handleStorage);
196
+ };
134
197
  }, []);
135
198
  return status;
136
199
  }
@@ -148,6 +211,6 @@ function AnalyticsProvider({ onTrack, debug = false, children }) {
148
211
  return /* @__PURE__ */ jsx(AnalyticsContext.Provider, { value, children });
149
212
  }
150
213
 
151
- export { AnalyticsProvider, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, CookieConsent, createAnalyticsLoader, loadClarity, loadGoogleAnalytics, useCookieConsent };
214
+ export { AnalyticsProvider, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, CookieConsent, createAnalyticsLoader, isEEAUser, loadClarity, loadGoogleAnalytics, useCookieConsent };
152
215
  //# sourceMappingURL=out.js.map
153
- //# sourceMappingURL=chunk-K526GN7P.js.map
216
+ //# sourceMappingURL=chunk-FZJLTJZS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/web/analytics/create-analytics-loader.ts","../src/web/analytics/region.ts","../src/web/CookieConsent/CookieConsent.tsx","../src/web/analytics/use-cookie-consent.ts","../src/web/analytics/analytics-provider.tsx"],"names":["useState","useEffect","useCallback","jsx"],"mappings":"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","sourcesContent":["/**\n * Generic analytics script loader.\n *\n * Framework-agnostic: injects a `<script>` tag into `<head>` once,\n * guards against double-loading, and respects cookie consent.\n *\n * @example\n * ```ts\n * import { createAnalyticsLoader } from '@salesmind-ai/design-system/web';\n *\n * const loadGA = createAnalyticsLoader({\n * id: 'ga-script',\n * src: (gaId) => `https://www.googletagmanager.com/gtag/js?id=${gaId}`,\n * onLoad: (gaId) => {\n * window.dataLayer = window.dataLayer || [];\n * function gtag(...args: unknown[]) { window.dataLayer.push(args); }\n * gtag('js', new Date());\n * gtag('config', gaId);\n * },\n * });\n *\n * // Later, after consent:\n * loadGA('G-XXXXXX');\n * ```\n */\n\nexport interface AnalyticsLoaderConfig<TId extends string = string> {\n /** Unique DOM id for the script element (prevents double-loading) */\n id: string;\n /** Build the script src URL from the tracking ID */\n src: (trackingId: TId) => string;\n /** Async attribute on the script tag (default: true) */\n async?: boolean;\n /** Called after the script is appended to the DOM */\n onLoad?: (trackingId: TId) => void;\n /** Custom inline script content instead of an external src */\n inlineScript?: (trackingId: TId) => string;\n}\n\n/**\n * Create a reusable analytics loader function.\n * The returned function injects the script once and is safe to call multiple times.\n */\nexport function createAnalyticsLoader<TId extends string = string>(\n config: AnalyticsLoaderConfig<TId>,\n): (trackingId: TId) => void {\n return (trackingId: TId) => {\n if (typeof document === 'undefined') return;\n if (document.getElementById(config.id)) return;\n\n const script = document.createElement('script');\n script.id = config.id;\n\n if (config.inlineScript) {\n script.textContent = config.inlineScript(trackingId);\n } else {\n script.async = config.async ?? true;\n script.src = config.src(trackingId);\n }\n\n document.head.appendChild(script);\n config.onLoad?.(trackingId);\n };\n}\n\n// ─── Pre-built Loaders ──────────────────────────────────────────────────────\n\ndeclare global {\n interface Window {\n dataLayer: unknown[];\n clarity?: (...args: unknown[]) => void;\n }\n}\n\n/**\n * Load Google Analytics (gtag.js).\n * Call with your GA measurement ID after cookie consent.\n */\nexport const loadGoogleAnalytics = createAnalyticsLoader({\n id: 'ga-script',\n src: (gaId) => `https://www.googletagmanager.com/gtag/js?id=${gaId}`,\n onLoad: (gaId) => {\n window.dataLayer = window.dataLayer || [];\n // Use array push directly to avoid @ts-ignore issues with gtag signature\n window.dataLayer.push(['js', new Date()]);\n window.dataLayer.push(['config', gaId]);\n },\n});\n\n/**\n * Load Microsoft Clarity.\n * Call with your Clarity project ID after cookie consent.\n */\nexport const loadClarity = createAnalyticsLoader({\n id: 'clarity-script',\n src: (clarityId) => `https://www.clarity.ms/tag/${clarityId}`,\n onLoad: () => {\n // Clarity auto-initializes from the script tag.\n // Expose the global `clarity` queue function for custom events.\n if (!window.clarity) {\n const queue: unknown[][] = [];\n window.clarity = (...args: unknown[]) => {\n queue.push(args);\n };\n (window.clarity as unknown as { q: unknown[][] }).q = queue;\n }\n },\n});\n","\"use client\";\n\n/**\n * EEA timezone-based detection for soft-consent flows.\n *\n * Returns `true` if the user's resolved timezone is in the European\n * Economic Area / UK / Switzerland (where GDPR strict consent applies).\n * Returns `false` for everyone else.\n *\n * Server-side and timezone-resolution failures return `true` (safe\n * default — assume GDPR applies).\n *\n * @example\n * ```ts\n * if (!isEEAUser()) {\n * // Auto-grant analytics consent for non-EEA visitors\n * localStorage.setItem('cookie_consent', 'granted');\n * }\n * ```\n */\n\n/**\n * Europe/* timezones that are NOT in the EEA and where GDPR-strict consent\n * rules do not apply. Includes Russia (multi-tz), Turkey, Ukraine, Belarus,\n * Moldova, and the Western Balkans (non-EU).\n */\nconst NON_EEA_EUROPE_TIMEZONES = new Set<string>([\n // Russia\n \"Europe/Moscow\",\n \"Europe/Volgograd\",\n \"Europe/Samara\",\n \"Europe/Saratov\",\n \"Europe/Ulyanovsk\",\n \"Europe/Kaliningrad\",\n \"Europe/Astrakhan\",\n \"Europe/Kirov\",\n \"Europe/Simferopol\",\n // Turkey\n \"Europe/Istanbul\",\n // Belarus\n \"Europe/Minsk\",\n // Western Balkans (non-EU)\n \"Europe/Belgrade\",\n \"Europe/Sarajevo\",\n \"Europe/Skopje\",\n \"Europe/Tirane\",\n \"Europe/Podgorica\",\n \"Europe/Pristina\",\n // Moldova\n \"Europe/Chisinau\",\n // Ukraine\n \"Europe/Kyiv\",\n \"Europe/Uzhgorod\",\n \"Europe/Zaporozhye\",\n]);\n\nexport function isEEAUser(): boolean {\n if (typeof window === \"undefined\") return true;\n try {\n const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;\n if (!tz) return true;\n if (!tz.startsWith(\"Europe/\")) return false;\n return !NON_EEA_EUROPE_TIMEZONES.has(tz);\n } catch {\n return true; // fail safe\n }\n}\n","\"use client\";\nimport { useState, useEffect, useCallback } from 'react';\nimport { Button } from '../../components/Button/Button';\nimport { OutboundLink } from '../../components/OutboundLink/OutboundLink';\nimport { X } from 'lucide-react';\nimport './CookieConsent.css';\nimport { isEEAUser } from '../analytics/region';\n\n/** Cookie consent state */\nexport type ConsentStatus = 'granted' | 'denied' | null;\n\n/** Event name dispatched on window when consent changes */\nexport const COOKIE_CONSENT_EVENT = 'cookie_consent_granted';\n\n/** localStorage key for cookie consent */\nexport const COOKIE_CONSENT_KEY = 'cookie_consent';\n\n/** Labels for i18n support */\nexport interface CookieConsentLabels {\n title?: string;\n description?: string;\n privacyLinkText?: string;\n acceptLabel?: string;\n declineLabel?: string;\n}\n\nexport interface CookieConsentProps {\n /** Delay in ms before showing the banner (default: 1000) */\n delay?: number;\n /** URL to the privacy policy page (default: \"/legal/privacy\") */\n privacyUrl?: string;\n /** Called when the user accepts cookies */\n onAccept?: () => void;\n /** Called when the user declines cookies */\n onDecline?: () => void;\n /** Override default labels for i18n */\n labels?: CookieConsentLabels;\n /** Custom className for the container */\n className?: string;\n}\n\n/**\n * CookieConsent — GDPR-compliant cookie consent banner.\n *\n * - Animated entrance/exit via CSS transitions (no framer-motion)\n * - Respects prior consent stored in localStorage\n * - Dispatches a `cookie_consent_granted` event on the window for analytics loaders\n * - Uses DS Button component\n *\n * @example\n * ```tsx\n * import { CookieConsent } from '@salesmind-ai/design-system/web';\n *\n * <CookieConsent\n * privacyUrl=\"/legal/privacy\"\n * onAccept={() => loadGoogleAnalytics('G-XXXX')}\n * />\n * ```\n */\nexport function CookieConsent({\n delay = 1000,\n privacyUrl = '/legal/privacy',\n onAccept,\n onDecline,\n labels,\n className,\n}: CookieConsentProps) {\n // 'idle' = not yet determined, 'open' = visible, 'closing' = exit animation, 'closed' = hidden\n const [state, setState] = useState<'idle' | 'open' | 'closing' | 'closed'>('idle');\n\n useEffect(() => {\n const consent = localStorage.getItem(COOKIE_CONSENT_KEY);\n if (consent) {\n setState('closed');\n return;\n }\n // Soft consent: non-EEA visitors auto-grant analytics. Persist the\n // implicit choice so cross-tab/cross-page state stays consistent,\n // skip the banner, and notify any subscribed hooks/listeners.\n if (!isEEAUser()) {\n try {\n localStorage.setItem(COOKIE_CONSENT_KEY, 'granted');\n window.dispatchEvent(new Event(COOKIE_CONSENT_EVENT));\n } catch {\n // localStorage may be unavailable (private mode, etc.) — fall through.\n }\n setState('closed');\n return;\n }\n const timer = setTimeout(() => setState('open'), delay);\n return () => clearTimeout(timer);\n }, [delay]);\n\n const handleDismiss = useCallback(() => {\n setState('closing');\n }, []);\n\n const handleAccept = useCallback(() => {\n localStorage.setItem(COOKIE_CONSENT_KEY, 'granted');\n handleDismiss();\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new Event(COOKIE_CONSENT_EVENT));\n }\n onAccept?.();\n }, [onAccept, handleDismiss]);\n\n const handleDecline = useCallback(() => {\n localStorage.setItem(COOKIE_CONSENT_KEY, 'denied');\n handleDismiss();\n onDecline?.();\n }, [onDecline, handleDismiss]);\n\n const handleTransitionEnd = useCallback(() => {\n if (state === 'closing') {\n setState('closed');\n }\n }, [state]);\n\n // Don't render anything until we know consent status, or after fully closed\n if (state === 'idle' || state === 'closed') return null;\n\n return (\n <div\n className={`ds-cookie-consent ${className ?? ''}`}\n data-state={state === 'open' ? 'open' : 'closed'}\n onTransitionEnd={handleTransitionEnd}\n role=\"dialog\"\n aria-label={labels?.title ?? 'Cookie consent'}\n >\n <div className=\"ds-cookie-consent__inner\">\n <div className=\"ds-cookie-consent__content\">\n <h3 className=\"ds-cookie-consent__title\">\n {labels?.title ?? 'We use cookies'}\n </h3>\n <p className=\"ds-cookie-consent__description\">\n {labels?.description ??\n 'We use tracking cookies to understand how you use the product and help us improve it.'}\n <OutboundLink href={privacyUrl} context=\"cookie-consent-privacy\" className=\"ds-cookie-consent__link\">\n {labels?.privacyLinkText ?? 'Privacy Policy'}\n </OutboundLink>\n </p>\n </div>\n <div className=\"ds-cookie-consent__actions\">\n <Button variant=\"outline\" size=\"sm\" onClick={handleDecline}>\n {labels?.declineLabel ?? 'Decline'}\n </Button>\n <Button variant=\"primary\" size=\"sm\" onClick={handleAccept}>\n {labels?.acceptLabel ?? 'Accept'}\n </Button>\n </div>\n <button\n onClick={handleDecline}\n className=\"ds-cookie-consent__close\"\n aria-label=\"Close\"\n >\n <X size={16} />\n </button>\n </div>\n </div>\n );\n}\n","\"use client\";\nimport { useState, useEffect } from 'react';\nimport { COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, type ConsentStatus } from '../CookieConsent/CookieConsent';\nimport { isEEAUser } from './region';\n\n/**\n * React hook that tracks cookie consent status.\n *\n * Returns `'granted'` once the user has granted cookie consent,\n * `'denied'` if denied, `null` if not yet decided.\n *\n * SOFT CONSENT: For non-EEA users (timezone-based detection), the default\n * resolves to `'granted'` after mount so analytics fire without requiring a\n * banner interaction. EEA users retain the strict `null → granted` flow via\n * the banner.\n *\n * Server-side renders as `null` (no window). The mount effect computes the\n * real status; this avoids hydration mismatches.\n *\n * Listens for the `cookie_consent_granted` window event dispatched by the\n * CookieConsent component, plus the cross-tab `storage` event.\n *\n * @example\n * ```tsx\n * const consent = useCookieConsent();\n *\n * useEffect(() => {\n * if (consent === 'granted') loadGoogleAnalytics('G-XXXX');\n * }, [consent]);\n * ```\n */\nexport function useCookieConsent(): ConsentStatus {\n const [status, setStatus] = useState<ConsentStatus>(null);\n\n useEffect(() => {\n // Compute initial status on mount only (avoids SSR mismatch).\n const stored = localStorage.getItem(COOKIE_CONSENT_KEY);\n if (stored === 'granted') {\n setStatus('granted');\n } else if (stored === 'denied') {\n setStatus('denied');\n } else if (!isEEAUser()) {\n // Soft consent: non-EEA users auto-grant on first visit.\n setStatus('granted');\n }\n // else: leave as null until the user interacts with the banner.\n\n const handleGranted = () => setStatus('granted');\n const handleStorage = (e: StorageEvent) => {\n if (e.key !== COOKIE_CONSENT_KEY) return;\n if (e.newValue === 'granted') setStatus('granted');\n else if (e.newValue === 'denied') setStatus('denied');\n else setStatus(isEEAUser() ? null : 'granted');\n };\n\n window.addEventListener(COOKIE_CONSENT_EVENT, handleGranted);\n window.addEventListener('storage', handleStorage);\n return () => {\n window.removeEventListener(COOKIE_CONSENT_EVENT, handleGranted);\n window.removeEventListener('storage', handleStorage);\n };\n }, []);\n\n return status;\n}\n","\"use client\";\nimport { useCallback, useMemo } from 'react';\nimport type { ReactNode } from 'react';\nimport { AnalyticsContext } from './analytics-context';\nimport type { TrackFn } from './analytics-context';\n\n/* ============================================================================\n ANALYTICS PROVIDER — Event Tracking Context\n ============================================================================\n\n Provides a unified event tracking interface for all marketing/conversion\n components. Components call `useAnalytics().track(...)` without knowing\n which analytics backend is active.\n\n When no provider is present, all calls are no-ops — safe for Storybook,\n tests, and SSR.\n\n Usage:\n // In your app root:\n <AnalyticsProvider onTrack={(event, props) => gtag('event', event, props)}>\n <App />\n </AnalyticsProvider>\n\n // In any DS component:\n const { track } = useAnalytics();\n track('cta_click', { location: 'hero', label: 'Book Demo' });\n\n ============================================================================ */\n\n/** Props for the AnalyticsProvider component */\nexport interface AnalyticsProviderProps {\n /** Callback invoked on every track() call. Wire this to your analytics backend. */\n onTrack: TrackFn;\n /** Enable console logging in development (default: false) */\n debug?: boolean;\n children: ReactNode;\n}\n\n/**\n * Provides analytics event tracking to all descendant DS components.\n *\n * @example\n * ```tsx\n * <AnalyticsProvider\n * onTrack={(event, props) => {\n * window.gtag?.('event', event, props);\n * }}\n * >\n * <App />\n * </AnalyticsProvider>\n * ```\n */\nexport function AnalyticsProvider({ onTrack, debug = false, children }: AnalyticsProviderProps) {\n const track: TrackFn = useCallback(\n (event, props) => {\n if (debug && typeof console !== 'undefined') {\n // eslint-disable-next-line no-console\n console.log('[DS Analytics]', event, props);\n }\n onTrack(event, props);\n },\n [onTrack, debug],\n );\n\n const value = useMemo(() => ({ track }), [track]);\n\n return <AnalyticsContext.Provider value={value}>{children}</AnalyticsContext.Provider>;\n}\n"]}
@@ -134,7 +134,8 @@ function createSchemaGenerators(brand, supportedLanguages = ["en", "fr", "es"])
134
134
  };
135
135
  },
136
136
  /** SoftwareApplication schema — Platform Hub, Platform Feature, Pricing. */
137
- softwareApplication(name, description, url, locale = "en") {
137
+ softwareApplication(name, description, url, locale = "en", options = {}) {
138
+ const aggregateRating = options.aggregateRating ?? brand.aggregateRating;
138
139
  return {
139
140
  "@context": "https://schema.org",
140
141
  "@type": "SoftwareApplication",
@@ -146,17 +147,15 @@ function createSchemaGenerators(brand, supportedLanguages = ["en", "fr", "es"])
146
147
  operatingSystem: "Web",
147
148
  inLanguage: locale,
148
149
  availableLanguage: [...supportedLanguages],
149
- offers: {
150
- "@type": "AggregateOffer",
151
- priceCurrency: "USD",
152
- lowPrice: "0",
153
- highPrice: "0",
154
- offerCount: "2",
155
- priceSpecification: {
156
- "@type": "PriceSpecification",
157
- description: "Custom pricing \u2014 contact sales for a tailored quote"
150
+ ...aggregateRating ? {
151
+ aggregateRating: {
152
+ "@type": "AggregateRating",
153
+ ratingValue: String(aggregateRating.ratingValue),
154
+ ratingCount: String(aggregateRating.reviewCount),
155
+ bestRating: String(aggregateRating.bestRating ?? 5),
156
+ worstRating: String(aggregateRating.worstRating ?? 1)
158
157
  }
159
- },
158
+ } : {},
160
159
  author: {
161
160
  "@type": "Organization",
162
161
  "@id": ENTITY_IDS.organization,
@@ -570,4 +569,4 @@ exports.canonicalUrl = canonicalUrl;
570
569
  exports.createEntityIds = createEntityIds;
571
570
  exports.createSchemaGenerators = createSchemaGenerators;
572
571
  //# sourceMappingURL=out.js.map
573
- //# sourceMappingURL=chunk-H5BOCR5Z.cjs.map
572
+ //# sourceMappingURL=chunk-K7NQ7TQG.cjs.map