create-kywi-app 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -76,7 +76,7 @@ the full proposal — ten minutes of sign-off beats re-entering fifty nodes.
76
76
  | Hardcode a list (testimonials, logos, posts, products, team) | Folder of nodes + a Feed + the Feed Display module (§1) |
77
77
  | Create one-off pages with identical field shapes | A custom content type + one node per item (§2) |
78
78
  | Filter/group content by topic, industry, audience | Categories & tags, queried by feeds (§3) |
79
- | Copy-paste a CTA/banner/snippet across pages | Save it as a reusable component (§4) |
79
+ | Copy-paste a CTA/banner/snippet across pages | Save it as a reusable component — linked, so one edit updates them all (§4) |
80
80
  | Write page sections as JSX | Build them as layout sections/modules the owner can edit (§5) |
81
81
  | Build a page as one column of full-width headings and paragraphs | Compose: section bands, columns, object modules (§5) |
82
82
  | Put a custom module's editable copy in a `json` prop | One `text`/`textarea`/`richText` prop per field the owner edits (§5) |
@@ -179,14 +179,99 @@ Anything designed once and used on multiple pages — CTA bands, promo banners,
179
179
  newsletter signup blocks, campaign snippets, "as seen in" strips — should be a
180
180
  **reusable component**, not copy-pasted sections.
181
181
 
182
- - In the layout editor, select the module and **Save as Component**; insert it
183
- elsewhere from the component picker. Placed instances are **live
184
- references**: editing the source component updates every page that uses it.
185
- - **Detach** an instance when a page genuinely needs a one-off variant it
186
- becomes an independent copy from that point on.
187
- - Rule of thumb: the second time you paste the same section, stop and make it
188
- a component. The owner should be able to update the sitewide CTA in one
189
- place, ten minutes before their webinar.
182
+ A component is a **live source**, not a snapshot. Every page that places one
183
+ holds a *link* to it (a `componentId` on the node), and the link is resolved on
184
+ every request so writing a new definition changes every page that uses it
185
+ immediately, published pages included, with nothing to re-publish. That is the
186
+ whole point, and it is also the thing to be careful with.
187
+
188
+ **What can be one.** A single module, a whole section (band), or a personalized /
189
+ A-B tested band with its variants intact — the three `type` values are `module`,
190
+ `section` and `variantContainer`. A band's variants travel with it; which arm a
191
+ visitor sees is still decided per request, per placement, so the same personalized
192
+ component can sit on five pages and target correctly on each.
193
+
194
+ **In the editors** (front-of-site overlay or the admin Layout tab): select the
195
+ module, section or band → **Save as component** → name it. The selection is
196
+ replaced in place by a linked instance, marked with an outline and a pill naming
197
+ the component. Place it again from the section picker's *Components* rows.
198
+
199
+ **Editing a linked instance ends in a question.** There are no per-prop overrides
200
+ in v1: an instance renders its component exactly. So when you edit one and save,
201
+ Kywi asks, naming the blast radius ("updates 3 instances across 2 pages, including
202
+ published"):
203
+
204
+ - **Save and update all instances** — the edit becomes the component. Every
205
+ instance everywhere follows it, live.
206
+ - **Save to this page only** — this instance *detaches*: it keeps the edit as its
207
+ own independent content and stops following the component. Everything else is
208
+ untouched.
209
+
210
+ **Rule of thumb:** the second time you paste the same section, stop and make it a
211
+ component. The owner should be able to update the sitewide CTA in one place, ten
212
+ minutes before their webinar.
213
+
214
+ **Managing them:** *Components* in the admin nav is the library — every component
215
+ with the number of pages using it, rename, an editing canvas for the definition,
216
+ and delete. Deleting **detaches every instance first**: each page keeps rendering
217
+ exactly what it rendered, as its own copy, and stops following.
218
+
219
+ ### Components over the API
220
+
221
+ The admin UI is one client; the endpoints are the contract. `siteId` is optional
222
+ and defaults to the resolved site — components are **per-site** in v1, and
223
+ per-site authorization applies.
224
+
225
+ 1. `POST /api/v1/components` — `{ name, type, definition, moduleType?, description? }`.
226
+ The `definition` is the node itself: a `section` component stores the whole
227
+ `LayoutSection`, a `variantContainer` the whole container, a `module` component
228
+ stores `{ props, style }` and names its `moduleType`.
229
+ 2. **Place it** by writing a layout that carries the link. There is no separate
230
+ "insert" endpoint: `PUT /api/v1/content/:type/:id/layout` with the definition's
231
+ own shape plus `componentId` on the node, giving the placement a fresh `id` of
232
+ its own. The node's inline content is the *fallback copy* — what renders if the
233
+ component ever stops resolving — so write the definition's content there, not a
234
+ placeholder.
235
+
236
+ **An `ab_test` placement must mint its own `experimentId`.** A
237
+ `variantContainer` definition normally carries an `experimentId` — the one the
238
+ band had on the page it was saved from — because `mode: 'ab_test'` without one
239
+ is invalid, so it cannot simply be left out of the definition. A placement that
240
+ names no binding of its own **inherits the definition's**. Place that component
241
+ on three pages and all three arms report into a *single* experiment run; a
242
+ `winnerId` that travels the same way locks every placement to a finished test,
243
+ permanently serving one arm on pages nobody tested. So when you write an
244
+ `ab_test` placement: set a **fresh `experimentId`** on the placement node (any
245
+ uuid — one run per placement is the whole model) and **never copy `winnerId`**.
246
+ The editors do exactly this (`buildConnectedVariantContainerNode`), and a
247
+ definition saved from an editor carries no `winnerId` at all; a definition
248
+ POSTed straight over the API can, so read what you are placing. A
249
+ `personalization` container has no binding and needs none.
250
+ 3. `GET /api/v1/components/:id/usage` — where it is placed (`instances`, `pages`,
251
+ the page ids and node ids, and trashed pages counted separately). This is the
252
+ blast radius; read it *before* changing a definition.
253
+ 4. `PUT /api/v1/components/:id` — a body with `definition` **is the propagation
254
+ write**: every linked instance renders the new definition on its next request.
255
+ A body with only `name` / `description` is a rename and changes nothing any page
256
+ renders.
257
+ 5. `POST /api/v1/components/:id/detach-all` — materialize the definition into every
258
+ page that links it and drop the links; each page keeps rendering identically.
259
+ 6. `DELETE /api/v1/components/:id` — refuses (409) while instances exist; add
260
+ `?detachAll=1` to detach-then-delete. `GET /api/v1/components?withUsage=1` folds
261
+ the counts into a list, in one read.
262
+
263
+ **Two refusals to design around.** A component definition may not link to another
264
+ component (one level only, deliberately: a nested link is invisible to usage counts
265
+ and the delete gate, so the editors *materialize* an inner component when you save
266
+ a section that contains one — the inner instance stops following its own source, and
267
+ the dialog says so). And a stored layout that a live layout could not hold is
268
+ rejected at the door, because detaching would copy it into every page that links it.
269
+
270
+ **Component writes bypass approvals in v1.** They are not routed through
271
+ changesets or the review workflow — a `PUT` with a definition is live on publish
272
+ pages the moment it returns. On a site with an editorial workflow, treat a
273
+ definition write as a publish, not a draft: no draft, schedule or review step
274
+ holds it back. Changeset integration is a follow-up.
190
275
 
191
276
  ## 5. Page layouts & modules — not hardcoded JSX
192
277
 
@@ -441,20 +526,22 @@ interchangeable.
441
526
  page is complete with JavaScript off. This is the default for anything the
442
527
  server can know — rule-based audiences (UTM, referrer, the `kywi_audience`
443
528
  pin) and A/B experiments. Since 0.6.3 it also renders in the layout editor as
444
- a badged block with an **arm switcher**, each arm editable with the ordinary
445
- section/column/module tools, so server-resolution no longer costs the owner
446
- their editing surface. Since kywi-cms#119, the owner doesn't need an agent to
447
- hand-author this shape at all: the section chrome in the admin layout
448
- editor and in the in-place front-of-site overlay alike offers
449
- **Personalize this section** / **A/B test** actions that wrap an ordinary
450
- section into a section-level variant container on the spot, seeding the
451
- default arm from the section's current content and opening straight into its
452
- config (audience/experiment picker included). An agent only needs to reach
453
- for `update_layout` when scripting bulk changes or building a container the
454
- UI can't reach.
529
+ a badged block with a **variant switcher** (internal editor code
530
+ `activeArms`, the `VariantArm` type, `data-arm-id` still says "arm"; the
531
+ persisted layout field is `variants`), each variant editable with the
532
+ ordinary section/column/module tools, so server-resolution no longer costs
533
+ the owner their editing surface. Since kywi-cms#119, the owner doesn't need
534
+ an agent to hand-author this shape at all: the section chrome in the
535
+ admin layout editor and in the in-place front-of-site overlay alike
536
+ offers **Personalize this section** / **A/B test** actions that wrap an
537
+ ordinary section into a section-level variant container on the spot,
538
+ seeding the default variant from the section's current content and
539
+ opening straight into its config (audience/experiment picker included).
540
+ An agent only needs to reach for `update_layout` when scripting bulk
541
+ changes or building a container the UI can't reach.
455
542
  - **Module-level** — the `variantContainer` *module*, placed in a column; its
456
- arms are HTML strings (`defaultContent`, `variants: [{audienceId, label,
457
- content}]`). Every arm ships in the HTML (default visible, the rest
543
+ variants are HTML strings (`defaultContent`, `variants: [{audienceId, label,
544
+ content}]`). Every variant ships in the HTML (default visible, the rest
458
545
  `display:none`) and the browser runtime reveals the matching one. Use it
459
546
  **only for signals the server cannot read at first paint** — in practice, a
460
547
  self-ID answer held in `localStorage`.
@@ -586,7 +673,8 @@ early, so ask at scoping time.
586
673
  - Creating types and entering content before the owner signed off on a
587
674
  content model.
588
675
  - A hardcoded array of testimonials/logos/posts in a page component.
589
- - The same CTA JSX pasted on four pages.
676
+ - The same CTA JSX pasted on four pages — or the same section duplicated on four
677
+ pages in the editor, which the owner then has to update four times (§4).
590
678
  - A `<form>` that POSTs to a hand-rolled route (or nowhere).
591
679
  - Marketing imagery in `/public`.
592
680
  - A "blog" that is a folder of `.mdx` files the owner can't edit.
@@ -44,16 +44,18 @@ is progressive enhancement; anonymous visitors get the default).
44
44
  ### 4b. Experiment path
45
45
 
46
46
  Admin → Experiments: create the experiment. In the layout, add an A/B
47
- container with an arm per treatment. Assignment is deterministic per visitor
48
- (the scaffold's middleware issues a persistent `kywi_visitor` cookie), so a
49
- visitor sees the same arm on every visit no client runtime needed.
47
+ container with a variant per treatment (the editor UI and API both call these
48
+ "variants"; the underlying data model calls each one an "arm" — same thing).
49
+ Assignment is deterministic per visitor (the scaffold's middleware issues a
50
+ persistent `kywi_visitor` cookie), so a visitor sees the same variant on every
51
+ visit — no client runtime needed.
50
52
 
51
53
  ### 5. Verify before calling it done
52
54
 
53
55
  - **Preview tokens** (Audiences → Preview Tokens): view the page *as each
54
56
  audience* and confirm the right variant serves.
55
57
  - Experiments: two fresh browser profiles should get (possibly) different
56
- arms, and each profile must get the **same** arm on reload.
58
+ variants, and each profile must get the **same** variant on reload.
57
59
  - Confirm exposures are being recorded in the Experiments admin.
58
60
  - Load the page as a plain anonymous visitor: the default must be complete.
59
61
 
package/lib/templates.mjs CHANGED
@@ -54,7 +54,11 @@ function packageJson(a) {
54
54
  dev: 'next dev',
55
55
  build: 'next build',
56
56
  start: 'next start',
57
- migrate: 'kywi migrate --push',
57
+ // Plain `kywi migrate` (not --push): the CLI bootstraps a migration
58
+ // journal via `drizzle-kit generate` on first run and grows it on
59
+ // every schema-bearing release after (kywi-cms#141) — a fresh
60
+ // scaffold is journal-managed from its very first `pnpm migrate`.
61
+ migrate: 'kywi migrate',
58
62
  seed: 'kywi seed',
59
63
  },
60
64
  dependencies: {
@@ -249,6 +253,34 @@ function tsconfig() {
249
253
  ) + '\n'
250
254
  }
251
255
 
256
+ // ── types/css.d.ts ────────────────────────────────────────────────────────────
257
+
258
+ /**
259
+ * Ambient declaration for stylesheet imports.
260
+ *
261
+ * WHY THIS FILE EXISTS (found by the scaffold compile gate, kywi-cms#147 T6).
262
+ * `app/(site)/kywi-front-edit.tsx` pulls the admin design system in LAZILY —
263
+ * `await import('@kywi-software/core/admin/styles.css')` inside `next/dynamic`'s
264
+ * factory — so the ~4.7k-line stylesheet never enters the public browse bundle.
265
+ * TypeScript treats a *dynamic* import of a non-code file as an ordinary module
266
+ * import and reports TS2307 for it, unlike a top-level `import 'x.css'` statement,
267
+ * which it lets through as a resource import. Next type-checks during `next build`,
268
+ * so without this declaration a freshly scaffolded project failed its own build on
269
+ * generated code — which nothing caught, because the reference app imports that
270
+ * stylesheet in statement form and the scaffold's own output had never been
271
+ * type-checked at all.
272
+ *
273
+ * `*.module.css` is deliberately NOT declared here: Next's own types declare it
274
+ * with a real `classes` record, and the more specific pattern wins.
275
+ */
276
+ function cssTypes() {
277
+ return `// Stylesheet imports (including the lazy \`await import('…/styles.css')\` in
278
+ // app/(site)/kywi-front-edit.tsx, which TypeScript checks like any other module).
279
+ // CSS Modules are typed by Next itself — the more specific pattern wins.
280
+ declare module '*.css'
281
+ `
282
+ }
283
+
252
284
  // ── next.config.mjs ───────────────────────────────────────────────────────────
253
285
 
254
286
  function nextConfig() {
@@ -408,16 +440,19 @@ function libSite() {
408
440
  import type {
409
441
  LayoutDocument,
410
442
  RegionNode,
411
- ModuleNode,
443
+ VariantContainer,
412
444
  FeedItemsResolver,
413
- ComponentResolver,
414
- SectionComponentResolver,
415
445
  PersonalizationState,
416
446
  } from '@kywi-software/core/layout'
417
- import { isLayoutSection, applyPageVariant } from '@kywi-software/core/layout'
447
+ import {
448
+ isLayoutSection,
449
+ collectComponentRefs,
450
+ componentDetachSource,
451
+ resolveComponentPlacements,
452
+ applyPageVariant,
453
+ } from '@kywi-software/core/layout'
418
454
  import { resolveContentByPath, normalizePath } from '@kywi-software/core/nav'
419
455
  import { getFeedBySlug, getComponentById, resolveLocaleFromRequest } from '@kywi-software/core'
420
- import { resolveComponentDefinition, resolveSectionComponentDefinition } from '@kywi-software/core/admin/server'
421
456
  import {
422
457
  evaluateActiveAudiences,
423
458
  getSelfIdWidgetConfig,
@@ -581,77 +616,48 @@ export function buildFeedResolver(runtime: KywiRuntime): FeedItemsResolver {
581
616
  }
582
617
  }
583
618
 
584
- // ─── Component resolver (#46) ────────────────────────────────────────────────
585
-
586
- function* moduleNodesInRegions(regions: Record<string, RegionNode[]>): Generator<ModuleNode> {
587
- for (const nodes of Object.values(regions)) {
588
- for (const node of nodes) {
589
- if (isLayoutSection(node)) {
590
- for (const col of node.columns) for (const m of col.nodes) yield m
591
- } else {
592
- for (const s of node.defaultSections) for (const col of s.columns) for (const m of col.nodes) yield m
593
- for (const v of node.variants) for (const s of v.sections) for (const col of s.columns) for (const m of col.nodes) yield m
594
- }
595
- }
596
- }
597
- }
619
+ // ─── Linked components: server-side pre-resolution (#46, #69, #147) ──────────
598
620
 
599
- /**
600
- * Prefetch every connected-component (\`componentId\`) referenced by the layout and
601
- * return a synchronous {@link ComponentResolver} over that snapshot — the
602
- * renderer resolves components during render, so they must be resolved up front.
603
- * Returns undefined when the layout references no components (skip the prop).
604
- */
605
- export async function buildComponentResolver(
606
- layout: LayoutDocument,
607
- runtime: KywiRuntime,
608
- ): Promise<ComponentResolver | undefined> {
609
- const ids = new Set<string>()
610
- for (const node of moduleNodesInRegions(layout.regions)) {
611
- if (node.componentId) ids.add(node.componentId)
612
- }
613
- if (ids.size === 0) return undefined
621
+ /** Fetch a set of component rows once and index them by id. */
622
+ async function loadComponents(ids: string[], runtime: KywiRuntime) {
614
623
  const { db, siteId } = runtime
615
624
  const entries = await Promise.all(
616
- [...ids].map(async (id) => [id, await getComponentById(db, id, siteId)] as const),
625
+ ids.map(async (id) => [id, await getComponentById(db, id, siteId)] as const),
617
626
  )
618
- const map = new Map(entries)
619
- return (componentId) => resolveComponentDefinition(map.get(componentId) ?? null)
620
- }
621
-
622
- /** Yield every section-level component reference (\`section.componentId\`) in the
623
- * layout, descending into variantContainer default + variant sections (#69). */
624
- function* sectionComponentIds(regions: Record<string, RegionNode[]>): Generator<string> {
625
- for (const nodes of Object.values(regions)) {
626
- for (const node of nodes) {
627
- if (isLayoutSection(node)) {
628
- if (node.componentId) yield node.componentId
629
- } else {
630
- for (const s of node.defaultSections) if (s.componentId) yield s.componentId
631
- for (const v of node.variants) for (const s of v.sections) if (s.componentId) yield s.componentId
632
- }
633
- }
634
- }
627
+ return new Map(entries)
635
628
  }
636
629
 
637
630
  /**
638
- * Prefetch every connected *section* component (\`section.componentId\`) referenced
639
- * by the layout and return a synchronous {@link SectionComponentResolver} over the
640
- * snapshot — parallel to {@link buildComponentResolver} but for reusable sections
641
- * (#69). Returns undefined when the layout references none (skip the prop).
631
+ * Resolve every linked component (\`componentId\`) the layout places, ON THE
632
+ * SERVER, before the document reaches the renderer.
633
+ *
634
+ * This is the ONLY way a Server Component can render linked components.
635
+ * \`KywiLayout\` is a client component and its \`componentResolver\` /
636
+ * \`sectionComponentResolver\` / \`variantContainerComponentResolver\` props are
637
+ * FUNCTIONS: passing them from here throws "Functions cannot be passed directly
638
+ * to Client Components" and the page 500s. Pre-resolution hands the renderer a
639
+ * plain document instead — only data crosses the boundary — and produces exactly
640
+ * the markup the resolvers were meant to produce, \`data-component-id\` included.
641
+ *
642
+ * \`collectComponentRefs\` is the engine's own link walker, the same one the
643
+ * instance index and detach-all use, so this never falls behind on where a link
644
+ * may live. It also knows that a connected section's inline columns are fallback
645
+ * copy rather than placements, so links buried in one are not fetched — they
646
+ * only render when that section's own component has gone missing.
647
+ *
648
+ * Definitions are read per request with no cache, so editing a component is live
649
+ * on the next page view.
642
650
  */
643
- export async function buildSectionComponentResolver(
651
+ export async function resolveLayoutComponents(
644
652
  layout: LayoutDocument,
645
653
  runtime: KywiRuntime,
646
- ): Promise<SectionComponentResolver | undefined> {
647
- const ids = new Set<string>(sectionComponentIds(layout.regions))
648
- if (ids.size === 0) return undefined
649
- const { db, siteId } = runtime
650
- const entries = await Promise.all(
651
- [...ids].map(async (id) => [id, await getComponentById(db, id, siteId)] as const),
654
+ ): Promise<LayoutDocument> {
655
+ const refs = collectComponentRefs(layout)
656
+ if (refs.length === 0) return layout
657
+ const map = await loadComponents([...new Set(refs.map((ref) => ref.componentId))], runtime)
658
+ return resolveComponentPlacements(layout, (componentId) =>
659
+ componentDetachSource(map.get(componentId) ?? null),
652
660
  )
653
- const map = new Map(entries)
654
- return (componentId) => resolveSectionComponentDefinition(map.get(componentId) ?? null)
655
661
  }
656
662
 
657
663
  // ─── Personalization + experiments (#50) ─────────────────────────────────────
@@ -716,7 +722,7 @@ export async function resolvePersonalization(
716
722
  }
717
723
  }
718
724
 
719
- function* variantContainersInRegions(regions: Record<string, RegionNode[]>): Generator<RegionNode> {
725
+ function* variantContainersInRegions(regions: Record<string, RegionNode[]>): Generator<VariantContainer> {
720
726
  for (const nodes of Object.values(regions)) {
721
727
  for (const node of nodes) {
722
728
  if (!isLayoutSection(node)) yield node
@@ -738,10 +744,17 @@ async function resolveExperimentAssignments(
738
744
  visitorId: string,
739
745
  ): Promise<Record<string, string | null>> {
740
746
  if (!layout) return {}
741
- const resolved = applyPageVariant(layout, audienceId)
747
+ // Pre-resolve FIRST, then read the containers. A container component saved
748
+ // with \`mode: 'ab_test'\` keeps its experiment in the DEFINITION; a placement
749
+ // that carries only a \`componentId\` would otherwise look like a plain
750
+ // container to this pass, no assignment would be made, and every visitor would
751
+ // see the Default arm of a live experiment. After pre-resolution there are no
752
+ // unresolved placements left to special-case — the containers here are the
753
+ // containers that render.
754
+ const resolved = await resolveLayoutComponents(applyPageVariant(layout, audienceId), runtime)
742
755
  const experimentIds = new Set<string>()
743
756
  for (const node of variantContainersInRegions(resolved.regions)) {
744
- if (!isLayoutSection(node) && node.mode === 'ab_test' && !node.winnerId && node.experimentId) {
757
+ if (node.mode === 'ab_test' && !node.winnerId && node.experimentId) {
745
758
  experimentIds.add(node.experimentId)
746
759
  }
747
760
  }
@@ -1346,8 +1359,7 @@ import {
1346
1359
  clientRuntimeEnabled,
1347
1360
  transparencyNoticeEnabled,
1348
1361
  buildFeedResolver,
1349
- buildComponentResolver,
1350
- buildSectionComponentResolver,
1362
+ resolveLayoutComponents,
1351
1363
  localeAlternates,
1352
1364
  mediaUrl,
1353
1365
  requestBaseUrl,
@@ -1499,12 +1511,14 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
1499
1511
  // (menuSlug → a real menu; the tree otherwise) — the same seam as feeds,
1500
1512
  // so a nav module dropped into any region/section just works (#112).
1501
1513
  const currentPath = String(node['path'] ?? '/')
1502
- const hydrated = await hydrateLayoutNav(
1514
+ const navHydrated = await hydrateLayoutNav(
1503
1515
  feedsHydrated,
1504
1516
  runtime.scope.menus.createHydrationResolver(runtime.siteId, currentPath),
1505
1517
  )
1506
- const componentResolver = await buildComponentResolver(hydrated, runtime)
1507
- const sectionComponentResolver = await buildSectionComponentResolver(hydrated, runtime)
1518
+ // Linked components resolve HERE, on the server, into the document itself:
1519
+ // KywiLayout is a client component and the renderer's resolver props are
1520
+ // functions, which cannot cross the RSC boundary (#147).
1521
+ const hydrated = await resolveLayoutComponents(navHydrated, runtime)
1508
1522
  content = (
1509
1523
  <article className="page page--layout" data-kywi-content-id={contentId}>
1510
1524
  {head}
@@ -1512,8 +1526,6 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
1512
1526
  layout={hydrated}
1513
1527
  personalization={perso.personalization}
1514
1528
  moduleComponents={moduleComponents}
1515
- {...(componentResolver ? { componentResolver } : {})}
1516
- {...(sectionComponentResolver ? { sectionComponentResolver } : {})}
1517
1529
  >
1518
1530
  {Object.keys(hydrated.regions).map((name) => (
1519
1531
  <KywiRegion key={name} name={name} />
@@ -2694,6 +2706,9 @@ export function buildFileSet(answers) {
2694
2706
  'package.json': packageJson(answers),
2695
2707
  'kywi.config.ts': kywiConfig(answers),
2696
2708
  'tsconfig.json': tsconfig(),
2709
+ // Ambient module declarations the generated code needs in order to type-check
2710
+ // (see `cssTypes`). Every mode: the admin catch-all imports the stylesheet too.
2711
+ 'types/css.d.ts': cssTypes(),
2697
2712
  'next.config.mjs': nextConfig(),
2698
2713
  '.env.example': envExample(),
2699
2714
  '.gitignore': gitignore(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kywi-app",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Scaffold a new Kywi CMS project — npx create-kywi-app my-site",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Kywi-Software/kywi-cms#readme",