@sonordev/site-kit 7.0.0 → 7.0.2

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 (90) hide show
  1. package/CHANGELOG.md +3539 -0
  2. package/README.md +12 -13
  3. package/agent-manifest.json +1 -1
  4. package/dist/{AnalyticsProvider-GJ6JFQO5.js → AnalyticsProvider-XXWTFKJH.js} +4 -4
  5. package/dist/{ArticleViewTracker-FEA5SBKW.js → ArticleViewTracker-V4KZB6QN.js} +3 -3
  6. package/dist/{BlocksPopup-MNK4SDZM.js → BlocksPopup-JHGHB6XW.js} +4 -4
  7. package/dist/{ChatWidget-M2YNLRYW.js → ChatWidget-CG32POI3.js} +5 -5
  8. package/dist/{EngageWidget-RR63JQCA.js → EngageWidget-LQMR4LEX.js} +4 -4
  9. package/dist/{FileField-JFBXXD43.js → FileField-KUG3CKXG.js} +3 -3
  10. package/dist/{FormSpotlight-QE3J3VB5.js → FormSpotlight-FVNPOCU3.js} +1 -1
  11. package/dist/{FormStage-OTPKEWID.js → FormStage-C7VKRURJ.js} +1 -1
  12. package/dist/{ManagedForm-3VRALWUD.js → ManagedForm-VLNJKV65.js} +6 -6
  13. package/dist/{ManagedNewsletterForm-A2FE6L5C.js → ManagedNewsletterForm-KJEU23BV.js} +4 -4
  14. package/dist/{SignalCore-S6DO3VQV.js → SignalCore-K2O46QG7.js} +3 -3
  15. package/dist/{SiteDesignReporter-2WZVWJMA.js → SiteDesignReporter-D7MD66GI.js} +5 -5
  16. package/dist/SitemapSync-NMXGMPCQ.js +8 -0
  17. package/dist/_client/booking-widget.js +5 -5
  18. package/dist/affiliates/index.js +3 -3
  19. package/dist/analytics/index.js +4 -4
  20. package/dist/articles/index.js +1 -1
  21. package/dist/articles/server-ui.js +1 -1
  22. package/dist/chat/index.js +5 -5
  23. package/dist/{chunk-XZMVNORA.js → chunk-42OXY4JV.js} +1 -1
  24. package/dist/{chunk-5YSI5KFD.js → chunk-56JNI463.js} +1 -1
  25. package/dist/{chunk-M4ZY2FVB.js → chunk-5FBY2ZIH.js} +1 -1
  26. package/dist/{chunk-3XHK5DFX.js → chunk-7JIKGKWD.js} +7 -7
  27. package/dist/{chunk-7ZHJIRM3.js → chunk-B6RZ2NRH.js} +1 -1
  28. package/dist/{chunk-UINSEWQ3.js → chunk-BEL7YFMC.js} +1 -1
  29. package/dist/{chunk-R3TOKUDJ.js → chunk-BS7FWUOY.js} +1 -1
  30. package/dist/{chunk-MZFF5F7R.js → chunk-DKTSGYLM.js} +2 -2
  31. package/dist/{chunk-LT5ITURG.js → chunk-GGD4P7UW.js} +1 -1
  32. package/dist/{chunk-OO2GZ272.js → chunk-GYY6ETGB.js} +1 -1
  33. package/dist/{chunk-I6WUCBKI.js → chunk-K5WZX776.js} +2 -2
  34. package/dist/{chunk-B52EXDWI.js → chunk-LJZ3SUET.js} +2 -2
  35. package/dist/{chunk-OSQHWIM5.js → chunk-O52CH273.js} +1 -1
  36. package/dist/{chunk-HPUXKMNR.js → chunk-OIETJKIL.js} +1 -1
  37. package/dist/{chunk-5UZN5V52.js → chunk-P2GIIQH5.js} +1 -1
  38. package/dist/{chunk-5C4WVOVO.js → chunk-P72ZJRSX.js} +3 -3
  39. package/dist/{chunk-4WI3FU5L.js → chunk-RU2RMTGT.js} +2 -2
  40. package/dist/{chunk-GYBMMWGF.js → chunk-SAUTJMK6.js} +1 -1
  41. package/dist/{chunk-JMRESWQT.js → chunk-SWP36NCB.js} +1 -1
  42. package/dist/{chunk-WFHI6HXP.js → chunk-T4SY3FMN.js} +3 -3
  43. package/dist/{chunk-6ZB3XNAT.js → chunk-ZRE4ZYEG.js} +1 -1
  44. package/dist/{chunk-6CUFRMMF.js → chunk-ZSLRAMCK.js} +1 -1
  45. package/dist/client/index.js +3 -3
  46. package/dist/commerce/index.js +4 -4
  47. package/dist/engage/index.js +6 -6
  48. package/dist/fleet/index.js +4 -4
  49. package/dist/forms/index.js +7 -7
  50. package/dist/forms/server.js +2 -2
  51. package/dist/forms/types.d.ts +3 -1
  52. package/dist/images/index.js +4 -4
  53. package/dist/index.js +1 -1
  54. package/dist/layout/client.js +7 -7
  55. package/dist/layout/index.js +8 -8
  56. package/dist/maps/index.js +3 -3
  57. package/dist/mcp/sonor.js +9 -7
  58. package/dist/seo/client.js +4 -4
  59. package/dist/seo/index.js +4 -4
  60. package/dist/server/index.js +2 -2
  61. package/dist/shared/version.d.ts +1 -1
  62. package/dist/signal/index.js +2 -2
  63. package/dist/sync/index.js +5 -5
  64. package/dist/website/images.js +4 -4
  65. package/dist/website/index.js +5 -5
  66. package/dist/website/popups.js +4 -4
  67. package/docs/MIGRATING-TO-7.md +146 -0
  68. package/docs.json +67 -0
  69. package/package.json +9 -4
  70. package/src/admin-auth/README.md +88 -0
  71. package/src/analytics/README.md +264 -0
  72. package/src/articles/README.md +325 -0
  73. package/src/commerce/README.md +109 -0
  74. package/src/cta-bar/README.md +154 -0
  75. package/src/engage/README.md +241 -0
  76. package/src/forms/README.md +219 -0
  77. package/src/images/README.md +74 -0
  78. package/src/layout/README.md +66 -0
  79. package/src/llms/README.md +723 -0
  80. package/src/mcp/README.md +376 -0
  81. package/src/motion/README.md +372 -0
  82. package/src/og/README.md +304 -0
  83. package/src/proxy/README.md +152 -0
  84. package/src/redirects/README.md +74 -0
  85. package/src/reputation/README.md +64 -0
  86. package/src/seo/README.md +359 -0
  87. package/src/signal/README.md +115 -0
  88. package/src/sitemap/README.md +127 -0
  89. package/src/sync/README.md +115 -0
  90. package/dist/SitemapSync-IKNKPT2G.js +0 -8
@@ -0,0 +1,325 @@
1
+ # Articles — `@sonordev/site-kit/articles`
2
+
3
+ ## Publication routes and artwork (5.4.0)
4
+
5
+ Use one routing object for the stock components and their SEO helpers. Existing
6
+ sites keep `/article/slug` unless they opt in. `basePath` remains supported as a
7
+ metadata alias; `basePath` takes precedence when both are provided.
8
+
9
+ ```tsx
10
+ import { Article, ArticleList } from '@sonordev/site-kit/articles/server-ui'
11
+ import {
12
+ createPublicationRoutes, generateArticleMetadata, generateArticleSchema,
13
+ generateArticleSitemap, generateArticleStaticParams, generateRssFeed,
14
+ getArticle,
15
+ } from '@sonordev/site-kit/articles/server'
16
+ import type { PublicationRoutingOptions } from '@sonordev/site-kit/articles/server'
17
+
18
+ const siteUrl = 'https://example.com'
19
+ const routing = {
20
+ basePath: '/theforge',
21
+ includeCategoryInPath: true,
22
+ categoryPath: (slug: string) => `/theforge?category=${encodeURIComponent(slug)}`,
23
+ } satisfies PublicationRoutingOptions
24
+
25
+ // Cards, related stories and cluster links now use /theforge/category/slug.
26
+ const article = <Article slug="build-first" routing={routing} />
27
+ const archive = <ArticleList routing={routing} />
28
+
29
+ // Use these helpers from their corresponding Next.js route exports.
30
+ const metadata = await generateArticleMetadata('build-first', { siteUrl, ...routing })
31
+ const post = await getArticle('build-first')
32
+ const schema = post ? generateArticleSchema(post, { siteUrl, ...routing }) : null
33
+ // In app/theforge/[category]/[slug]/page.tsx, wrap it to pass the routing:
34
+ // export function generateStaticParams() { return generateArticleStaticParams(routing) }
35
+ const params = await generateArticleStaticParams(routing) // { category, slug }[]
36
+ const sitemap = await generateArticleSitemap(siteUrl, {
37
+ ...routing,
38
+ includeCategories: false, // category filters don't need separate sitemap entries
39
+ includeClusters: false, // enable when your site implements cluster routes
40
+ })
41
+ const rss = await generateRssFeed({ siteUrl, siteName: 'The Forge', ...routing })
42
+ const href = createPublicationRoutes(routing).post({ slug: 'build-first', category: 'guides' })
43
+ ```
44
+
45
+ `routing` also works on `PublicationLayout`, `PublicationSidebar`, `RelatedPosts`, `AuthorPage`,
46
+ `ClusterLandingPage`, and `ClusterNavigation`. To support another article shape,
47
+ provide `postPath: post => '/articles/' + encodeURIComponent(post.slug)`.
48
+ Callbacks return local paths; generated metadata, feeds, and sitemaps separately
49
+ honor a valid `canonical_url`. The same routes apply to Atom and generated
50
+ breadcrumb/cluster schemas. Supplied `schema`/`schema_json` objects stay intact.
51
+
52
+ For custom layouts, `resolveArticleArtwork(post, 'article')` prefers
53
+ `editorial_image`, preserving an empty decorative `editorial_image_alt`.
54
+ `resolveArticleArtwork(post, 'card')` keeps the complete `featured_image`. Older API
55
+ responses fall back to the featured image for both. The stock UI keeps generated
56
+ Sonor cards fully visible, while manually selected photos keep their existing
57
+ layout. Social metadata and RSS enclosures use the featured share card.
58
+
59
+ The stock article renders accessible table scroll regions automatically. Custom
60
+ renderers can call `wrapArticleTables(html)` and include `articleTableCss` within their
61
+ `.sk-article-content` scope. Both exports are available from `article/server` and
62
+ `article/server-ui`. The wrapper preserves the original table and is safe to apply
63
+ twice. It's a layout transform, not a sanitizer; keep your existing content trust
64
+ policy. Scrollbars and keyboard focus use `--sk-*` tokens.
65
+
66
+ ## Import server components from `article/server-ui`
67
+
68
+ `Article`, `ArticleList`, `PublicationLayout`, `PublicationSidebar` and `RelatedPosts` are async
69
+ server components. The `@sonordev/site-kit/articles` barrel is stamped `'use client'`
70
+ at build time, and an async component inside a client module is not something Next
71
+ can render — the route returns a 500.
72
+
73
+ ```tsx
74
+ import { Article, ArticleList } from '@sonordev/site-kit/articles/server-ui' // ✅
75
+ import { Article } from '@sonordev/site-kit/articles' // ❌ 500s
76
+ ```
77
+
78
+ The barrel still re-exports them for backwards compatibility and they will move out
79
+ in the next major. Reach for `@sonordev/site-kit/articles` only for genuine client
80
+ components: `TableOfContents`, `ArticleFAQ`, `AuthorCard`, `ServiceCallout`,
81
+ `NewsletterWidget`.
82
+
83
+ `NewsletterWidget` needs either an `onSubmit` callback or a `formSlug`
84
+ pointing at a managed Sonor form (newsletter routing) — with neither it
85
+ renders nothing rather than a form that discards emails. `Article` mounts a
86
+ childless `ArticleViewTracker` client island that counts real readers (one POST
87
+ per post per session, deferred to idle); it needs `SiteKitLayout`'s globals
88
+ and silently no-ops without them.
89
+
90
+ Data helpers live in `@sonordev/site-kit/articles/server`, which is `server-only` — a
91
+ client import there is a build error rather than a runtime one.
92
+
93
+ Sonor-managed article with SSG, topic clusters, E-E-A-T author profiles, and full SEO integration. Create posts in the Sonor dashboard — they appear on your site automatically.
94
+
95
+ ## Quick Start
96
+
97
+ ### Articles Index
98
+
99
+ ```tsx
100
+ // app/article/page.tsx
101
+ import { ArticleList, PublicationLayout } from '@sonordev/site-kit/articles/server-ui'
102
+ import { generatePublicationMetadata } from '@sonordev/site-kit/articles/server'
103
+
104
+ export async function generateMetadata() {
105
+ return generatePublicationMetadata({ siteName: 'My Site', siteUrl: 'https://example.com' })
106
+ }
107
+
108
+ export default function PublicationPage() {
109
+ return (
110
+ <PublicationLayout hero={{ title: 'The Forge', subtitle: 'Latest articles' }}>
111
+ <ArticleList showCategoryFilter showPagination />
112
+ </PublicationLayout>
113
+ )
114
+ }
115
+ ```
116
+
117
+ ### Single Post
118
+
119
+ ```tsx
120
+ // app/article/[slug]/page.tsx
121
+ import { Article } from '@sonordev/site-kit/articles/server-ui'
122
+ import {
123
+ generateArticleStaticParams,
124
+ generateArticleMetadata,
125
+ requireArticle,
126
+ } from '@sonordev/site-kit/articles/server'
127
+
128
+ export const generateStaticParams = generateArticleStaticParams
129
+
130
+ type Props = { params: Promise<{ slug: string }> }
131
+
132
+ export async function generateMetadata({ params }: Props) {
133
+ const { slug } = await params
134
+ await requireArticle(slug) // 404 for an unknown slug, before anything streams
135
+ return generateArticleMetadata(slug, { siteName: 'My Site', siteUrl: 'https://example.com' })
136
+ }
137
+
138
+ export default async function Post({ params }: Props) {
139
+ const { slug } = await params
140
+ return <Article slug={slug} showRelated showToc showAuthor />
141
+ }
142
+ ```
143
+
144
+ **A missing post is a 404.** `requireArticle`, `generateArticleMetadata` and
145
+ `Article` all call Next's `notFound()` when the post doesn't exist, so a
146
+ mistyped or deleted post URL answers 404 with `noindex`. They used to render a
147
+ "Post Not Found" page on a 200 with an indexable title, which is a soft 404.
148
+ Calling `requireArticle` from `generateMetadata` is what guarantees the status:
149
+ metadata resolves before the page streams. To render your own missing-post
150
+ state instead, pass `notFound: false` to `generateArticleMetadata` (its
151
+ placeholder is marked `noindex`) and `notFound={false}` to `Article`.
152
+
153
+ **A post with its own social card.** If the post route has an
154
+ `opengraph-image.tsx` beside the page (see `@sonordev/site-kit/og/route`), pass
155
+ `images: false`:
156
+
157
+ ```ts
158
+ return generateArticleMetadata(slug, { siteName: 'My Site', images: false })
159
+ ```
160
+
161
+ Otherwise the featured image is declared as `openGraph.images`, and Next lets
162
+ declared images beat the file convention: the card never ships. `images: false`
163
+ leaves the keys out entirely (Next checks `hasOwnProperty('images')`, so even
164
+ `images: undefined` would hide the card). `sonor-setup doctor` flags a post
165
+ route that has a card but still declares the image.
166
+
167
+ ### Topic Cluster Landing
168
+
169
+ ```tsx
170
+ // app/article/topics/[slug]/page.tsx
171
+ // From `article/server-ui`, not `article` — it's an async server component that fetches
172
+ // the cluster, so it must stay out of the client-side `article` barrel.
173
+ import { ClusterLandingPage } from '@sonordev/site-kit/articles/server-ui'
174
+
175
+ export default function ClusterPage({ params }: { params: { slug: string } }) {
176
+ return <ClusterLandingPage slug={params.slug} basePath="/article" />
177
+ }
178
+ ```
179
+
180
+ ## Components
181
+
182
+ | Component | Import | Purpose |
183
+ |-----------|--------|---------|
184
+ | `Article` | `article/server-ui` | Single post with content, TOC, author, related |
185
+ | `ArticleList` | `article/server-ui` | Post grid with pagination and category filter |
186
+ | `PublicationLayout` | `article/server-ui` | Full layout with optional sidebar |
187
+ | `PublicationSidebar` | `article/server-ui` | Categories, recent posts, tags |
188
+ | `RelatedPosts` | `article/server-ui` | Related articles widget |
189
+ | `PublicationPage` | `article/server-ui` | Drop-in publication index page (layout + list) |
190
+ | `ArticlePage` | `article/server-ui` | Drop-in single-post page |
191
+ | `CategoryPage` | `article/server-ui` | Drop-in category archive page |
192
+ | `ClusterLandingPage` | `article/server-ui` | Topic cluster overview with pillar + support articles |
193
+ | `ClusterNavigation` | `article` | Breadcrumb-style cluster nav |
194
+ | `AuthorCard` | `article` | Author profile with E-E-A-T fields |
195
+ | `TableOfContents` | `article` | Auto-generated from H2-H4 headings |
196
+ | `ArticleFAQ` | `article` | FAQ section with schema |
197
+ | `ServiceCallout` | `article` | CTA callout for related services |
198
+ | `NewsletterWidget` | `article` | Email capture; needs `onSubmit` or `formSlug` |
199
+
200
+ ## Server Functions (`article/server`)
201
+
202
+ ```ts
203
+ // Data fetching
204
+ getArticle(slug) // Single post with full data, or null
205
+ requireArticle(slug) // Same, but calls notFound() when there's no post
206
+ getAllArticleSlugs() // All published slugs (for generateStaticParams)
207
+ getArticleCategories() // Categories with post counts
208
+ getTopicCluster(slug) // Cluster with pillar + support articles
209
+ getTopicClusters() // All clusters
210
+
211
+ // Next.js integration
212
+ generateArticleStaticParams(routing?) // [{ slug }], or [{ category, slug }] with includeCategoryInPath
213
+ generateCategoryStaticParams() // Returns [{ category }]
214
+ generateAuthorStaticParams() // Returns [{ slug }]
215
+ // With no routing options, all three can be exported directly as
216
+ // `generateStaticParams`; the props Next passes are ignored.
217
+ generateArticleMetadata(slug, opts) // Next.js Metadata object; notFound() for a missing post.
218
+ // opts.images: false when the route has its own opengraph-image card
219
+ generatePublicationMetadata(opts) // Index page metadata
220
+ generateArticleCategoryMetadata(name, opts)
221
+
222
+ // Schema & SEO
223
+ generateArticleSchema(post, opts) // JSON-LD Article with FAQ
224
+ generateArticleListSchema() // JSON-LD for publication index
225
+ generateFaqSchema(items) // FAQ Page schema
226
+ generateArticleSitemap(siteUrl) // Sitemap entries for article
227
+
228
+ // Validation
229
+ validateArticleSeo(post) // Returns field-by-field SEO audit
230
+ validateSeoTitle(title, keyphrase?) // Title length + keyword checks
231
+ validateMetaDescription(desc) // Description length check
232
+ ```
233
+
234
+ ## Multi-site projects
235
+
236
+ One Sonor project can serve many domains (example.com plus its city
237
+ microsites), each with its own article. A post with no site is project-wide and
238
+ shows on every host; a post tagged `charlotte.example.com` shows only there.
239
+
240
+ Every article read sends the site host automatically, as `?site=` on GETs and a
241
+ `site` field on the related-posts and view-count POSTs. The host resolves from
242
+ `NEXT_PUBLIC_SITE_URL`, which every microsite already sets, so most sites
243
+ change nothing. To pin a host explicitly, pass `site`:
244
+
245
+ ```tsx
246
+ <ArticleList site="charlotte.example.com" /> // also Article, PublicationSidebar, PublicationLayout, RelatedPosts, ClusterLandingPage
247
+ await getArticle(slug, { site: 'charlotte.example.com' })
248
+ await getAllArticles({ site: 'charlotte.example.com' })
249
+ ```
250
+
251
+ `getAllArticleSlugs()` and `getAllAuthorSlugs()` take no arguments, so they can
252
+ still be exported as `generateStaticParams`. They always use
253
+ `NEXT_PUBLIC_SITE_URL`. Single-site projects and older API servers ignore
254
+ `site`.
255
+
256
+ ## Article Props
257
+
258
+ ```ts
259
+ interface ArticleProps {
260
+ slug: string
261
+ showRelated?: boolean // Related posts section
262
+ showToc?: boolean // Table of contents
263
+ showAuthor?: boolean // Author card
264
+ unstyled?: boolean // Skip default styles
265
+ notFound?: boolean // Default true: notFound() when the post is missing. false renders a message.
266
+ className?: string
267
+ children?: (props: { post, toc, related }) => ReactNode // Render prop
268
+ }
269
+ ```
270
+
271
+ ## ArticleList Props
272
+
273
+ ```ts
274
+ interface ArticleListProps {
275
+ category?: string // Filter by category slug
276
+ tag?: string // Filter by tag
277
+ author?: string // Filter by author
278
+ featured?: boolean // Featured posts only
279
+ search?: string // Search posts
280
+ page?: number // Default: 1
281
+ perPage?: number // Default: 12
282
+ orderBy?: 'published_at' | 'title' | 'view_count'
283
+ order?: 'asc' | 'desc'
284
+ showCategoryFilter?: boolean
285
+ showPagination?: boolean
286
+ unstyled?: boolean
287
+ className?: string
288
+ children?: (props: { posts, pagination, categories }) => ReactNode
289
+ }
290
+ ```
291
+
292
+ ## Key Types
293
+
294
+ ```ts
295
+ interface Article {
296
+ slug: string; title: string; excerpt?: string; content: string;
297
+ featured_image?: string; author?: ArticleAuthor; category?: ArticleCategory;
298
+ tags?: string[]; meta_title?: string; meta_description?: string;
299
+ faq_items?: { question: string; answer: string }[];
300
+ article_type?: 'pillar' | 'support' | 'comparison' | 'faq' | 'glossary' | 'checklist';
301
+ cluster_slug?: string; reading_time?: number; // the API column ('X min read')
302
+ published_at?: string; status: 'draft' | 'published' | 'scheduled' | 'archived';
303
+ }
304
+
305
+ interface ArticleAuthor {
306
+ name: string; slug: string; bio?: string; avatar_url?: string;
307
+ title?: string; credentials?: string[]; expertise_areas?: string[];
308
+ years_experience?: number; is_subject_matter_expert?: boolean;
309
+ }
310
+
311
+ interface TopicCluster {
312
+ cluster_name: string; cluster_slug: string; core_topic: string;
313
+ geo_target?: string; target_service_page?: string; article_count: number;
314
+ pillar: Article | null; supports: Article[];
315
+ }
316
+ ```
317
+
318
+ ## Styling
319
+
320
+ Components use `.sk-article-*` and `.sk-article-list-*` classes. Import default styles:
321
+
322
+ ```tsx
323
+ ```
324
+
325
+ Or use `unstyled` prop + `children` render prop for complete control.
@@ -0,0 +1,109 @@
1
+ # Commerce — `@sonordev/site-kit/commerce`
2
+
3
+ Products, services, classes, events, and checkout flows — all managed from the Sonor dashboard.
4
+
5
+ ## Components
6
+
7
+ | Component | Purpose |
8
+ |-----------|---------|
9
+ | `OfferingCard` | Card display for any offering type |
10
+ | `OfferingList` | Grid/list of offerings with filtering |
11
+ | `ProductPage` / `ProductDetail` | Full product page with gallery, sizes, variants |
12
+ | `ProductGrid` / `ProductEmbed` | Product showcase widgets |
13
+ | `SizeChart` | Clothing size chart display |
14
+ | `EventTile` / `UpcomingEvents` | Event display widgets |
15
+ | `EventCalendar` / `EventModal` | Calendar view + detail modal |
16
+ | `EventEmbed` / `EventsWidget` | Embeddable event components |
17
+ | `CheckoutForm` | Payment checkout flow |
18
+ | `RegistrationForm` | Event/class registration |
19
+ | `CalendarView` | Date-based calendar component |
20
+
21
+ ## Usage
22
+
23
+ ```tsx
24
+ import { OfferingList, ProductPage } from '@sonordev/site-kit/commerce'
25
+
26
+ // List all offerings
27
+ export default function ShopPage() {
28
+ return <OfferingList type="product" />
29
+ }
30
+
31
+ // Single product page
32
+ export default function Product({ params }) {
33
+ return <ProductPage slug={params.slug} />
34
+ }
35
+ ```
36
+
37
+ ## API Functions
38
+
39
+ ```ts
40
+ import {
41
+ fetchOfferings, fetchOffering,
42
+ fetchProducts, fetchProductBySlug,
43
+ fetchUpcomingEvents, fetchNextEvent,
44
+ fetchCategories, fetchServices,
45
+ createCheckoutSession, createPaymentIntent,
46
+ validateDiscountCode, registerForEvent,
47
+ fetchShippingRates, validateAddress,
48
+ } from '@sonordev/site-kit/commerce'
49
+ ```
50
+
51
+ ## Offering Types
52
+
53
+ ```ts
54
+ type OfferingType = 'product' | 'service' | 'class' | 'event' | 'subscription'
55
+
56
+ interface CommerceOffering {
57
+ name: string; slug: string; type: OfferingType;
58
+ description?: string; featured_image_url?: string;
59
+ price_type: 'fixed' | 'variable' | 'quote' | 'free';
60
+ price?: number; compare_at_price?: number; currency: string;
61
+ track_inventory?: boolean; inventory_count?: number;
62
+ is_clothing?: boolean; size_chart?: SizeChart;
63
+ duration_minutes?: number; capacity?: number;
64
+ location?: string; is_virtual?: boolean;
65
+ schedules?: CommerceSchedule[]; variants?: CommerceVariant[];
66
+ }
67
+ ```
68
+
69
+ ## Per-category styling (`data-category` / `data-offering-type`)
70
+
71
+ Event and offering surfaces expose the offering's category slug and type as
72
+ data attributes, so sites can theme categories with plain CSS — no custom
73
+ components needed. `data-category` is only present when the offering has a
74
+ category; `data-offering-type` is always present.
75
+
76
+ Elements carrying the attributes:
77
+
78
+ - `CalendarView` / `EventCalendar` — `.site-kit-calendar-event` chips (both
79
+ `title` and `image` display modes) and the image-mode wrapper
80
+ - `EventTile` (both variants — also covers `UpcomingEvents` and `EventEmbed`)
81
+ - `EventsWidget` — `.site-kit-event-card` in list and grid views
82
+ - `OfferingCard` (all variants — also covers `OfferingList` / `ProductGrid`)
83
+
84
+ ```css
85
+ /* e.g. two color schemes on one calendar: PTO vs school district */
86
+ .site-kit-calendar-event[data-category="pto"] {
87
+ background: rgba(16, 185, 129, 0.15);
88
+ color: #059669;
89
+ }
90
+ .site-kit-calendar-event[data-category="school-district"] {
91
+ background: rgba(59, 130, 246, 0.15);
92
+ color: #2563eb;
93
+ }
94
+ ```
95
+
96
+ ## Size Charts (clothing products)
97
+
98
+ ```ts
99
+ interface SizeChart {
100
+ unit: 'inches' | 'cm'
101
+ fit_note?: string // e.g., "Runs small. Order one size up."
102
+ measurements: string[] // ['Chest', 'Length', 'Sleeve']
103
+ rows: Array<{
104
+ size: string // 'S', 'M', 'L', 'XL'
105
+ values: number[] // Primary unit values
106
+ values_alt?: number[] // Auto-converted alternate unit
107
+ }>
108
+ }
109
+ ```
@@ -0,0 +1,154 @@
1
+ # @sonordev/site-kit/cta-bar
2
+
3
+ The Liquid Glass mobile CTA bar (6.1.0). A floating frosted capsule that
4
+ keeps a site's one or two highest-intent actions a thumb away on phones.
5
+
6
+ It replaces every hand-rolled sticky mobile bar in the fleet. Those eleven
7
+ copies had each solved part of the same problem set; this one does all of it:
8
+
9
+ | Behaviour | Before | Here |
10
+ |---|---|---|
11
+ | Hide while the form it points at is on screen | two sites, hand-built | `hideOver` |
12
+ | Stay off the hero until the hero CTA scrolls away | one site | `showAfter` |
13
+ | Get out from under the Echo launcher | two sites (`body:has`, a 72px dead corner) | automatic |
14
+ | Ride out the iOS toolbar collapsing | one site (a GSAP tween) | `--sk-vv-layout-gap` |
15
+ | Get out of the way of the keyboard | nobody | `hideWhileTyping` |
16
+ | Tell Sonor which action converts | one site (hand-wired) | `cta_click` event |
17
+
18
+ ## Use it
19
+
20
+ ```tsx
21
+ // app/layout.tsx (a server component)
22
+ import Link from 'next/link'
23
+ import { Phone, ClipboardCheck } from 'lucide-react'
24
+ import { CtaBar, CtaBarAction } from '@sonordev/site-kit/cta-bar'
25
+
26
+ <SiteKitLayout>
27
+ <Header />
28
+ <main>{children}</main>
29
+ <Footer />
30
+ <CtaBar label="Call or request a quote" hideOver="#quote">
31
+ <CtaBarAction href="tel:+15135550100" variant="secondary" icon={<Phone />}>
32
+ Call now
33
+ </CtaBarAction>
34
+ <CtaBarAction as={Link} href="/quote" icon={<ClipboardCheck />}>
35
+ Free quote
36
+ </CtaBarAction>
37
+ </CtaBar>
38
+ </SiteKitLayout>
39
+ ```
40
+
41
+ Rules:
42
+
43
+ - **Render it once per page:** at the layout root for a site-wide bar, or
44
+ inside the page for a page-specific one. It's a labelled `<aside>`, valid
45
+ at either depth; the kit's axe gate covers both placements.
46
+ - **Never inside a blurred header.** `backdrop-filter` on an ancestor becomes
47
+ the containing block for this fixed bar and clips it.
48
+ - **Delete the site's own bar and its compensating `padding-bottom`.** The
49
+ kit renders a spacer that reserves the bar's height at the end of the page.
50
+ - **Better: pad the footer instead of adding a strip after it.** A spacer
51
+ after a dark footer is a blank band in the page colour. Pass
52
+ `spacer={false}` and let the footer's own background run under the bar:
53
+
54
+ ```css
55
+ footer { padding-bottom: calc(2rem + var(--sk-cta-bar-space, 0px)); }
56
+ ```
57
+
58
+ `--sk-cta-bar-space` is set on `<html>` only while a bar is present and
59
+ below its breakpoint, so desktop and bar-less pages get `0px`.
60
+ - **Delete any `--sk-echo-offset-bottom` rule written for the old bar.** The
61
+ kit sets it while the bar is on screen.
62
+
63
+ `CtaBar` and `CtaBarAction` are plain components with no hooks, so a server
64
+ layout can pass `as={Link}` and Link children without crossing a client
65
+ boundary. The only client code is a childless behaviour island the bar
66
+ mounts itself (about 4.4 KB gzipped for the whole module, glass and
67
+ analytics included).
68
+
69
+ ## `<CtaBar>`
70
+
71
+ | Prop | Default | |
72
+ |---|---|---|
73
+ | `label` | `"Quick actions"` | Accessible name of the landmark. |
74
+ | `breakpoint` | `"lg"` | Hidden at this width and up: `sm` 640, `md` 768, `lg` 1024, `xl` 1280, `none` = every width. |
75
+ | `layout` | `"fill"` | `fill` stretches actions across the capsule. `fit` hugs a single action in a centred capsule (the old floating "Request a quote" button). |
76
+ | `showAfter` | | Selector. Hidden until that element scrolls off the top (the hero CTA). Server-rendered hidden, so it never slides in over the hero during hydration. No match = shown. |
77
+ | `hideOver` | | Selector or selectors. Steps aside while any match is on screen: the form it points at, the footer. |
78
+ | `hideWhileTyping` | `true` | Hidden while a text field has focus, so it never sits on the keyboard. |
79
+ | `compactOnScroll` | `true` | Tightens while scrolling down; an icon-bearing secondary action folds to its icon. Scrolling up restores it. |
80
+ | `echoClearance` | `true` | Lifts the Echo launcher above the bar while the bar is on screen, below the breakpoint only. |
81
+ | `spacer` | `true` | Reserves the bar's height at the end of the page. |
82
+ | `track` | `true` | Sends `cta_click` (`category: engagement`, `label`, `properties.href`, `properties.variant`, `properties.location = "cta_bar"`) through the standalone analytics dispatch. |
83
+
84
+ Client navigation re-finds `showAfter` and `hideOver` targets on each route.
85
+
86
+ ## `<CtaBarAction>`
87
+
88
+ | Prop | Default | |
89
+ |---|---|---|
90
+ | `as` | `a` with an `href`, else `button` | Any element or component, e.g. Next's `Link`. Polymorphic since 6.1.3: the action then takes that component's own props (required ones included), so `<CtaBarAction as={ScheduleTourButton} values={unit}>` type-checks and a missing required prop doesn't. |
91
+ | `variant` | `"primary"` | `primary`: solid brand. `secondary`: tinted glass. `plain`: layout only, bring your own classes. |
92
+ | `icon` | | Leading icon, hidden from assistive tech. Lucide icons are sized to 18px. |
93
+ | `collapse` | secondary + icon | Fold to icon-only while compact. The label stays in the accessible name. |
94
+
95
+ Everything else (`href`, `onClick`, `target`, `aria-*`, `data-*`) passes
96
+ through. A `button` without an explicit `type` gets `type="button"`.
97
+
98
+ ## Theme it
99
+
100
+ Colours and geometry are custom properties. Set them on `:root` so the bar,
101
+ its spacer and the Echo clearance all read the same values.
102
+
103
+ ```css
104
+ :root {
105
+ --sk-cta-primary-bg: var(--brand-primary); /* default: --sk-primary, then #2563eb */
106
+ --sk-cta-primary-text: #fff;
107
+ --sk-cta-bar-text: var(--ink-900); /* secondary label colour */
108
+ }
109
+ ```
110
+
111
+ **A dark bar: scope the tint to the bar, not `:root`.** The Echo window
112
+ reads the same `--sk-glass-tint`, and its text follows `--sk-text-primary`
113
+ (dark by default). A dark tint on `:root` puts that dark text on dark glass.
114
+ Either scope the bar's colours to the bar:
115
+
116
+ ```css
117
+ .sk-cta-bar {
118
+ --sk-glass-tint: #0b0a09;
119
+ --sk-cta-bar-text: #fff;
120
+ --sk-cta-primary-bg: #fff;
121
+ --sk-cta-primary-text: #0b0a09;
122
+ }
123
+ ```
124
+
125
+ or theme the whole kit dark with `--sk-bg` and `--sk-text-primary` on
126
+ `:root`, which both surfaces (and site-kit forms) read.
127
+
128
+ | Token | Default |
129
+ |---|---|
130
+ | `--sk-cta-bar-inset` | `12px` from the screen edges |
131
+ | `--sk-cta-bar-padding` | `6px` between glass and buttons |
132
+ | `--sk-cta-bar-action-height` | `48px` (44px tap target minimum; don't go lower) |
133
+ | `--sk-cta-bar-max-width` | `520px` |
134
+ | `--sk-cta-bar-z` | `40` |
135
+ | `--sk-cta-bar-text` | `--sk-text-primary`, then `#111827` |
136
+ | `--sk-cta-primary-bg` / `--sk-cta-primary-text` | `--sk-primary` / `#fff` |
137
+ | `--sk-cta-secondary-bg` / `--sk-cta-secondary-text` | 8% of the text colour / the text colour |
138
+ | `--sk-glass-*` | the shared glass recipe (`--sk-glass-tint`, `--sk-glass-opacity`, `--sk-glass-blur`, `--sk-glass-saturate`) |
139
+
140
+ ## The glass
141
+
142
+ The surface is the kit's one Liquid Glass recipe (`src/shared/glass.tsx`),
143
+ the same material as the Echo launcher and chat window, so the bar and the
144
+ chat always match. The tint is mostly opaque (72%): the tint carries the
145
+ contrast, and the blur only softens what shows through. Browsers without
146
+ `backdrop-filter`, and visitors who ask their OS for reduced transparency
147
+ or more contrast, get the same surface solid. Reduced motion turns the
148
+ transitions off. JS-off visitors get the bar, visible, through
149
+ `@media (scripting: none)`.
150
+
151
+ On phones where the layout viewport runs taller than the visible one
152
+ (in-app browsers, toolbar animations), mount `VisualViewportGap` from
153
+ `@sonordev/site-kit/client`; the bar already adds `--sk-vv-layout-gap` to
154
+ its `bottom`.