@soloworks/smking-next 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # @soloworks/smking-next
2
2
 
3
+ ## 0.14.1 — 2026-05-23
4
+
5
+ **Carousel polish in canonical CSS — nav buttons, dots indicator, image-frame, caption-row + download icon now ship to customer Mode A pages.**
6
+
7
+ ### Added
8
+
9
+ - `packages/shared/src/styles/cms-styles.css` (regenerated into `SMKING_CMS_STYLES` const) now carries the full Apple-newsroom-style carousel CSS: container-query-anchored nav buttons (vertical centre tracks the image height across all 5 aspect ratios), per-slide `image-frame` with placeholder state, caption-row with download icon affordance, dots indicator with active state. Same rules the smking dashboard preview already used — bringing customer parity.
10
+ - Carousel works identically regardless of whether the Web Component IIFE has upgraded the markup: CSS positions everything relative to the `<smking-carousel>` container query, so static HTML renders just as well as the JS-upgraded interactive variant.
11
+
12
+ ### Pairs with
13
+
14
+ - `smking/laravel` v0.15.1 — same canonical CSS payload (identical sha256), Laravel `<x-smking-cms />` Mode A render picks up the polish automatically on view-cache clear.
15
+
16
+ ### Customer
17
+
18
+ ```bash
19
+ pnpm update @soloworks/smking-next # → 0.14.1
20
+ ```
21
+
22
+ No code change required. Existing `<SmkingCms>` Mode A renders get nav / dots / image-frame styling on the next page load.
23
+
24
+ ## 0.14.0 — 2026-05-23
25
+
26
+ **Single-source CSS distribution + theme mode toggle.** `<SmkingCms>` now ships its own polished default styles by default — no customer CSS work needed. Customers who already use Tailwind + `@tailwindcss/typography` can flip to a prose-utility markup variant via the `mode` prop or `SMKING_CMS_THEME` env var.
27
+
28
+ ### Added
29
+
30
+ - **Mode A (`css`, default)** — `<SmkingCms>` emits a `<style data-smking="cms-styles">` block once per render carrying the canonical SDK stylesheet. Class hooks `.smk-cms` / `.smk-block--*` / `.smk-hero` / `.smk-nav` / `.smk-carousel`. Customer themes via CSS variables on `:root` (`--smk-accent`, `--smk-font-serif`, etc).
31
+ - **Mode B (`tailwind-prose`)** — opt-in via `<SmkingCms mode="tailwind-prose" ... />` or `SMKING_CMS_THEME=tailwind-prose` env var. Wraps the article in `<article class="prose dark:prose-invert">` and emits Tailwind utility classes on every block (`not-prose` resets around hero/nav/carousel/search). Requires the customer's `@tailwindcss/typography` plugin and a tailwind safelist (the `@soloworks/smking-wizard` 0.4+ installs both automatically).
32
+ - **`CmsThemeMode` type export** — discriminated string literal `'css' | 'tailwind-prose'` for typed prop usage.
33
+ - **`SMKING_CMS_STYLES` constant** at `@soloworks/smking-next/lib/smking-cms-styles` — auto-generated from `packages/shared/src/styles/cms-styles.css` via the monorepo `pnpm sync:cms-styles` script. Customers who want to ship the SDK stylesheet as a standalone CSS file (CDN / static asset) can copy this constant verbatim.
34
+
35
+ ### Changed
36
+
37
+ - `SmkingCms` props now accept `mode?: CmsThemeMode` alongside the existing `CmsParams`. No breaking change — omitted prop defaults to `'css'` (legacy behavior plus the new inline style block).
38
+
39
+ ### Pairs with
40
+
41
+ - `smking/laravel` v0.15.0 — identical theme-mode toggle on the Laravel side. Both SDKs ship byte-identical canonical CSS in Mode A; Mode B markup also mirrors across both runtimes.
42
+ - `@soloworks/smking-wizard` v0.4.0 — install-time Tailwind detect + theme-mode-select TUI step. When Tailwind is detected the wizard offers Mode B and installs `@tailwindcss/typography` + safelist patches automatically.
43
+
3
44
  ## 0.13.0 — 2026-05-22
4
45
 
5
46
  **Dynamic CMS blocks — `nav-recent-posts`, `nav-taxonomy-list`, `nav-search` now render with real publish-time data.**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soloworks/smking-next",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
4
4
  "description": "AI-native SEO (AEO) for Next.js — auto-inject JSON-LD, FAQ, AI summary, and SEO metadata so AI crawlers (ChatGPT, Perplexity, Google AI) can cite your pages.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/sillyleo/smking/tree/main/packages/smking-next",
@@ -1,13 +1,31 @@
1
1
  import { getCmsPage } from "../lib/cms-client";
2
+ import { SMKING_CMS_STYLES } from "../lib/smking-cms-styles";
2
3
  import { SMKING_ELEMENTS_IIFE } from "../lib/smking-elements-iife";
3
4
  import type {
4
5
  Block,
5
6
  CmsParams,
7
+ CmsThemeMode,
6
8
  ModuleHeader,
7
9
  NavSearchIndexEntry,
8
10
  NavSnapshotEntry,
9
11
  } from "../types";
10
12
 
13
+ /**
14
+ * Pick the active theme mode for this render. Precedence:
15
+ * 1. explicit `mode` prop (per-render override)
16
+ * 2. SMKING_CMS_THEME env var (per-deployment default)
17
+ * 3. 'css' (Mode A — the SDK ships its own styles)
18
+ *
19
+ * Wizard auto-detect writes the env var on install; customer can still
20
+ * flip per-page via the prop.
21
+ */
22
+ function resolveThemeMode(modeProp?: CmsThemeMode): CmsThemeMode {
23
+ if (modeProp === "css" || modeProp === "tailwind-prose") return modeProp;
24
+ const env = process.env.SMKING_CMS_THEME;
25
+ if (env === "tailwind-prose") return "tailwind-prose";
26
+ return "css";
27
+ }
28
+
11
29
  /**
12
30
  * Server Component that renders a published smking CMS page.
13
31
  *
@@ -41,7 +59,7 @@ import type {
41
59
  * Emits `<title>` / `<meta>` / `og:*` / canonical inline; React 19
42
60
  * hoists them into `<head>` automatically.
43
61
  */
44
- export async function SmkingCms(props: CmsParams) {
62
+ export async function SmkingCms(props: CmsParams & { mode?: CmsThemeMode }) {
45
63
  const data = await getCmsPage(props);
46
64
  if (!data || data.status !== "ready" || !data.page) return null;
47
65
 
@@ -51,6 +69,7 @@ export async function SmkingCms(props: CmsParams) {
51
69
  // also sets these still overrides ours where present.
52
70
  const seo = data.seo;
53
71
  const blocks = data.page.blocks ?? [];
72
+ const mode = resolveThemeMode(props.mode);
54
73
  // Emit the Web Component registration `<script>` once if any block
55
74
  // needs `<smking-carousel>` / `<smking-slide>`. The IIFE is
56
75
  // self-guarded so duplicate emits are harmless; gating here keeps
@@ -60,12 +79,24 @@ export async function SmkingCms(props: CmsParams) {
60
79
  // inline IIFE (no Web Component registration needed) so we don't gate
61
80
  // on it here.
62
81
  const needsSmkingElements = blocks.some((b) => b.component === "carousel");
63
- // IIFE source is a hand-authored package constant (not user data) —
64
- // wrapping in __html here is the standard React server-side script
65
- // emit pattern used elsewhere in this SDK for the article block.
82
+ // IIFE / CSS sources are hand-authored package constants (not user
83
+ // data) — passing them through __html here is the standard React
84
+ // server-side inline-script / inline-style emit pattern.
66
85
  const elementsScript: { __html: string } = { __html: SMKING_ELEMENTS_IIFE };
86
+ const cmsStylesPayload: { __html: string } = { __html: SMKING_CMS_STYLES };
87
+ // Mode A (`css`) — emit the canonical SDK stylesheet once. Mode B
88
+ // (`tailwind-prose`) opts the customer's Tailwind + Typography
89
+ // plugin into rendering and skips this entirely.
90
+ const shouldEmitInlineStyles = mode === "css";
67
91
  return (
68
92
  <>
93
+ {shouldEmitInlineStyles && (
94
+ <style
95
+ data-smking="cms-styles"
96
+ // eslint-disable-next-line react/no-danger -- CSS source is a package constant generated from packages/shared/src/styles/cms-styles.css.
97
+ dangerouslySetInnerHTML={cmsStylesPayload}
98
+ />
99
+ )}
69
100
  {needsSmkingElements && (
70
101
  <script
71
102
  data-smking="elements"
@@ -98,16 +129,27 @@ export async function SmkingCms(props: CmsParams) {
98
129
  <link rel="canonical" href={seo.canonicalUrl} data-smking="cms" />
99
130
  )}
100
131
 
101
- <article
102
- className="smk-cms"
103
- data-smking="cms"
104
- data-content-type={data.page.contentType}
105
- >
106
- {data.page.title && (
107
- <h1 className="smk-cms__title">{data.page.title}</h1>
108
- )}
109
- {blocks.map((block) => renderBlock(block))}
110
- </article>
132
+ {mode === "tailwind-prose" ? (
133
+ <article
134
+ className="prose lg:prose-xl dark:prose-invert mx-auto"
135
+ data-smking="cms"
136
+ data-content-type={data.page.contentType}
137
+ >
138
+ {data.page.title && <h1>{data.page.title}</h1>}
139
+ {blocks.map((block) => renderBlockProse(block))}
140
+ </article>
141
+ ) : (
142
+ <article
143
+ className="smk-cms"
144
+ data-smking="cms"
145
+ data-content-type={data.page.contentType}
146
+ >
147
+ {data.page.title && (
148
+ <h1 className="smk-cms__title">{data.page.title}</h1>
149
+ )}
150
+ {blocks.map((block) => renderBlock(block))}
151
+ </article>
152
+ )}
111
153
  </>
112
154
  );
113
155
  }
@@ -436,3 +478,265 @@ function formatPublishedAt(iso: string): string {
436
478
  return iso.slice(0, 10);
437
479
  }
438
480
  }
481
+
482
+ // ── Mode B (`tailwind-prose`) dispatcher ─────────────────────────────
483
+ //
484
+ // Mirror of `renderBlock` but emits Tailwind utility classes instead
485
+ // of `.smk-*` namespace. The outer `<article class="prose">` wrapper
486
+ // (set up in SmkingCms above) styles `article` blocks automatically;
487
+ // structured blocks (hero/nav/carousel/search) sit inside `not-prose`
488
+ // resets and re-apply Tailwind utility styling.
489
+ //
490
+ // Required Tailwind safelist (the wizard adds these on install):
491
+ // prose / prose-invert / lg:prose-xl / dark:prose-invert / not-prose
492
+ // mx-auto / my-* / mt-* / mb-* / px-* / py-* / pt-* / pb-* / p-*
493
+ // text-* / font-* / tracking-* / leading-*
494
+ // bg-* / border / border-* / rounded-*
495
+ // grid / flex / gap-* / items-* / justify-* / space-y-*
496
+ // w-* / h-* / aspect-*
497
+ function renderBlockProse(block: Block) {
498
+ switch (block.component) {
499
+ case "article": {
500
+ const articleHtml: { __html: string } = { __html: block.props.html };
501
+ // Article HTML inherits `prose` from the outer wrapper.
502
+ return (
503
+ <div
504
+ key={block.id}
505
+ data-block-id={block.id}
506
+ // eslint-disable-next-line react/no-danger -- same trust boundary as renderBlock article case.
507
+ dangerouslySetInnerHTML={articleHtml}
508
+ />
509
+ );
510
+ }
511
+ case "hero":
512
+ return (
513
+ <section
514
+ key={block.id}
515
+ className="not-prose my-12 text-center bg-zinc-50 dark:bg-zinc-900 py-16 px-6 rounded-lg"
516
+ data-block-id={block.id}
517
+ >
518
+ {block.props.image && (
519
+ <img
520
+ src={block.props.image.url}
521
+ alt={block.props.image.alt}
522
+ className="mx-auto mb-8 rounded-lg max-w-full h-auto"
523
+ loading="lazy"
524
+ />
525
+ )}
526
+ {block.props.title && (
527
+ <h2 className="text-4xl md:text-5xl font-serif font-extrabold tracking-tight leading-tight mb-4">
528
+ {block.props.title}
529
+ </h2>
530
+ )}
531
+ {block.props.subtitle && (
532
+ <p className="text-sm text-zinc-600 dark:text-zinc-400 mt-6">
533
+ {block.props.subtitle}
534
+ </p>
535
+ )}
536
+ {block.props.cta && (
537
+ <a
538
+ className="inline-block mt-8 px-6 py-3 bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 rounded font-medium"
539
+ href={block.props.cta.href}
540
+ >
541
+ {block.props.cta.label}
542
+ </a>
543
+ )}
544
+ </section>
545
+ );
546
+ case "nav-recent-posts":
547
+ case "nav-taxonomy-list":
548
+ return renderNavListProse(block);
549
+ case "nav-search":
550
+ return renderNavSearchProse(block);
551
+ case "carousel": {
552
+ const aspectClass = (() => {
553
+ switch (block.props.aspectRatio) {
554
+ case "4:3":
555
+ return "aspect-[4/3]";
556
+ case "1:1":
557
+ return "aspect-square";
558
+ case "3:4":
559
+ return "aspect-[3/4]";
560
+ case "9:16":
561
+ return "aspect-[9/16]";
562
+ default:
563
+ return "aspect-video";
564
+ }
565
+ })();
566
+ return (
567
+ <section
568
+ key={block.id}
569
+ className="not-prose my-8"
570
+ data-block-id={block.id}
571
+ >
572
+ <div className="grid grid-flow-col auto-cols-[80%] md:auto-cols-[60%] gap-4 overflow-x-auto snap-x snap-mandatory pb-2 -mx-6 px-6">
573
+ {block.props.slides.map((slide, i) => (
574
+ <div
575
+ key={i}
576
+ className="snap-start bg-zinc-100 dark:bg-zinc-900 rounded-lg overflow-hidden"
577
+ >
578
+ {slide.imageUrl && (
579
+ <img
580
+ src={slide.imageUrl}
581
+ alt=""
582
+ className={`w-full ${aspectClass} object-cover block`}
583
+ loading="lazy"
584
+ />
585
+ )}
586
+ {slide.caption && (
587
+ <div className="px-4 py-3 text-sm text-zinc-500 dark:text-zinc-400">
588
+ {slide.caption}
589
+ </div>
590
+ )}
591
+ </div>
592
+ ))}
593
+ </div>
594
+ </section>
595
+ );
596
+ }
597
+ default:
598
+ // Unknown component — render nothing in Mode B.
599
+ return null;
600
+ }
601
+ }
602
+
603
+ function renderNavListProse(
604
+ block: Extract<
605
+ Block,
606
+ { component: "nav-recent-posts" | "nav-taxonomy-list" }
607
+ >,
608
+ ) {
609
+ const snapshot: NavSnapshotEntry[] = block.props.snapshot ?? [];
610
+ const layout = block.props.layout ?? "list";
611
+ const heading = (block.props as ModuleHeader).heading;
612
+ const viewAll = (block.props as ModuleHeader).viewAll;
613
+
614
+ const listClass =
615
+ layout === "grid"
616
+ ? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 list-none p-0 m-0"
617
+ : layout === "carousel"
618
+ ? "grid grid-flow-col auto-cols-[70%] md:auto-cols-[40%] gap-4 list-none p-0 m-0 overflow-x-auto snap-x snap-mandatory"
619
+ : "flex flex-col list-none p-0 m-0";
620
+
621
+ return (
622
+ <section
623
+ key={block.id}
624
+ className="not-prose my-10 border-t border-zinc-200 dark:border-zinc-800 pt-8"
625
+ data-block-id={block.id}
626
+ >
627
+ {heading && (
628
+ <h2 className="text-xs font-bold uppercase tracking-widest text-zinc-500 dark:text-zinc-400 mb-6">
629
+ {heading}
630
+ </h2>
631
+ )}
632
+ {snapshot.length === 0 ? (
633
+ <p className="text-zinc-400 italic py-4">No posts yet.</p>
634
+ ) : (
635
+ <ul className={listClass}>
636
+ {snapshot.map((entry) => {
637
+ const itemClass =
638
+ layout === "grid"
639
+ ? "group"
640
+ : layout === "carousel"
641
+ ? "snap-start bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg overflow-hidden"
642
+ : "border-t border-zinc-200 dark:border-zinc-800 first:border-t-0";
643
+ const linkClass =
644
+ layout === "grid"
645
+ ? "flex flex-col bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg overflow-hidden no-underline text-inherit hover:-translate-y-0.5 hover:shadow-lg transition"
646
+ : layout === "carousel"
647
+ ? "flex flex-col no-underline text-inherit"
648
+ : "flex gap-5 items-center py-5 no-underline text-inherit hover:opacity-80 transition";
649
+ const thumbClass =
650
+ layout === "list"
651
+ ? "w-24 h-24 object-cover rounded shrink-0 bg-zinc-100 dark:bg-zinc-900"
652
+ : "w-full aspect-[16/10] object-cover bg-zinc-100 dark:bg-zinc-900";
653
+ const metaClass = layout === "list" ? "flex-1 min-w-0" : "p-4";
654
+ return (
655
+ <li key={entry.slug} className={itemClass}>
656
+ <a href={`/${entry.slug}`} className={linkClass}>
657
+ {entry.featuredImageUrl && (
658
+ <img
659
+ src={entry.featuredImageUrl}
660
+ alt=""
661
+ className={thumbClass}
662
+ loading="lazy"
663
+ />
664
+ )}
665
+ <div className={metaClass}>
666
+ <h3 className="text-lg font-serif font-bold leading-snug mb-2 tracking-tight">
667
+ {entry.title || entry.slug}
668
+ </h3>
669
+ {entry.excerpt && (
670
+ <p className="text-sm text-zinc-500 dark:text-zinc-400 line-clamp-2 leading-relaxed">
671
+ {entry.excerpt}
672
+ </p>
673
+ )}
674
+ {entry.publishedAt && (
675
+ <time
676
+ className="block text-xs text-zinc-400 mt-2 uppercase tracking-wider"
677
+ dateTime={entry.publishedAt}
678
+ >
679
+ {formatPublishedAt(entry.publishedAt)}
680
+ </time>
681
+ )}
682
+ </div>
683
+ </a>
684
+ </li>
685
+ );
686
+ })}
687
+ </ul>
688
+ )}
689
+ {viewAll && (
690
+ <a
691
+ className="inline-flex items-center gap-1 mt-6 text-sm font-semibold text-zinc-900 dark:text-zinc-100 no-underline hover:underline"
692
+ href={viewAll.href}
693
+ >
694
+ {viewAll.label} <span aria-hidden>→</span>
695
+ </a>
696
+ )}
697
+ </section>
698
+ );
699
+ }
700
+
701
+ function renderNavSearchProse(
702
+ block: Extract<Block, { component: "nav-search" }>,
703
+ ) {
704
+ const index: NavSearchIndexEntry[] = block.props.index ?? [];
705
+ const placeholder = block.props.placeholder || "Search…";
706
+ const indexJson = JSON.stringify(index).replace(/</g, "\\u003c");
707
+ const scriptSrc =
708
+ "(function(){var s=document.currentScript;var r=s&&s.previousElementSibling;if(!r)return;var i=r.querySelector('[data-smk-search-input]');var u=r.querySelector('[data-smk-search-results]');var e=r.querySelector('[data-smk-search-empty]');var idx=" +
709
+ indexJson +
710
+ ";function up(){var q=(i.value||'').trim().toLowerCase();u.textContent='';if(!q){u.hidden=true;e.hidden=true;return}var m=idx.filter(function(x){return((x.title||'')+' '+(x.excerpt||'')+' '+(x.slug||'')).toLowerCase().indexOf(q)>=0}).slice(0,20);if(m.length===0){u.hidden=true;e.hidden=false;return}e.hidden=true;u.hidden=false;m.forEach(function(x){var li=document.createElement('li');li.className='border-t border-zinc-200 dark:border-zinc-800 first:border-t-0';var a=document.createElement('a');a.href='/'+x.slug;a.className='block px-4 py-3 text-zinc-900 dark:text-zinc-100 no-underline hover:bg-zinc-50 dark:hover:bg-zinc-900';a.textContent=x.title||x.slug;li.appendChild(a);u.appendChild(li)})}i.addEventListener('input',up)})();";
711
+
712
+ return (
713
+ <section
714
+ key={block.id}
715
+ className="not-prose my-8 border-t border-zinc-200 dark:border-zinc-800 pt-6"
716
+ data-block-id={block.id}
717
+ >
718
+ <div className="relative">
719
+ <input
720
+ type="search"
721
+ data-smk-search-input
722
+ placeholder={placeholder}
723
+ className="w-full px-4 py-3 border border-zinc-200 dark:border-zinc-800 rounded-lg bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 text-base focus:outline-none focus:border-zinc-900 dark:focus:border-zinc-100 focus:ring-4 focus:ring-zinc-900/10 dark:focus:ring-zinc-100/10 transition"
724
+ />
725
+ <ul
726
+ data-smk-search-results
727
+ hidden
728
+ className="list-none p-0 mt-3 border border-zinc-200 dark:border-zinc-800 rounded-lg bg-white dark:bg-zinc-900 overflow-hidden shadow-lg"
729
+ />
730
+ <p
731
+ data-smk-search-empty
732
+ hidden
733
+ className="text-zinc-400 pt-4 text-sm"
734
+ >
735
+ No matches.
736
+ </p>
737
+ </div>
738
+ {/* eslint-disable-next-line react/no-danger -- inline IIFE; index JSON-escaped against </script> smuggling. */}
739
+ <script dangerouslySetInnerHTML={{ __html: scriptSrc }} />
740
+ </section>
741
+ );
742
+ }
@@ -0,0 +1,704 @@
1
+ /**
2
+ * AUTO-GENERATED - do not edit by hand.
3
+ * Source: packages/shared/src/styles/cms-styles.css
4
+ * Sync: pnpm sync:cms-styles
5
+ * Hash: f555c5fed446
6
+ *
7
+ * Inlined by <SmkingCms> (smking-cms.tsx) inside a
8
+ * <style data-smking="cms-styles"> ... </style>
9
+ * tag exactly once per page render. Mode A only; Mode B
10
+ * ("tailwind-prose") skips this entirely.
11
+ */
12
+ export const SMKING_CMS_STYLES = `/*
13
+ * smking CMS — canonical block styles (Mode A "css")
14
+ *
15
+ * Single source of truth for all customer-facing CMS render styling.
16
+ * Consumed at build time by \`scripts/sync-cms-styles.mjs\` which emits:
17
+ * - packages/smking-next/src/lib/smking-cms-styles.ts (TS const)
18
+ * - packages/smking-laravel/resources/views/cms-styles.blade.php (Blade)
19
+ *
20
+ * The dashboard imports the same file directly via Next.js workspace
21
+ * alias so what the editor preview shows = what the customer site renders
22
+ * (WYSIWYG strict per boss original spec).
23
+ *
24
+ * Mode B (\`tailwind-prose\`) customers do NOT load this file — their
25
+ * Tailwind + @tailwindcss/typography plugin replaces all styles below.
26
+ *
27
+ * Namespacing rules (cross-platform parity):
28
+ * - All selectors are scoped to \`.smk-cms\` or \`.smk-block--*\` — no
29
+ * global element rules (\`h1 {}\` \`p {}\`) so SDK styles never leak
30
+ * into customer's other page sections.
31
+ * - Wrapped in \`@layer smking-cms\` so customer CSS authoring with
32
+ * Tailwind \`@layer base|components|utilities\` has predictable order.
33
+ * - Tokens on \`.smk-cms\` (CSS variables) — customer overrides via
34
+ * \`:root { --smk-accent: …; }\` or per-instance.
35
+ *
36
+ * Edit invariants:
37
+ * 1. Never write a global element selector.
38
+ * 2. Always declare variants via \`[data-*]\` attribute selectors — the
39
+ * SDK markup emits the attributes; CSS reads them. No hard-coded
40
+ * ratios / layouts / colors.
41
+ * 3. Test against Mode A's class hooks: smk-cms / smk-block--{type} /
42
+ * smk-hero / smk-nav / smk-nav-search / smk-carousel.
43
+ */
44
+
45
+ @layer smking-cms {
46
+ /* ============================================================
47
+ Design tokens — customer overrides on :root or .smk-cms
48
+ ============================================================ */
49
+ .smk-cms {
50
+ --smk-fg: #0a0a0a;
51
+ --smk-fg-muted: #525252;
52
+ --smk-fg-subtle: #a3a3a3;
53
+ --smk-bg: #fff;
54
+ --smk-bg-soft: #fafafa;
55
+ --smk-border: #e5e5e5;
56
+ --smk-border-strong: #d4d4d4;
57
+ --smk-accent: #0a0a0a;
58
+ --smk-accent-fg: #fff;
59
+ --smk-radius-sm: 6px;
60
+ --smk-radius-md: 10px;
61
+ --smk-radius-lg: 16px;
62
+ --smk-section-gap: 4rem;
63
+ --smk-font-serif: ui-serif, Georgia, "Iowan Old Style", "Apple Garamond",
64
+ "Times New Roman", serif;
65
+ --smk-font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI",
66
+ "PingFang TC", "Noto Sans CJK TC", "Microsoft JhengHei", sans-serif;
67
+ }
68
+ @media (prefers-color-scheme: dark) {
69
+ .smk-cms {
70
+ --smk-fg: #f5f5f5;
71
+ --smk-fg-muted: #a3a3a3;
72
+ --smk-fg-subtle: #525252;
73
+ --smk-bg: #0a0a0a;
74
+ --smk-bg-soft: #171717;
75
+ --smk-border: #262626;
76
+ --smk-border-strong: #404040;
77
+ --smk-accent: #f5f5f5;
78
+ --smk-accent-fg: #0a0a0a;
79
+ }
80
+ }
81
+
82
+ /* ============================================================
83
+ Article wrapper
84
+ ============================================================ */
85
+ .smk-cms {
86
+ color: var(--smk-fg);
87
+ font-family: var(--smk-font-sans);
88
+ line-height: 1.7;
89
+ font-size: 17px;
90
+ max-width: 42rem;
91
+ margin: 0 auto;
92
+ padding: 3rem 1.5rem;
93
+ }
94
+ .smk-cms__title {
95
+ font-family: var(--smk-font-serif);
96
+ font-size: clamp(2rem, 5vw, 3rem);
97
+ font-weight: 700;
98
+ line-height: 1.1;
99
+ letter-spacing: -0.02em;
100
+ margin: 0 0 3rem;
101
+ }
102
+ .smk-cms > .smk-block + .smk-block {
103
+ margin-top: 2.5rem;
104
+ }
105
+ /* Hero edges out to wrapper bounds; other blocks stay readable column. */
106
+ .smk-cms > .smk-block--hero {
107
+ margin-left: -1.5rem;
108
+ margin-right: -1.5rem;
109
+ }
110
+
111
+ /* ============================================================
112
+ Article — Plate-rendered HTML
113
+ SDK emits: \`<div class="smk-block smk-block--article">{html}</div>\`
114
+ The inner HTML comes from Plate's serializeHtml on the SaaS side,
115
+ so customer SDK ships zero editor deps. Below selectors style the
116
+ emitted h1/h2/p/ul/blockquote etc inside the article wrapper.
117
+ ============================================================ */
118
+ .smk-block--article p {
119
+ margin: 0 0 1.25em;
120
+ }
121
+ .smk-block--article p:last-child {
122
+ margin-bottom: 0;
123
+ }
124
+ .smk-block--article h1,
125
+ .smk-block--article h2,
126
+ .smk-block--article h3 {
127
+ font-family: var(--smk-font-serif);
128
+ font-weight: 700;
129
+ letter-spacing: -0.015em;
130
+ line-height: 1.2;
131
+ }
132
+ .smk-block--article h1 {
133
+ font-size: 2rem;
134
+ margin: 2em 0 0.5em;
135
+ }
136
+ .smk-block--article h2 {
137
+ font-size: 1.6rem;
138
+ margin: 1.8em 0 0.5em;
139
+ }
140
+ .smk-block--article h3 {
141
+ font-size: 1.25rem;
142
+ margin: 1.5em 0 0.5em;
143
+ }
144
+ .smk-block--article h1:first-child,
145
+ .smk-block--article h2:first-child,
146
+ .smk-block--article h3:first-child {
147
+ margin-top: 0;
148
+ }
149
+ .smk-block--article ul,
150
+ .smk-block--article ol {
151
+ margin: 0 0 1.25em;
152
+ padding-left: 1.75em;
153
+ }
154
+ .smk-block--article li {
155
+ margin: 0.4em 0;
156
+ }
157
+ .smk-block--article li::marker {
158
+ color: var(--smk-fg-subtle);
159
+ }
160
+ .smk-block--article a {
161
+ color: inherit;
162
+ text-decoration: underline;
163
+ text-decoration-color: var(--smk-border-strong);
164
+ text-underline-offset: 3px;
165
+ transition: text-decoration-color 0.15s;
166
+ }
167
+ .smk-block--article a:hover {
168
+ text-decoration-color: currentColor;
169
+ }
170
+ .smk-block--article blockquote {
171
+ margin: 1.5em 0;
172
+ padding: 0.25em 0 0.25em 1.25rem;
173
+ border-left: 3px solid var(--smk-border-strong);
174
+ color: var(--smk-fg-muted);
175
+ font-style: italic;
176
+ }
177
+ .smk-block--article code {
178
+ background: var(--smk-bg-soft);
179
+ padding: 0.15em 0.4em;
180
+ border-radius: 4px;
181
+ font-size: 0.9em;
182
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
183
+ }
184
+ .smk-block--article pre {
185
+ background: var(--smk-bg-soft);
186
+ padding: 1rem;
187
+ border-radius: var(--smk-radius-sm);
188
+ overflow-x: auto;
189
+ margin: 1.5em 0;
190
+ }
191
+ .smk-block--article pre code {
192
+ background: transparent;
193
+ padding: 0;
194
+ }
195
+ .smk-block--article img {
196
+ max-width: 100%;
197
+ height: auto;
198
+ border-radius: var(--smk-radius-sm);
199
+ margin: 1.5em 0;
200
+ }
201
+
202
+ /* ============================================================
203
+ Hero — magazine cover banner
204
+ SDK emits: \`<section class="smk-block smk-block--hero smk-hero">\`
205
+ ============================================================ */
206
+ .smk-hero {
207
+ text-align: center;
208
+ padding: 5rem 1.5rem 4rem;
209
+ background: var(--smk-bg-soft);
210
+ border-radius: 0;
211
+ margin-bottom: 3rem;
212
+ position: relative;
213
+ }
214
+ .smk-hero__image {
215
+ max-width: 100%;
216
+ height: auto;
217
+ margin: 0 auto 2rem;
218
+ border-radius: var(--smk-radius-md);
219
+ display: block;
220
+ }
221
+ .smk-hero__title {
222
+ font-family: var(--smk-font-serif);
223
+ font-size: clamp(2.25rem, 6vw, 4rem);
224
+ font-weight: 800;
225
+ line-height: 1.05;
226
+ letter-spacing: -0.025em;
227
+ margin: 0 0 1rem;
228
+ color: var(--smk-fg);
229
+ max-width: 32rem;
230
+ margin-inline: auto;
231
+ }
232
+ .smk-hero__subtitle {
233
+ color: var(--smk-fg-muted);
234
+ font-size: 0.95rem;
235
+ margin: 1.5rem auto 0;
236
+ max-width: 28rem;
237
+ letter-spacing: 0.02em;
238
+ }
239
+ .smk-hero__cta {
240
+ display: inline-block;
241
+ padding: 0.75rem 1.5rem;
242
+ margin-top: 2rem;
243
+ background: var(--smk-accent);
244
+ color: var(--smk-accent-fg);
245
+ border-radius: var(--smk-radius-sm);
246
+ text-decoration: none;
247
+ font-weight: 500;
248
+ font-size: 0.95rem;
249
+ transition: opacity 0.15s;
250
+ }
251
+ .smk-hero__cta:hover {
252
+ opacity: 0.85;
253
+ }
254
+
255
+ /* ============================================================
256
+ Nav — recent-posts / taxonomy-list
257
+ SDK emits: \`<section class="smk-block smk-block--nav-{type} smk-nav {sub-class}" data-layout="list|grid|carousel">\`
258
+ ============================================================ */
259
+ .smk-nav {
260
+ padding: 2.5rem 0;
261
+ border-top: 1px solid var(--smk-border);
262
+ }
263
+ .smk-nav__heading {
264
+ font-size: 0.75rem;
265
+ font-weight: 700;
266
+ text-transform: uppercase;
267
+ letter-spacing: 0.12em;
268
+ color: var(--smk-fg-muted);
269
+ margin: 0 0 1.5rem;
270
+ }
271
+ .smk-nav__list {
272
+ list-style: none;
273
+ padding: 0;
274
+ margin: 0;
275
+ }
276
+
277
+ /* List layout (data-layout="list") */
278
+ .smk-nav[data-layout="list"] .smk-nav__list {
279
+ display: flex;
280
+ flex-direction: column;
281
+ }
282
+ .smk-nav[data-layout="list"] .smk-nav__item + .smk-nav__item {
283
+ border-top: 1px solid var(--smk-border);
284
+ }
285
+ .smk-nav[data-layout="list"] .smk-nav__link {
286
+ display: flex;
287
+ gap: 1.25rem;
288
+ align-items: center;
289
+ padding: 1.25rem 0;
290
+ color: inherit;
291
+ text-decoration: none;
292
+ transition: opacity 0.15s;
293
+ }
294
+ .smk-nav[data-layout="list"] .smk-nav__thumb {
295
+ width: 6rem;
296
+ height: 6rem;
297
+ object-fit: cover;
298
+ border-radius: var(--smk-radius-sm);
299
+ flex-shrink: 0;
300
+ background: var(--smk-bg-soft);
301
+ }
302
+
303
+ /* Grid layout (data-layout="grid") */
304
+ .smk-nav[data-layout="grid"] .smk-nav__list {
305
+ display: grid;
306
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
307
+ gap: 1.25rem;
308
+ }
309
+ .smk-nav[data-layout="grid"] .smk-nav__link {
310
+ display: flex;
311
+ flex-direction: column;
312
+ color: inherit;
313
+ text-decoration: none;
314
+ background: var(--smk-bg);
315
+ border: 1px solid var(--smk-border);
316
+ border-radius: var(--smk-radius-md);
317
+ overflow: hidden;
318
+ transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s;
319
+ }
320
+ .smk-nav[data-layout="grid"] .smk-nav__link:hover {
321
+ transform: translateY(-2px);
322
+ border-color: var(--smk-border-strong);
323
+ box-shadow: 0 8px 24px -8px rgb(0 0 0 / 0.12);
324
+ }
325
+ .smk-nav[data-layout="grid"] .smk-nav__thumb {
326
+ width: 100%;
327
+ aspect-ratio: 16 / 10;
328
+ object-fit: cover;
329
+ background: var(--smk-bg-soft);
330
+ }
331
+ .smk-nav[data-layout="grid"] .smk-nav__meta {
332
+ padding: 1rem;
333
+ }
334
+
335
+ /* Carousel layout (data-layout="carousel") — horizontal scroll-snap */
336
+ .smk-nav[data-layout="carousel"] .smk-nav__list {
337
+ display: grid;
338
+ grid-auto-flow: column;
339
+ grid-auto-columns: 70%;
340
+ gap: 1rem;
341
+ overflow-x: auto;
342
+ scroll-snap-type: x mandatory;
343
+ padding-bottom: 0.5rem;
344
+ }
345
+ @media (min-width: 640px) {
346
+ .smk-nav[data-layout="carousel"] .smk-nav__list {
347
+ grid-auto-columns: 40%;
348
+ }
349
+ }
350
+ .smk-nav[data-layout="carousel"] .smk-nav__item {
351
+ scroll-snap-align: start;
352
+ background: var(--smk-bg);
353
+ border: 1px solid var(--smk-border);
354
+ border-radius: var(--smk-radius-md);
355
+ overflow: hidden;
356
+ }
357
+ .smk-nav[data-layout="carousel"] .smk-nav__link {
358
+ display: flex;
359
+ flex-direction: column;
360
+ color: inherit;
361
+ text-decoration: none;
362
+ }
363
+ .smk-nav[data-layout="carousel"] .smk-nav__thumb {
364
+ width: 100%;
365
+ aspect-ratio: 16 / 10;
366
+ object-fit: cover;
367
+ background: var(--smk-bg-soft);
368
+ }
369
+ .smk-nav[data-layout="carousel"] .smk-nav__meta {
370
+ padding: 1rem;
371
+ }
372
+
373
+ /* Shared internals */
374
+ .smk-nav__link:hover {
375
+ opacity: 0.85;
376
+ }
377
+ .smk-nav[data-layout="grid"] .smk-nav__link:hover,
378
+ .smk-nav[data-layout="carousel"] .smk-nav__link:hover {
379
+ opacity: 1;
380
+ }
381
+ .smk-nav__meta {
382
+ flex: 1;
383
+ min-width: 0;
384
+ }
385
+ .smk-nav__title {
386
+ font-family: var(--smk-font-serif);
387
+ font-size: 1.15rem;
388
+ font-weight: 700;
389
+ line-height: 1.3;
390
+ margin: 0 0 0.5rem;
391
+ letter-spacing: -0.01em;
392
+ }
393
+ .smk-nav__excerpt {
394
+ font-size: 0.9rem;
395
+ color: var(--smk-fg-muted);
396
+ margin: 0;
397
+ line-height: 1.55;
398
+ display: -webkit-box;
399
+ -webkit-line-clamp: 2;
400
+ -webkit-box-orient: vertical;
401
+ overflow: hidden;
402
+ }
403
+ .smk-nav__date {
404
+ display: block;
405
+ font-size: 0.75rem;
406
+ color: var(--smk-fg-subtle);
407
+ margin-top: 0.6rem;
408
+ letter-spacing: 0.04em;
409
+ text-transform: uppercase;
410
+ }
411
+ .smk-nav__empty {
412
+ color: var(--smk-fg-subtle);
413
+ font-style: italic;
414
+ padding: 1rem 0;
415
+ }
416
+ .smk-nav__view-all {
417
+ display: inline-flex;
418
+ align-items: center;
419
+ gap: 0.4rem;
420
+ margin-top: 1.5rem;
421
+ color: var(--smk-accent);
422
+ font-size: 0.875rem;
423
+ font-weight: 600;
424
+ text-decoration: none;
425
+ }
426
+ .smk-nav__view-all::after {
427
+ content: "→";
428
+ transition: transform 0.15s;
429
+ }
430
+ .smk-nav__view-all:hover::after {
431
+ transform: translateX(3px);
432
+ }
433
+
434
+ /* ============================================================
435
+ Nav-search — inline filter input
436
+ SDK emits: \`<section class="smk-block smk-block--nav-search smk-nav-search">\`
437
+ with \`data-smk-search-input\` / \`data-smk-search-results\` /
438
+ \`data-smk-search-empty\` attributes the IIFE wires up.
439
+ ============================================================ */
440
+ .smk-nav-search {
441
+ padding: 2rem 0;
442
+ border-top: 1px solid var(--smk-border);
443
+ }
444
+ .smk-nav-search__shell {
445
+ position: relative;
446
+ }
447
+ .smk-nav-search__input {
448
+ width: 100%;
449
+ padding: 0.875rem 1.125rem;
450
+ border: 1px solid var(--smk-border);
451
+ border-radius: var(--smk-radius-md);
452
+ background: var(--smk-bg);
453
+ color: var(--smk-fg);
454
+ font-size: 1rem;
455
+ font-family: inherit;
456
+ transition: border-color 0.15s, box-shadow 0.15s;
457
+ }
458
+ .smk-nav-search__input::placeholder {
459
+ color: var(--smk-fg-subtle);
460
+ }
461
+ .smk-nav-search__input:focus {
462
+ outline: none;
463
+ border-color: var(--smk-fg);
464
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--smk-fg) 8%, transparent);
465
+ }
466
+ .smk-nav-search__list {
467
+ list-style: none;
468
+ padding: 0;
469
+ margin: 0.75rem 0 0;
470
+ border: 1px solid var(--smk-border);
471
+ border-radius: var(--smk-radius-md);
472
+ background: var(--smk-bg);
473
+ overflow: hidden;
474
+ box-shadow: 0 4px 12px -4px rgb(0 0 0 / 0.1);
475
+ }
476
+ .smk-nav-search__result + .smk-nav-search__result {
477
+ border-top: 1px solid var(--smk-border);
478
+ }
479
+ .smk-nav-search__result a {
480
+ display: block;
481
+ padding: 0.875rem 1.125rem;
482
+ color: var(--smk-fg);
483
+ text-decoration: none;
484
+ transition: background 0.1s;
485
+ }
486
+ .smk-nav-search__result a:hover {
487
+ background: var(--smk-bg-soft);
488
+ }
489
+ .smk-nav-search__empty {
490
+ color: var(--smk-fg-subtle);
491
+ padding: 1rem 0 0;
492
+ font-size: 0.9rem;
493
+ }
494
+
495
+ /* ============================================================
496
+ Carousel — Apple-newsroom style horizontal carousel with
497
+ \`data-aspect\` token-driven aspect-ratio, container-query-anchored
498
+ nav buttons, and dots indicator.
499
+ SDK emits:
500
+ <smking-carousel class="smk-block smk-block--carousel smk-carousel"
501
+ data-aspect="16:9|4:3|1:1|3:4|9:16">
502
+ <div class="smk-carousel__viewport">
503
+ <smking-slide>
504
+ <div class="smk-carousel__image-frame">
505
+ <img class="smk-carousel__image" /> or .__image--placeholder
506
+ </div>
507
+ <div class="smk-carousel__caption-row">
508
+ <div class="smk-carousel__caption">…</div>
509
+ <a class="smk-carousel__download" href />
510
+ </div>
511
+ </smking-slide>
512
+ </div>
513
+ <button class="smk-carousel__nav-prev|next" />
514
+ <div class="smk-carousel__dots">
515
+ <button class="smk-carousel__dot" aria-current="true|false" />
516
+ </div>
517
+ </smking-carousel>
518
+
519
+ Web Component IIFE (SMKING_ELEMENTS_IIFE) upgrades the markup with
520
+ swipe / scroll-snap / dot-binding behaviour client-side. CSS below
521
+ drives all visual state — image cover-fill, nav-button vertical
522
+ centering anchored to image height (cqw container-queries), dot
523
+ active state.
524
+ ============================================================ */
525
+ smking-carousel,
526
+ .smk-carousel {
527
+ display: block;
528
+ position: relative;
529
+ margin: 2.5rem 0;
530
+ padding-top: 2.5rem; /* room for editor settings overlay; harmless on customer side */
531
+ padding-bottom: 1.5rem;
532
+ container-type: inline-size; /* enables \`cqw\` for nav-button vertical anchor */
533
+ --smk-carousel-aspect: 16 / 9;
534
+ --smk-carousel-nav-top: 28.125cqw; /* 50cqw × 9/16 = half of image height */
535
+ }
536
+ .smk-carousel[data-aspect="4:3"],
537
+ smking-carousel[data-aspect="4:3"] {
538
+ --smk-carousel-aspect: 4 / 3;
539
+ --smk-carousel-nav-top: 37.5cqw;
540
+ }
541
+ .smk-carousel[data-aspect="1:1"],
542
+ smking-carousel[data-aspect="1:1"] {
543
+ --smk-carousel-aspect: 1 / 1;
544
+ --smk-carousel-nav-top: 50cqw;
545
+ }
546
+ .smk-carousel[data-aspect="3:4"],
547
+ smking-carousel[data-aspect="3:4"] {
548
+ --smk-carousel-aspect: 3 / 4;
549
+ --smk-carousel-nav-top: 66.667cqw;
550
+ }
551
+ .smk-carousel[data-aspect="9:16"],
552
+ smking-carousel[data-aspect="9:16"] {
553
+ --smk-carousel-aspect: 9 / 16;
554
+ --smk-carousel-nav-top: 88.889cqw;
555
+ }
556
+
557
+ .smk-carousel__viewport {
558
+ display: flex;
559
+ overflow-x: auto;
560
+ scroll-snap-type: x mandatory;
561
+ scroll-behavior: smooth;
562
+ scrollbar-width: none;
563
+ -webkit-overflow-scrolling: touch;
564
+ }
565
+ .smk-carousel__viewport::-webkit-scrollbar {
566
+ display: none;
567
+ }
568
+
569
+ smking-slide,
570
+ .smk-carousel__slide {
571
+ display: flex;
572
+ flex-direction: column;
573
+ flex: 0 0 100%;
574
+ scroll-snap-align: start;
575
+ min-width: 0;
576
+ padding-inline: 0.5rem;
577
+ gap: 1rem;
578
+ }
579
+
580
+ /* Image frame — fixed-aspect box. Image cover-fills. */
581
+ .smk-carousel__image-frame {
582
+ position: relative;
583
+ aspect-ratio: var(--smk-carousel-aspect);
584
+ overflow: hidden;
585
+ border-radius: var(--smk-radius-md);
586
+ }
587
+ .smk-carousel__image {
588
+ display: block;
589
+ width: 100%;
590
+ height: 100%;
591
+ object-fit: cover;
592
+ }
593
+ .smk-carousel__image--placeholder {
594
+ width: 100%;
595
+ height: 100%;
596
+ display: flex;
597
+ align-items: center;
598
+ justify-content: center;
599
+ background: var(--smk-bg-soft);
600
+ border: 1px dashed var(--smk-border);
601
+ }
602
+
603
+ /* Caption row — text on the left, download icon on the right. Matches
604
+ Apple newsroom's per-slide attribution + asset-download pattern. */
605
+ .smk-carousel__caption-row {
606
+ display: flex;
607
+ align-items: center;
608
+ justify-content: space-between;
609
+ gap: 1rem;
610
+ padding-inline: 0.25rem;
611
+ }
612
+ .smk-carousel__caption {
613
+ flex: 1;
614
+ text-align: center;
615
+ font-size: 0.875rem;
616
+ color: var(--smk-fg-muted);
617
+ line-height: 1.5;
618
+ }
619
+ .smk-carousel__download {
620
+ display: inline-flex;
621
+ align-items: center;
622
+ justify-content: center;
623
+ width: 1.75rem;
624
+ height: 1.75rem;
625
+ border-radius: 9999px;
626
+ color: var(--smk-fg-muted);
627
+ text-decoration: none;
628
+ transition: background-color 0.15s, color 0.15s;
629
+ }
630
+ .smk-carousel__download:hover {
631
+ background: var(--smk-bg-soft);
632
+ color: var(--smk-fg);
633
+ }
634
+
635
+ /* Nav buttons — circular cards overlapping the image edges. Top is
636
+ centered on the IMAGE (not the whole carousel) via container query:
637
+ image height = 100cqw × (ratio_h / ratio_w); half = nav-top var.
638
+ 2.5rem offset = container padding-top. */
639
+ .smk-carousel__nav-prev,
640
+ .smk-carousel__nav-next {
641
+ position: absolute;
642
+ top: calc(2.5rem + var(--smk-carousel-nav-top));
643
+ transform: translateY(-50%);
644
+ z-index: 1;
645
+ display: inline-flex;
646
+ align-items: center;
647
+ justify-content: center;
648
+ width: 2.5rem;
649
+ height: 2.5rem;
650
+ padding: 0;
651
+ border-radius: 9999px;
652
+ border: 1px solid var(--smk-border);
653
+ background: var(--smk-bg);
654
+ color: var(--smk-fg);
655
+ cursor: pointer;
656
+ box-shadow: 0 1px 3px rgb(0 0 0 / 0.06);
657
+ transition: background-color 0.15s, transform 0.15s;
658
+ }
659
+ .smk-carousel__nav-prev { left: 0.75rem; }
660
+ .smk-carousel__nav-next { right: 0.75rem; }
661
+ .smk-carousel__nav-prev:hover,
662
+ .smk-carousel__nav-next:hover {
663
+ background: var(--smk-bg-soft);
664
+ }
665
+ .smk-carousel__nav-prev:active,
666
+ .smk-carousel__nav-next:active {
667
+ transform: translateY(-50%) scale(0.95);
668
+ }
669
+
670
+ /* Dots — small circles centered below the caption row. Inactive is
671
+ hairline grey, active fills with foreground colour. */
672
+ .smk-carousel__dots {
673
+ display: flex;
674
+ justify-content: center;
675
+ align-items: center;
676
+ gap: 0.5rem;
677
+ margin-top: 0.75rem;
678
+ }
679
+ .smk-carousel__dot {
680
+ width: 0.5rem;
681
+ height: 0.5rem;
682
+ padding: 0;
683
+ border-radius: 9999px;
684
+ border: none;
685
+ background: color-mix(in srgb, var(--smk-fg) 20%, transparent);
686
+ cursor: pointer;
687
+ transition: background-color 0.15s, transform 0.15s;
688
+ }
689
+ .smk-carousel__dot:hover {
690
+ background: color-mix(in srgb, var(--smk-fg) 45%, transparent);
691
+ }
692
+ .smk-carousel__dot[aria-current="true"] {
693
+ background: var(--smk-fg);
694
+ transform: scale(1.1);
695
+ }
696
+
697
+ /* ============================================================
698
+ Unknown block — forward-compat fallback
699
+ ============================================================ */
700
+ .smk-block--unknown {
701
+ display: none;
702
+ }
703
+ }
704
+ `;
package/src/types.ts CHANGED
@@ -58,6 +58,25 @@ export type CmsStatus = "ready" | "pending" | "not_found";
58
58
 
59
59
  export type CmsContentType = "article" | "landing" | "listing";
60
60
 
61
+ /**
62
+ * Theme mode (v0.14+) — decides how `<SmkingCms>` ships styling.
63
+ *
64
+ * - `'css'` (Mode A, default) — SDK inlines the canonical
65
+ * `<style data-smking="cms-styles">` once per render plus emits
66
+ * `.smk-*` class hooks on every block. Zero dependency on Tailwind.
67
+ * Customers theme via CSS variable overrides (`:root { --smk-accent: … }`).
68
+ *
69
+ * - `'tailwind-prose'` (Mode B) — SDK skips the inline stylesheet,
70
+ * wraps the article in `<article class="prose dark:prose-invert">`
71
+ * and emits Tailwind utility classes on each block. Customer must
72
+ * have `@tailwindcss/typography` plugin wired up and the required
73
+ * classes safelisted (the wizard does this on install).
74
+ *
75
+ * Resolution order: explicit `<SmkingCms mode>` prop → `SMKING_CMS_THEME`
76
+ * env var → 'css'.
77
+ */
78
+ export type CmsThemeMode = "css" | "tailwind-prose";
79
+
61
80
  /**
62
81
  * v2 substrate block primitives. Mirrors `apps/web/src/features/cms/types.ts`
63
82
  * on the SaaS side (kept in sync manually — when the v1 block catalog