@soloworks/smking-next 0.15.0 → 0.16.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 +57 -0
- package/package.json +3 -1
- package/src/components/smking-cms.tsx +27 -402
- package/src/components/smking-runtime.tsx +30 -0
- package/src/index.ts +1 -0
- package/src/lib/cms-client.ts +5 -2
- package/src/lib/smking-cms-styles.ts +0 -704
- package/src/lib/smking-elements-iife.ts +0 -46
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# @soloworks/smking-next
|
|
2
2
|
|
|
3
|
+
## 0.16.1 — 2026-05-25
|
|
4
|
+
|
|
5
|
+
**New `<SmkingRuntime />` component — one-time mount in customer root layout to load saas-served CSS + Web Component runtime JS.**
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `<SmkingRuntime>` exports from package root. Customers mount it once in `app/layout.tsx` (or equivalent); it emits `<link rel="stylesheet">` + `<script async>` pointing at `/api/v1/public/runtime.{css,js}` on the saas.
|
|
10
|
+
- Optional `baseUrl` prop overrides `process.env.SMKING_BASE_URL`. Default falls back to `https://smking.app`.
|
|
11
|
+
|
|
12
|
+
### Why
|
|
13
|
+
|
|
14
|
+
bodyHtml emitted by SaaS publish (v0.16+) carries Tailwind utility classes from the dashboard's cva variants. Customer Tailwind JIT can't see those runtime strings at build time, so utility classes render as dead strings without `<SmkingRuntime>` providing the compiled CSS. `<SmkingRuntime>` solves the propagation gap and additionally bundles Web Component IIFEs (`<smking-carousel>` etc.) so future dashboard custom elements reach customer sites without an SDK bump cycle.
|
|
15
|
+
|
|
16
|
+
### Migration
|
|
17
|
+
|
|
18
|
+
Add one line to your root layout:
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { SmkingRuntime } from "@soloworks/smking-next";
|
|
22
|
+
|
|
23
|
+
export default function RootLayout({ children }) {
|
|
24
|
+
return (
|
|
25
|
+
<html>
|
|
26
|
+
<body>
|
|
27
|
+
<SmkingRuntime />
|
|
28
|
+
{children}
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Patch bump — no breaking changes. Existing v0.16.0 installs keep working; mounting `<SmkingRuntime>` is opt-in but recommended.
|
|
36
|
+
|
|
37
|
+
## 0.16.0 — 2026-05-24
|
|
38
|
+
|
|
39
|
+
**Raw passthrough render — `<SmkingCms>` now echoes the full Plate-serialized bodyHtml exactly, no `<article class="smk-cms">` wrapper and no inline CSS injection.**
|
|
40
|
+
|
|
41
|
+
### What changed
|
|
42
|
+
|
|
43
|
+
Companion release to a SaaS-side simplification: the publish pipeline now calls Plate's stock `serializeHtml(editor)` with no options — preserving the full class chain (Tailwind utility from the dashboard cva variants + `slate-*` hook classes + `data-slate-*` attrs) instead of running an in-house `stripPlateNoise()` post-process. `<SmkingCms>` mirrors that posture: drop the wrapper, drop the canonical CSS inject, let bodyHtml render exactly as the dashboard Edit / Preview tabs render it.
|
|
44
|
+
|
|
45
|
+
### Removed (breaking)
|
|
46
|
+
|
|
47
|
+
- **`<article class="smk-cms">` shell wrapper** — replaced with a plain `<div data-smking="cms">`. Customer sites that depended on `.smk-cms` for layout (max-width / padding) need to reapply that themselves.
|
|
48
|
+
- **Inline canonical CSS injection** — gone. Customer sites that rely on the canonical block CSS must now ship it themselves (the constant `SMKING_CMS_STYLES` is still exported from `lib/smking-cms-styles` for sites that want to opt in).
|
|
49
|
+
- **Block[] dispatch path** — `renderBlock` + `renderNavList` + `renderNavSearch` + `formatPublishedAt` helpers (~325 lines) removed. Pages still on the legacy `page.blocks` shape will not render until the SaaS re-publishes them (every page since v0.15 has a bodyHtml field, so this only affects truly ancient drafts that have not been touched in months).
|
|
50
|
+
- **Smking Web Component IIFE injection** — `<smking-carousel>` registration script is no longer auto-emitted. Customer sites with carousel blocks need to ship their own registration; the IIFE constant remains exported from `lib/smking-elements-iife` for sites that want to inline it manually.
|
|
51
|
+
|
|
52
|
+
### Why
|
|
53
|
+
|
|
54
|
+
Dashboard now publishes the same Plate output the Edit canvas renders. Wrapping that markup in a separate "customer article shell" with its own typography tokens broke the WYSIWYG promise — author saw one thing in the editor, customer saw another. Raw passthrough makes the contract unambiguous: bodyHtml is the source of truth, customer site owns layout.
|
|
55
|
+
|
|
56
|
+
### Migration
|
|
57
|
+
|
|
58
|
+
If your customer site relied on the `.smk-cms` shell or the canonical CSS inject, ship them at the customer side before bumping — import `SMKING_CMS_STYLES` from `@soloworks/smking-next/lib/smking-cms-styles` and emit it via a `<style>` tag, then wrap `<SmkingCms>` in your own `<article className="smk-cms">`. For Tailwind sites: utility classes in bodyHtml (`text-4xl`, `font-bold`, etc.) resolve natively — no extra step needed.
|
|
59
|
+
|
|
3
60
|
## 0.15.0 — 2026-05-24
|
|
4
61
|
|
|
5
62
|
**Customer Mode A now consumes Plate-serialized HTML directly — Preview tab = customer site, byte-identical.**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soloworks/smking-next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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",
|
|
@@ -74,10 +74,12 @@
|
|
|
74
74
|
"@tiptap/starter-kit": "^3.23.4",
|
|
75
75
|
"@tiptap/static-renderer": "^3.23.4",
|
|
76
76
|
"@types/react": "^19.0.0",
|
|
77
|
+
"@types/react-dom": "^19",
|
|
77
78
|
"@vitejs/plugin-react": "^6.0.1",
|
|
78
79
|
"jsdom": "^29.1.0",
|
|
79
80
|
"next": "^16.2.2",
|
|
80
81
|
"react": "^19.0.0",
|
|
82
|
+
"react-dom": "19.2.4",
|
|
81
83
|
"typescript": "^5",
|
|
82
84
|
"vitest": "^4.1.5"
|
|
83
85
|
},
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import { getCmsPage } from "../lib/cms-client";
|
|
2
|
-
import {
|
|
3
|
-
import { SMKING_ELEMENTS_IIFE } from "../lib/smking-elements-iife";
|
|
4
|
-
import type {
|
|
5
|
-
Block,
|
|
6
|
-
CmsParams,
|
|
7
|
-
ModuleHeader,
|
|
8
|
-
NavSearchIndexEntry,
|
|
9
|
-
NavSnapshotEntry,
|
|
10
|
-
} from "../types";
|
|
2
|
+
import type { CmsParams } from "../types";
|
|
11
3
|
|
|
12
4
|
/**
|
|
13
5
|
* Server Component that renders a published smking CMS page.
|
|
@@ -20,21 +12,22 @@ import type {
|
|
|
20
12
|
* return (
|
|
21
13
|
* <SmkingCms
|
|
22
14
|
* apiKey={process.env.SMKING_API_KEY!}
|
|
23
|
-
* slug="
|
|
15
|
+
* slug="" // root → /blog landing
|
|
24
16
|
* />
|
|
25
17
|
* );
|
|
26
18
|
* }
|
|
27
19
|
* ```
|
|
28
20
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
21
|
+
* **Raw passthrough mode** — echoes the full Plate-serialized bodyHtml
|
|
22
|
+
* exactly as the SaaS publish handler emits it (Tailwind utility
|
|
23
|
+
* classes + `slate-*` hook classes + `data-slate-*` attrs all
|
|
24
|
+
* intact). The component itself does NOT wrap the markup in an
|
|
25
|
+
* `<article>` shell or inject canonical CSS — what the dashboard
|
|
26
|
+
* Edit / Preview tabs render is what reaches the customer page. If
|
|
27
|
+
* your customer site uses Tailwind, the utility classes resolve
|
|
28
|
+
* natively; otherwise you can ship `cms-styles.css` (still exported
|
|
29
|
+
* from this package via `lib/smking-cms-styles`) alongside, which
|
|
30
|
+
* targets `.slate-*` hooks in plain CSS.
|
|
38
31
|
*
|
|
39
32
|
* Returns null when the response isn't ready (pending / not_found /
|
|
40
33
|
* unreachable / mis-configured) — fail-open by design.
|
|
@@ -46,40 +39,14 @@ export async function SmkingCms(props: CmsParams) {
|
|
|
46
39
|
const data = await getCmsPage(props);
|
|
47
40
|
if (!data || data.status !== "ready" || !data.page) return null;
|
|
48
41
|
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// also sets these still overrides ours where present.
|
|
42
|
+
// SEO head tags emit inline. React 19 hoists `<title>` and `<meta>`
|
|
43
|
+
// tags found anywhere in the tree into `<head>` automatically (last
|
|
44
|
+
// write wins on duplicates).
|
|
53
45
|
const seo = data.seo;
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
// Emit the Web Component registration `<script>` once when the page
|
|
57
|
-
// contains a `<smking-carousel>`. With bodyHtml we'd have to parse
|
|
58
|
-
// the HTML to detect; conservatively check blocks (still emitted for
|
|
59
|
-
// back-compat) or fall back to always emitting when bodyHtml is
|
|
60
|
-
// present (cheap — the IIFE is small + self-guarded).
|
|
61
|
-
const needsSmkingElements =
|
|
62
|
-
blocks.some((b) => b.component === "carousel") ||
|
|
63
|
-
(bodyHtml !== null && /<smking-carousel/i.test(bodyHtml));
|
|
64
|
-
// IIFE / CSS sources are hand-authored package constants (not user
|
|
65
|
-
// data) — passing them through __html here is the standard React
|
|
66
|
-
// server-side inline-script / inline-style emit pattern.
|
|
67
|
-
const elementsScript: { __html: string } = { __html: SMKING_ELEMENTS_IIFE };
|
|
68
|
-
const cmsStylesPayload: { __html: string } = { __html: SMKING_CMS_STYLES };
|
|
46
|
+
const bodyHtml = data.page.bodyHtml ?? "";
|
|
47
|
+
|
|
69
48
|
return (
|
|
70
49
|
<>
|
|
71
|
-
<style
|
|
72
|
-
data-smking="cms-styles"
|
|
73
|
-
// eslint-disable-next-line react/no-danger -- CSS source is a package constant generated from packages/shared/src/styles/cms-styles.css.
|
|
74
|
-
dangerouslySetInnerHTML={cmsStylesPayload}
|
|
75
|
-
/>
|
|
76
|
-
{needsSmkingElements && (
|
|
77
|
-
<script
|
|
78
|
-
data-smking="elements"
|
|
79
|
-
// eslint-disable-next-line react/no-danger -- IIFE source is a package constant.
|
|
80
|
-
dangerouslySetInnerHTML={elementsScript}
|
|
81
|
-
/>
|
|
82
|
-
)}
|
|
83
50
|
{seo?.title && <title data-smking="cms">{seo.title}</title>}
|
|
84
51
|
{seo?.metaDescription && (
|
|
85
52
|
<meta
|
|
@@ -105,362 +72,20 @@ export async function SmkingCms(props: CmsParams) {
|
|
|
105
72
|
<link rel="canonical" href={seo.canonicalUrl} data-smking="cms" />
|
|
106
73
|
)}
|
|
107
74
|
|
|
108
|
-
{bodyHtml
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
<article
|
|
117
|
-
className="smk-cms"
|
|
75
|
+
{/* Raw passthrough — bodyHtml is SaaS-emitted Plate output (Tailwind
|
|
76
|
+
utility + slate-* hooks + data-slate-* attrs all intact). No
|
|
77
|
+
article shell, no canonical CSS injection: what the dashboard
|
|
78
|
+
renders is what the customer page renders. Trust boundary:
|
|
79
|
+
schema-bounded Plate serialize on the SaaS side, same posture
|
|
80
|
+
as the previous wrapped version. */}
|
|
81
|
+
{bodyHtml ? (
|
|
82
|
+
<div
|
|
118
83
|
data-smking="cms"
|
|
119
84
|
data-content-type={data.page.contentType}
|
|
120
|
-
// eslint-disable-next-line react/no-danger --
|
|
85
|
+
// eslint-disable-next-line react/no-danger -- see trust note above.
|
|
121
86
|
dangerouslySetInnerHTML={{ __html: bodyHtml }}
|
|
122
87
|
/>
|
|
123
|
-
) :
|
|
124
|
-
// Legacy fallback — SaaS hasn't republished the page since the
|
|
125
|
-
// v0.16 upgrade, so bodyHtml is null. Fall back to the per-block
|
|
126
|
-
// dispatch path so customer pages keep rendering.
|
|
127
|
-
<article
|
|
128
|
-
className="smk-cms"
|
|
129
|
-
data-smking="cms"
|
|
130
|
-
data-content-type={data.page.contentType}
|
|
131
|
-
>
|
|
132
|
-
{data.page.title && (
|
|
133
|
-
<h1 className="smk-cms__title">{data.page.title}</h1>
|
|
134
|
-
)}
|
|
135
|
-
{blocks.map((block) => renderBlock(block))}
|
|
136
|
-
</article>
|
|
137
|
-
)}
|
|
88
|
+
) : null}
|
|
138
89
|
</>
|
|
139
90
|
);
|
|
140
91
|
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Dispatch a single block to its rendered React element.
|
|
144
|
-
*
|
|
145
|
-
* Trust boundary for `article` blocks: HTML is rendered server-side at
|
|
146
|
-
* write time by `@tiptap/html/server` inside the SaaS (Tiptap's renderer
|
|
147
|
-
* only emits HTML for known nodes per the configured schema, so author
|
|
148
|
-
* input can't smuggle arbitrary tags through). Customer site renders
|
|
149
|
-
* that HTML as-is via React's raw-HTML escape hatch. If customer wants
|
|
150
|
-
* defence-in-depth beyond the SaaS sanitisation tier, layer CSP on
|
|
151
|
-
* their host page (smking SDK does not ship a customer-side sanitiser
|
|
152
|
-
* to keep zero-dep customer install).
|
|
153
|
-
*/
|
|
154
|
-
function renderBlock(block: Block) {
|
|
155
|
-
switch (block.component) {
|
|
156
|
-
case "article": {
|
|
157
|
-
const articleHtml: { __html: string } = { __html: block.props.html };
|
|
158
|
-
return (
|
|
159
|
-
<div
|
|
160
|
-
key={block.id}
|
|
161
|
-
className="smk-block smk-block--article"
|
|
162
|
-
data-block-id={block.id}
|
|
163
|
-
// eslint-disable-next-line react/no-danger -- HTML pre-rendered server-side via @tiptap/html/server; trust boundary = SaaS schema. See function-level doc-comment above.
|
|
164
|
-
dangerouslySetInnerHTML={articleHtml}
|
|
165
|
-
/>
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
case "hero":
|
|
169
|
-
return (
|
|
170
|
-
<section
|
|
171
|
-
key={block.id}
|
|
172
|
-
className="smk-block smk-block--hero smk-hero"
|
|
173
|
-
data-block-id={block.id}
|
|
174
|
-
>
|
|
175
|
-
{block.props.image && (
|
|
176
|
-
<img
|
|
177
|
-
src={block.props.image.url}
|
|
178
|
-
alt={block.props.image.alt}
|
|
179
|
-
className="smk-hero__image"
|
|
180
|
-
/>
|
|
181
|
-
)}
|
|
182
|
-
<h2 className="smk-hero__title">{block.props.title}</h2>
|
|
183
|
-
{block.props.subtitle && (
|
|
184
|
-
<p className="smk-hero__subtitle">{block.props.subtitle}</p>
|
|
185
|
-
)}
|
|
186
|
-
{block.props.cta && (
|
|
187
|
-
<a className="smk-hero__cta" href={block.props.cta.href}>
|
|
188
|
-
{block.props.cta.label}
|
|
189
|
-
</a>
|
|
190
|
-
)}
|
|
191
|
-
</section>
|
|
192
|
-
);
|
|
193
|
-
case "carousel": {
|
|
194
|
-
// Canonical `<smking-carousel>` markup — the Web Component
|
|
195
|
-
// registered by `SMKING_ELEMENTS_IIFE` upgrades this into a
|
|
196
|
-
// swipe + nav + dots carousel on the client. Custom-element
|
|
197
|
-
// tags aren't in React's JSX intrinsic types by default; cast
|
|
198
|
-
// through a string ref so we don't need a global d.ts in the
|
|
199
|
-
// customer's package.
|
|
200
|
-
const CustomCarousel = "smking-carousel" as unknown as "section";
|
|
201
|
-
const CustomSlide = "smking-slide" as unknown as "div";
|
|
202
|
-
const aspect = block.props.aspectRatio ?? "16:9";
|
|
203
|
-
return (
|
|
204
|
-
<CustomCarousel
|
|
205
|
-
key={block.id}
|
|
206
|
-
className="smk-block smk-block--carousel smk-carousel"
|
|
207
|
-
data-block-id={block.id}
|
|
208
|
-
data-aspect={aspect}
|
|
209
|
-
>
|
|
210
|
-
<div className="smk-carousel__viewport">
|
|
211
|
-
{block.props.slides.map((slide, i) => (
|
|
212
|
-
<CustomSlide key={i}>
|
|
213
|
-
<div className="smk-carousel__image-frame">
|
|
214
|
-
{slide.imageUrl ? (
|
|
215
|
-
<img
|
|
216
|
-
src={slide.imageUrl}
|
|
217
|
-
alt=""
|
|
218
|
-
className="smk-carousel__image"
|
|
219
|
-
/>
|
|
220
|
-
) : (
|
|
221
|
-
<div className="smk-carousel__image smk-carousel__image--placeholder" />
|
|
222
|
-
)}
|
|
223
|
-
</div>
|
|
224
|
-
<div className="smk-carousel__caption-row">
|
|
225
|
-
<div className="smk-carousel__caption">{slide.caption}</div>
|
|
226
|
-
{slide.imageUrl ? (
|
|
227
|
-
<a
|
|
228
|
-
className="smk-carousel__download"
|
|
229
|
-
href={slide.imageUrl}
|
|
230
|
-
download
|
|
231
|
-
aria-label="Download image"
|
|
232
|
-
>
|
|
233
|
-
<svg
|
|
234
|
-
width="14"
|
|
235
|
-
height="14"
|
|
236
|
-
viewBox="0 0 24 24"
|
|
237
|
-
fill="none"
|
|
238
|
-
stroke="currentColor"
|
|
239
|
-
strokeWidth={2.4}
|
|
240
|
-
strokeLinecap="round"
|
|
241
|
-
strokeLinejoin="round"
|
|
242
|
-
aria-hidden
|
|
243
|
-
>
|
|
244
|
-
<path d="M12 5v14M5 12l7 7 7-7" />
|
|
245
|
-
</svg>
|
|
246
|
-
</a>
|
|
247
|
-
) : null}
|
|
248
|
-
</div>
|
|
249
|
-
</CustomSlide>
|
|
250
|
-
))}
|
|
251
|
-
</div>
|
|
252
|
-
<button
|
|
253
|
-
type="button"
|
|
254
|
-
className="smk-carousel__nav-prev"
|
|
255
|
-
aria-label="Previous slide"
|
|
256
|
-
>
|
|
257
|
-
<svg
|
|
258
|
-
width="16"
|
|
259
|
-
height="16"
|
|
260
|
-
viewBox="0 0 24 24"
|
|
261
|
-
fill="none"
|
|
262
|
-
stroke="currentColor"
|
|
263
|
-
strokeWidth={2.4}
|
|
264
|
-
strokeLinecap="round"
|
|
265
|
-
strokeLinejoin="round"
|
|
266
|
-
aria-hidden
|
|
267
|
-
>
|
|
268
|
-
<path d="M15 18l-6-6 6-6" />
|
|
269
|
-
</svg>
|
|
270
|
-
</button>
|
|
271
|
-
<button
|
|
272
|
-
type="button"
|
|
273
|
-
className="smk-carousel__nav-next"
|
|
274
|
-
aria-label="Next slide"
|
|
275
|
-
>
|
|
276
|
-
<svg
|
|
277
|
-
width="16"
|
|
278
|
-
height="16"
|
|
279
|
-
viewBox="0 0 24 24"
|
|
280
|
-
fill="none"
|
|
281
|
-
stroke="currentColor"
|
|
282
|
-
strokeWidth={2.4}
|
|
283
|
-
strokeLinecap="round"
|
|
284
|
-
strokeLinejoin="round"
|
|
285
|
-
aria-hidden
|
|
286
|
-
>
|
|
287
|
-
<path d="M9 6l6 6-6 6" />
|
|
288
|
-
</svg>
|
|
289
|
-
</button>
|
|
290
|
-
<div className="smk-carousel__dots" />
|
|
291
|
-
</CustomCarousel>
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
case "nav-recent-posts":
|
|
295
|
-
return renderNavList(block, "smk-nav-recent-posts");
|
|
296
|
-
case "nav-taxonomy-list":
|
|
297
|
-
return renderNavList(block, "smk-nav-taxonomy-list");
|
|
298
|
-
case "nav-search":
|
|
299
|
-
return renderNavSearch(block);
|
|
300
|
-
default:
|
|
301
|
-
// Forward-compat: unknown block component — emit empty marker
|
|
302
|
-
// so the page renders. Better than throwing.
|
|
303
|
-
return (
|
|
304
|
-
<section
|
|
305
|
-
key={(block as { id: string }).id}
|
|
306
|
-
className="smk-block smk-block--unknown"
|
|
307
|
-
data-block-component={(block as { component: string }).component}
|
|
308
|
-
/>
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// ── nav-* render helpers ─────────────────────────────────────────────
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Card-list render for `nav-recent-posts` + `nav-taxonomy-list`. Both
|
|
317
|
-
* blocks expose the same `{heading, viewAll, layout, snapshot}` shape;
|
|
318
|
-
* only the source of the snapshot differs (resolver-side concern, not
|
|
319
|
-
* SDK-side). The `layout` prop drives the wrapper's `data-layout` attr
|
|
320
|
-
* so customer CSS can style list / grid / carousel variants. Default
|
|
321
|
-
* styling ships sufficient base CSS for the list variant to be usable
|
|
322
|
-
* without customer overrides.
|
|
323
|
-
*
|
|
324
|
-
* Slugs in the snapshot are stored without leading slash (matching the
|
|
325
|
-
* `productContent.slug` DB column); we prepend one when forming hrefs.
|
|
326
|
-
* Customer's catch-all route at `/[...slug]` resolves them.
|
|
327
|
-
*/
|
|
328
|
-
function renderNavList(
|
|
329
|
-
block: Extract<
|
|
330
|
-
Block,
|
|
331
|
-
{ component: "nav-recent-posts" | "nav-taxonomy-list" }
|
|
332
|
-
>,
|
|
333
|
-
baseClass: string,
|
|
334
|
-
) {
|
|
335
|
-
const snapshot: NavSnapshotEntry[] = block.props.snapshot ?? [];
|
|
336
|
-
const layout = block.props.layout ?? "list";
|
|
337
|
-
const heading = (block.props as ModuleHeader).heading;
|
|
338
|
-
const viewAll = (block.props as ModuleHeader).viewAll;
|
|
339
|
-
|
|
340
|
-
return (
|
|
341
|
-
<section
|
|
342
|
-
key={block.id}
|
|
343
|
-
className={`smk-block smk-block--${block.component} smk-nav ${baseClass}`}
|
|
344
|
-
data-block-id={block.id}
|
|
345
|
-
data-block-component={block.component}
|
|
346
|
-
data-layout={layout}
|
|
347
|
-
>
|
|
348
|
-
{heading && <h2 className="smk-nav__heading">{heading}</h2>}
|
|
349
|
-
{snapshot.length === 0 ? (
|
|
350
|
-
<p className="smk-nav__empty">No posts yet.</p>
|
|
351
|
-
) : (
|
|
352
|
-
<ul className="smk-nav__list">
|
|
353
|
-
{snapshot.map((entry) => (
|
|
354
|
-
<li key={entry.slug} className="smk-nav__item">
|
|
355
|
-
<a href={`/${entry.slug}`} className="smk-nav__link">
|
|
356
|
-
{entry.featuredImageUrl && (
|
|
357
|
-
<img
|
|
358
|
-
src={entry.featuredImageUrl}
|
|
359
|
-
alt=""
|
|
360
|
-
className="smk-nav__thumb"
|
|
361
|
-
loading="lazy"
|
|
362
|
-
/>
|
|
363
|
-
)}
|
|
364
|
-
<div className="smk-nav__meta">
|
|
365
|
-
<h3 className="smk-nav__title">
|
|
366
|
-
{entry.title || entry.slug}
|
|
367
|
-
</h3>
|
|
368
|
-
{entry.excerpt && (
|
|
369
|
-
<p className="smk-nav__excerpt">{entry.excerpt}</p>
|
|
370
|
-
)}
|
|
371
|
-
{entry.publishedAt && (
|
|
372
|
-
<time
|
|
373
|
-
className="smk-nav__date"
|
|
374
|
-
dateTime={entry.publishedAt}
|
|
375
|
-
>
|
|
376
|
-
{formatPublishedAt(entry.publishedAt)}
|
|
377
|
-
</time>
|
|
378
|
-
)}
|
|
379
|
-
</div>
|
|
380
|
-
</a>
|
|
381
|
-
</li>
|
|
382
|
-
))}
|
|
383
|
-
</ul>
|
|
384
|
-
)}
|
|
385
|
-
{viewAll && (
|
|
386
|
-
<a className="smk-nav__view-all" href={viewAll.href}>
|
|
387
|
-
{viewAll.label}
|
|
388
|
-
</a>
|
|
389
|
-
)}
|
|
390
|
-
</section>
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* `nav-search` ships a server-rendered `<input>` + result `<ul>`, then
|
|
396
|
-
* an inline IIFE that wires keystroke → filter → list mutate. Zero
|
|
397
|
-
* client-component dependency on the customer side (no React hydration,
|
|
398
|
-
* no use client boundary). Index lives in DOM via `JSON.stringify` with
|
|
399
|
-
* `</` escape — prevents `</script>` smuggling.
|
|
400
|
-
*
|
|
401
|
-
* Match algorithm: case-insensitive substring against `title`, `excerpt`,
|
|
402
|
-
* and `slug`. Top 20 results shown. Customer can override the script by
|
|
403
|
-
* shipping their own `<SmkingSearch>` component if they want fancier
|
|
404
|
-
* matching (fuzzy, stemming, weighting).
|
|
405
|
-
*/
|
|
406
|
-
function renderNavSearch(
|
|
407
|
-
block: Extract<Block, { component: "nav-search" }>,
|
|
408
|
-
) {
|
|
409
|
-
const index: NavSearchIndexEntry[] = block.props.index ?? [];
|
|
410
|
-
const placeholder = block.props.placeholder || "Search…";
|
|
411
|
-
|
|
412
|
-
// Escape `</` so the JSON payload can't terminate the surrounding
|
|
413
|
-
// <script> tag — standard React-docs-recommended JSON-in-script
|
|
414
|
-
// hardening, since we serialize titles / excerpts that came from
|
|
415
|
-
// arbitrary author input.
|
|
416
|
-
const indexJson = JSON.stringify(index).replace(/</g, "\\u003c");
|
|
417
|
-
const scriptSrc =
|
|
418
|
-
"(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=" +
|
|
419
|
-
indexJson +
|
|
420
|
-
";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='smk-nav-search__result';var a=document.createElement('a');a.href='/'+x.slug;a.textContent=x.title||x.slug;li.appendChild(a);u.appendChild(li)})}i.addEventListener('input',up)})();";
|
|
421
|
-
|
|
422
|
-
return (
|
|
423
|
-
<section
|
|
424
|
-
key={block.id}
|
|
425
|
-
className="smk-block smk-block--nav-search smk-nav-search"
|
|
426
|
-
data-block-id={block.id}
|
|
427
|
-
>
|
|
428
|
-
<div className="smk-nav-search__shell">
|
|
429
|
-
<input
|
|
430
|
-
type="search"
|
|
431
|
-
data-smk-search-input
|
|
432
|
-
placeholder={placeholder}
|
|
433
|
-
className="smk-nav-search__input"
|
|
434
|
-
/>
|
|
435
|
-
<ul
|
|
436
|
-
data-smk-search-results
|
|
437
|
-
hidden
|
|
438
|
-
className="smk-nav-search__list"
|
|
439
|
-
/>
|
|
440
|
-
<p
|
|
441
|
-
data-smk-search-empty
|
|
442
|
-
hidden
|
|
443
|
-
className="smk-nav-search__empty"
|
|
444
|
-
>
|
|
445
|
-
No matches.
|
|
446
|
-
</p>
|
|
447
|
-
</div>
|
|
448
|
-
{/* eslint-disable-next-line react/no-danger -- inline IIFE wires search filter; index is JSON-escaped against </script> smuggling. */}
|
|
449
|
-
<script dangerouslySetInnerHTML={{ __html: scriptSrc }} />
|
|
450
|
-
</section>
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* Best-effort locale-naive date render. SDK doesn't ship a date library;
|
|
456
|
-
* customer can override via CSS / their own `<SmkingCms>` wrapper if
|
|
457
|
-
* they need a specific format / timezone.
|
|
458
|
-
*/
|
|
459
|
-
function formatPublishedAt(iso: string): string {
|
|
460
|
-
try {
|
|
461
|
-
return new Date(iso).toLocaleDateString();
|
|
462
|
-
} catch {
|
|
463
|
-
return iso.slice(0, 10);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time-mount runtime asset loader. Customers place a single
|
|
3
|
+
* `<SmkingRuntime />` in their root layout. Emits a `<link>` to the
|
|
4
|
+
* saas-side compiled CSS and a `<script async>` to the bundled Web
|
|
5
|
+
* Component IIFE. Both are aggressively browser-cached
|
|
6
|
+
* (stale-while-revalidate in production), so the cost amortises to
|
|
7
|
+
* effectively zero across all the page's `<SmkingCms>` instances.
|
|
8
|
+
*
|
|
9
|
+
* Why customer must mount it explicitly:
|
|
10
|
+
* `<SmkingCms>` stays in raw-passthrough mode (v0.16 commit 4bde390)
|
|
11
|
+
* — it doesn't decide for the customer whether to load runtime assets
|
|
12
|
+
* or where. Separating concerns means a customer can use the SDK to
|
|
13
|
+
* fetch a page's bodyHtml + render it inside their own design system,
|
|
14
|
+
* without our CSS bleeding in.
|
|
15
|
+
*/
|
|
16
|
+
export function SmkingRuntime({
|
|
17
|
+
baseUrl,
|
|
18
|
+
}: {
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
}) {
|
|
21
|
+
const url = (
|
|
22
|
+
baseUrl ?? process.env.SMKING_BASE_URL ?? "https://smking.app"
|
|
23
|
+
).replace(/\/$/, "");
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<link rel="stylesheet" href={`${url}/api/v1/public/runtime.css`} />
|
|
27
|
+
<script src={`${url}/api/v1/public/runtime.js`} async />
|
|
28
|
+
</>
|
|
29
|
+
);
|
|
30
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { SmkingAEO } from "./components/smking-aeo";
|
|
2
2
|
export { SmkingCms } from "./components/smking-cms";
|
|
3
|
+
export { SmkingRuntime } from "./components/smking-runtime";
|
|
3
4
|
export { getAeoContent } from "./lib/client";
|
|
4
5
|
export { getCmsPage } from "./lib/cms-client";
|
|
5
6
|
export type {
|
package/src/lib/cms-client.ts
CHANGED
|
@@ -61,10 +61,13 @@ export async function getCmsPage(
|
|
|
61
61
|
return null;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
// Root page is `slug=""` (empty string), customer mounts it at e.g.
|
|
65
|
+
// `/blog`. Only reject `undefined` / `null` — empty string is valid
|
|
66
|
+
// and routes to the site's root page row in `productContent`.
|
|
67
|
+
if (params.slug === undefined || params.slug === null) {
|
|
65
68
|
warnOnce(
|
|
66
69
|
"missing-slug",
|
|
67
|
-
"[@soloworks/smking-next/cms] slug prop is required.",
|
|
70
|
+
"[@soloworks/smking-next/cms] slug prop is required (use \"\" for the root page).",
|
|
68
71
|
);
|
|
69
72
|
return null;
|
|
70
73
|
}
|
|
@@ -1,704 +0,0 @@
|
|
|
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
|
-
`;
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* IIFE source for `<smking-carousel>` + `<smking-slide>` Web Component
|
|
3
|
-
* registration. The Next.js SDK inlines this as a `<script>` tag once
|
|
4
|
-
* per page so customer sites get the carousel interactivity without
|
|
5
|
-
* adding a `<script src>` to their layout.
|
|
6
|
-
*
|
|
7
|
-
* Idempotent: the IIFE guards each `customElements.define` call with a
|
|
8
|
-
* `customElements.get(...)` check, so multiple emits per page are
|
|
9
|
-
* harmless.
|
|
10
|
-
*
|
|
11
|
-
* Keep this string in sync with
|
|
12
|
-
* `apps/web/src/features/cms/sdk/smking-elements.ts` — the SaaS-side
|
|
13
|
-
* Plate editor imports the TS module directly, so both paths register
|
|
14
|
-
* the same runtime.
|
|
15
|
-
*/
|
|
16
|
-
export const SMKING_ELEMENTS_IIFE = `(function(){if(typeof window==="undefined"||typeof customElements==="undefined")return;
|
|
17
|
-
var C="smking-carousel",S="smking-slide";
|
|
18
|
-
if(customElements.get(C))return;
|
|
19
|
-
function nextFrame(cb){if(typeof requestAnimationFrame!=="undefined")return requestAnimationFrame(cb);return setTimeout(cb,16);}
|
|
20
|
-
function cancelFrame(id){if(typeof cancelAnimationFrame!=="undefined")return cancelAnimationFrame(id);return clearTimeout(id);}
|
|
21
|
-
function CarouselEl(){return Reflect.construct(HTMLElement,[],CarouselEl);}
|
|
22
|
-
CarouselEl.prototype=Object.create(HTMLElement.prototype);
|
|
23
|
-
CarouselEl.prototype.constructor=CarouselEl;
|
|
24
|
-
CarouselEl.prototype.connectedCallback=function(){var self=this;
|
|
25
|
-
var viewport=self.querySelector(".smk-carousel__viewport");
|
|
26
|
-
if(!viewport)return;
|
|
27
|
-
var dotsContainer=self.querySelector(".smk-carousel__dots");
|
|
28
|
-
var prevBtn=self.querySelector(".smk-carousel__nav-prev");
|
|
29
|
-
var nextBtn=self.querySelector(".smk-carousel__nav-next");
|
|
30
|
-
var slides=Array.prototype.slice.call(viewport.querySelectorAll(":scope > "+S));
|
|
31
|
-
if(dotsContainer){var dotEls=slides.map(function(_,i){var d=document.createElement("button");d.type="button";d.className="smk-carousel__dot";d.dataset.index=String(i);d.setAttribute("aria-label","Go to slide "+(i+1));if(i===0)d.setAttribute("aria-current","true");d.addEventListener("click",function(e){e.preventDefault();viewport.scrollTo({left:i*viewport.clientWidth,behavior:"smooth"});});return d;});dotsContainer.replaceChildren.apply(dotsContainer,dotEls);}
|
|
32
|
-
function onPrev(e){e.preventDefault();viewport.scrollBy({left:-viewport.clientWidth,behavior:"smooth"});}
|
|
33
|
-
function onNext(e){e.preventDefault();viewport.scrollBy({left:viewport.clientWidth,behavior:"smooth"});}
|
|
34
|
-
if(prevBtn)prevBtn.addEventListener("click",onPrev);
|
|
35
|
-
if(nextBtn)nextBtn.addEventListener("click",onNext);
|
|
36
|
-
var raf=0;
|
|
37
|
-
function onScroll(){if(raf)return;raf=nextFrame(function(){raf=0;var ai=Math.round(viewport.scrollLeft/Math.max(1,viewport.clientWidth));if(!dotsContainer)return;var dots=dotsContainer.querySelectorAll(".smk-carousel__dot");dots.forEach(function(d,i){if(i===ai)d.setAttribute("aria-current","true");else d.removeAttribute("aria-current");});});}
|
|
38
|
-
viewport.addEventListener("scroll",onScroll,{passive:true});
|
|
39
|
-
self._smkCleanup=function(){if(prevBtn)prevBtn.removeEventListener("click",onPrev);if(nextBtn)nextBtn.removeEventListener("click",onNext);viewport.removeEventListener("scroll",onScroll);if(raf)cancelFrame(raf);};};
|
|
40
|
-
CarouselEl.prototype.disconnectedCallback=function(){if(this._smkCleanup)this._smkCleanup();};
|
|
41
|
-
function SlideEl(){return Reflect.construct(HTMLElement,[],SlideEl);}
|
|
42
|
-
SlideEl.prototype=Object.create(HTMLElement.prototype);
|
|
43
|
-
SlideEl.prototype.constructor=SlideEl;
|
|
44
|
-
customElements.define(C,CarouselEl);
|
|
45
|
-
customElements.define(S,SlideEl);
|
|
46
|
-
})();`;
|