Docs

This page isn't translated yet

v1.6.0

The Payload admin on a diet (a 30-section page drops from 83 MB to 19.4 MB), plus a multi-pack icon picker, tier-aware blocks, cookie consent and `systhema doctor`.

On this page

✨ HighlightsLink to this section

This release puts the Payload admin on a diet. Three changes work together to cut the warm /admin/collections/pages/[id] response on a 30-section synthetic page from 83 MB → 19.4 MB (−77%) and the warm TTFB from ~1.7 s → ~360 ms (−79%):

  1. A targeted pnpm patch against @payloadcms/richtext-lexical that memoizes initLexicalFeatures per request and runs a content-keyed dedup pass on every featureClientSchemaMap.
  2. A lazy-mount shell for region / fragment / slot Lexical editors so nested editors below the fold don't pay their hydration cost until they scroll into view.
  3. The public site no longer pulls the admin import map, the admin-bar CSS, or the live-preview CSS into anonymous-visitor bundles — public homepage drops from 27 → 12 chunks and 5.10 MB → 2.27 MB.

Alongside the perf work: a multi-pack icon picker (Font Awesome + Material Symbols + custom packs), a new .systhema/artifacts/ directory that survives pnpm install, a generate-references CLI command that emits TOON files for agents, a top-layer @layer app cascade slot for project-specific overrides, and a homepage picker in General Settings that promotes any page to / with automatic link rewrites and a 308 redirect from its own slug.

The biggest structural change is tier-aware blocks: the old root*/region*/fragment*/nested*/*InStack block-slug proliferation collapses into one canonical block per concept whose fields gate themselves by editor depth — a breaking slug rename that systhema upgrade migrates automatically (stored Lexical content rewritten in place, no manual step). Plus a real figure-breakout CSS contract so container/screen-width media escapes narrow column tracks, with sub-track left/center/right alignment.

Multi-pack Icon Picker (FA + Material Symbols + Custom)Link to this section

The PayloadCMS icon picker gains Google Material Symbols as a first-class bundled pack alongside Font Awesome, plus an escape hatch for custom SVG packs. The admin editor gets a tab-based picker with smarter tokenized search, and each block type (Button, Chip) can ship a default icon for newly created instances.

Zero icon-pack code leaks into the frontend bundle — packs are loaded dynamically in admin only, and the public render is fully server-side.

withSysthema(baseConfig, {
  // Everything optional — defaults give you FA Solid + Brands
  // and Material Symbols Outlined 400 without writing any `icons` config.
  icons: {
    fontAwesome: { styles: ['solid', 'regular', 'brands'] },
    materialSymbols: { style: 'rounded', weight: 300, fill: true },
    custom: [
      { id: 'my-pack', label: 'Company Icons', icons: { myLogo: '<svg…></svg>' } },
    ],
  },
  button: { defaultIconAfter: 'fa-solid:faArrowRight' },
  chip:   { defaultIconBefore: 'material-symbols:tag' },
})

Per-instance pack overrides on iconPickerField:

iconPickerField({
  name: 'icon',
  packs: {
    fontAwesome: false,
    materialSymbols: { weight: 200 },
    custom: [{ id: 'company', label: 'Company', icons: { logo: '<svg…/>' } }],
  },
})

Material Symbols variant swaps propagate automatically. Picks are stored as identifier-only ({"id":"material-symbols:arrow_forward"}); the render path looks up the SVG on every render from the active variant's digest. Change weight: 400 → 200 in payload.config.ts, redeploy, every MS icon swaps. All 7 weights × 3 styles × 2 fills (42 digests) ship in dist/.

The Material Symbols Grade axis (-25 / 0 / 200) is not supported — the static SVG packages only expose style + fill. See docs/payload/editor/icons.md FAQ.

Full reference: docs/payload/editor/icons.md.

A consent-scanner + Payload-controlled cookie banner ships as an opt-in feature. Marketing copy and toggles live in generalSettings (new Payload global section); the scanner classifies cookies into Necessary / Functional / Statistics / Marketing and respects the user's choice. The add-cookie-consent-to-next-layout codemod (run by systhema upgrade) inserts the provider into existing projects' root layout.

Tier-aware blocks — one definition per concept, fields gated by depth (#74)Link to this section

Media and rich-content blocks no longer ship a separate definition per editor depth. A single canonical block (image, video, youtube, googleMap, embed, card, columns, carousel, form) now carries every tier's fields, gated by a reactive _tier field so the admin form hides what doesn't apply at the current depth, and the JSX converters branch on _tier to render correctly everywhere:

TierWidth radioWidth type/valueSub-track alignmentSection-style (padding/theme/layoutBg)Stack-layout
root✅✅✅✅ (columns only)❌
region✅✅✅❌❌
fragment❌❌❌❌❌
stackmedia size only✅ (image only)❌❌✅

_tier is owned end-to-end by the server: an injectTier walker runs in the Pages collection's afterRead (backwards-compatible with un-migrated content) and beforeChange (authoritative recompute from the live tree). The walker descends both Lexical sub-editor states and Payload type: 'blocks' arrays (so Stack items get _tier='stack').

systhema doctor — unified project-health checks with auto-fix (#86)Link to this section

One command to answer "is this project healthy, and can you fix it?". systhema doctor runs a registry of checks and reports them grouped by severity:

systhema doctor                  # grouped health report
systhema doctor --fix            # interactively resolve the fixable findings (git-safe)
systhema doctor --json | jq …    # machine-readable, read-only

It covers pending migrations / dependency drift, missing or outdated peer dependencies, legacy or missing token files, configuration and artifact sanity, and whether a newer CLI is available. With --fix it resolves the auto-fixable findings by delegating to the existing engines — systhema upgrade, peer-dependency bumps, migrate-tokens, and sync — never re-implementing a fixer, and following the same git-safety preflight as upgrade. Detection is entirely CLI-side, so it still works when the project's installed @systhemaui/core is broken.

The other commands (info, upgrade --dry-run, create, and the proxied sync / copy-files / generate-types / migrate-tokens) now emit a single throttled one-line nudge toward systhema doctor --fix when a project has fixable issues. The nudge only runs fast, local checks (never the network), is cached for six hours per project, and can be silenced with SYSTHEMA_NO_DOCTOR=1, in CI, or with --json.

systhema create rounded out — runnable on first pnpm dev (#89)Link to this section

systhema create now produces a project you can cd into and run with no further setup, and a fresh scaffold passes systhema doctor cleanly. Two changes:

  • Agent-skills install for every template. The flow offers to install Systhema agent skills (Next.js and Payload alike), mirroring systhema skills install — prompts for agent target(s), defaults to the cross-agent .agents/ location, installs before git init so they're in the first commit. Flags: --skills / --no-skills, --skill-agents <list>.
  • A complete Payload questionnaire. Database (SQLite default / Postgres), email (Resend / SendGrid / SMTP / None), Google Maps, and form captcha (none / reCAPTCHA / Turnstile / both), with PAYLOAD_SECRET + SYSTHEMA_API_SECRET auto-generated. Only the packages you pick are installed — the db/email adapters live in src/payload/database.ts / src/payload/email.ts modules that create regenerates per choice and the matching @payloadcms/* dep is swapped in before install. A fresh .env is generated from the answers; template-local .env files are never copied into a scaffold. Every credential is settable non-interactively via flags (--db, --db-url, --email, --captcha, --maps-key) or env vars.

Redesigned systhema info + human-only branded header (#92)Link to this section

systhema info is rebuilt into a doctor-style sectioned diagnostic — CLI (version + install kind, registry reachability/latency, update-available hint), Project (root, humanized type, systhema-vs-CLI version drift, the version the tokens were generated at, Payload connections), Skills (how many installed + for which agents), and Health (an error/warning summary from the same fast offline checks doctor runs). The --json output is extended, not restructured (new additive fields cli.installKind, project.tokens, project.connections, top-level health), so existing consumers keep working.

The branded SYSTHEMA header now renders only when stdout is a TTY — a human at a terminal sees it; agents, CI, pipes, and --json don't. All header call sites (create, upgrade, self-update, doctor, info, top-level help) route through one gate; also silenceable with SYSTHEMA_NO_BANNER=1. (Note: doctor used to print the header unconditionally; it's now gated like everything else — intentional.)

Admin RSC payload cut ~77% on a 30-section page (#71)Link to this section

A targeted pnpm patch against @payloadcms/richtext-lexical@3.84.1 ships inside @systhemaui/payload and a new install-lexical-schema-dedup-patch codemod adds it to consumers' patches/ + pnpm.patchedDependencies on systhema upgrade. The patch does two things:

  1. Memoizes initLexicalFeatures by (sanitizedEditorConfig, clientFieldSchemaMap, schemaPath). Identical-shape editors at the same schema location now share one object reference, so React Flight collapses what used to be 29 × 1.83 MB into 1 × 1.83 MB + 28 × ~25 KB references.
  2. Content-keyed dedup pass on every featureClientSchemaMap. The values inside a single editor's map are themselves ~99% structurally identical (300-ish distinct values across 30K positions in the root editor map); a per-request Map<hash, firstValue> collapses them.

Measured against templates/payload, 30-section seeded page:

beforeafterΔ
total inline RSC83.36 MB19.37 MB−63.99 MB / −77%
warm response~1700 ms~358 ms−79%
dominant chunk27.90 MB27.90 MBunchanged (root)
0-section baseline28.34 MB19.37 MB−32%

Functionally a no-op: no UX changes, no admin behaviour changes, no Payload version bump. Cache is module-level with WeakMap roots so it follows normal GC semantics.

The patch is version-locked — when @systhemaui/payload bumps the bundled @payloadcms/richtext-lexical, the patch is regenerated and the codemod's PATCH_TARGET_VERSION constant moves in lockstep. Consumers on the previous Payload version see "version mismatch — skipped" and stay on the unpatched runtime until they bump too. Re-running the codemod against an already-patched project is a no-op.

Lazy-mount nested Lexical editors (#71)Link to this section

region / fragment / slot editor factories now wrap their FieldComponent in a lazy-mount shell (@systhemaui/payload/admin/components/RscEntryLazyLexicalField). It forwards every prop unchanged to Payload's own RscEntryLexicalField — server-side feature resolution and form-state build still happen — but the client component holds the rendered tree behind a placeholder until an IntersectionObserver fires or the user clicks/focuses it. Once mounted, the editor stays mounted; scrolling away does not unmount.

The root editor is not wrapped (it's always above the fold on a page edit).

Verified end-to-end against a 30-section page: 30 nested placeholders render initially, only those within ~400 px of the viewport hydrate, and once mounted the editor is fully editable. Server payload, schema map, and warm response time are unchanged — this is purely a client-hydration win — but it removes the dominant TBT bottleneck on long documents.

withLazyMount also wraps the editor descriptor's generateImportMap, so the new component path is registered in projects' importMap.js on the next payload generate:importmap (which pnpm sync already runs).

⬆️ UpgradeLink to this section

If you're on 1.5.x and want to land on 1.6.0:

# Update the global CLI first so it carries the 1.6.0 migrations.
pnpm add -g @systhemaui/cli@latest

# Then upgrade your project. Plan first; commit a clean tree before running for real.
cd <your-project>
systhema upgrade --dry-run
systhema upgrade --yes

The upgrade will:

  1. Bump @systhemaui/core, react, next, payload, and the peer-dep ranges for Payload + Next.
  2. Run ten codemods: rename-systhema-bin, add-cookie-consent-to-next-layout, add-payload-instance-to-root-layout, install-lexical-schema-dedup-patch, remove-importmap-from-systhema-page, add-browserslist-to-package-json, rename-emails-capability-strings, optin-cookie-consent-tab, rename-figure-w-full, adopt-icon-picker.
  3. Apply the token migration that restructures icon.* into icon.{normal,hover}.* plus any other accumulated token additions.
  4. Run pnpm install, systhema-core payload create-app-files --override, systhema-core sync, payload generate:types, payload generate:importmap.
  5. Run post-upgrade hooks against the freshly-installed packages — currently migrate-emails-data (copies legacy emails global rows into general-settings.emails_*), migrate-header-nav-dbnames (renames the Header nav tables on Postgres to the namespaced header_nav / header_sub_nav form, from either the 1.5.x long names or the earlier canary bare names — see "Breaking & Behavioral Changes" §1), and unify-block-slugs-data (rewrites legacy block slugs in stored Lexical content). All are idempotent and adapter-aware.

The post-upgrade DB hooks talk to the database directly (not via getPayload), so they never trigger Drizzle's interactive dev schema-push — systhema upgrade runs start-to-finish non-interactively even on a Postgres project with pre-1.6.0 schema.

If you've moved managed Systhema files (e.g. into an [locale] segment for i18n), the --override step in create-app-files will recreate the defaults alongside your moved versions. Delete the duplicates after the upgrade completes.


⚠️ Breaking & Behavioral ChangesLink to this section

Running systhema upgrade adopts almost all of these for you — via codemods, the token migration, and three post-upgrade database hooks (including the Header global's Postgres table rename, which now runs as a hook rather than a manual SQL step). The only steps you do by hand are the ones a tool can't safely touch: the ambiguous custom Lexical converters the adopt-icon-picker codemod reports but won't rewrite (§2), role definitions stored outside .ts/.tsx (§7), and your own code that still references a removed block slug or generated type name (§9). Each entry below marks what — if anything — is left for you.

1. Header global Postgres identifiers shortened (#65)Link to this section

The default Header global's navigation and nested subNavigation arrays generated Postgres identifiers up to 75 chars, which Postgres silently truncated to 63. Drizzle then re-issued ADD CONSTRAINT on every dev push and produced a recurring 42710 duplicate object. Both arrays now carry a namespaced dbName (header_nav, header_sub_nav). On Payload 3.85.0 a dbName becomes the table name verbatim — header_nav / header_sub_nav, with versioned siblings _header_nav_v / _header_sub_nav_v — so the longest generated identifier (_header_sub_nav_v_reference_id_pages_id_fk, 42 chars) lands comfortably under the 63-char limit.

The table name is woven into every dependent object Drizzle generates — not just the table, but its constraints, indexes, enum types (enum_nav_type, enum__sub_nav_v_link_type, …) and owned sequences (_nav_v_id_seq, …). Enum types in particular are diffed by name, so a stale one is what triggers the interactive Is enum_header_sub_nav_link_type created or renamed? prompt. The migration renames all of them.

You are affected if all three are true:

  1. Your project uses the bundled Header global from @systhemaui/payload.
  2. You're on the Postgres adapter (@payloadcms/db-postgres).
  3. You've already pushed the old long-named tables to a database you want to keep.

Fresh installs, dev DBs you don't mind recreating, SQLite/MongoDB users, and projects that ship a custom Header are unaffected.

This is now automated (#94, #95). systhema upgrade runs a migrate-header-nav-dbnames post-upgrade hook (Postgres-only, idempotent) that renames the affected objects in place before Payload's first schema push — so there's no manual step and no interactive "create or rename?" prompt. Because 1.6.0 was corrected while still on the canary channel, the hook recognises every possible pre-upgrade state and converges them on the same namespaced target:

  • the 1.5.x long-named tables (header_navigation / header_navigation_sub_navigation),
  • the earlier-canary bare-named tables (nav / sub_nav), and
  • the half-migrated state left by an earlier 1.6.0-canary that renamed the tables but not their enums/sequences — these are now healed on the next upgrade.
Source table (1.5.x)Source table (canary)New table
header_navigationnavheader_nav
header_navigation_sub_navigationsub_navheader_sub_nav
_header_v_version_navigation_nav_v_header_nav_v
_header_v_version_navigation_sub_navigation_sub_nav_v_header_sub_nav_v

plus the matching constraints, indexes, enum types, and owned sequences for each. Whichever source state a project is on, only the objects that actually exist are touched (and projects that strip enums to varchar via beforeSchemaInit simply have no enums to rename — the hook adapts).

If you'd rather migrate by hand, skip the hook with --skip migrate-header-nav-dbnames and either run the equivalent ALTER TABLE … RENAME statements yourself, or drop the source tables (header_navigation / header_navigation_sub_navigation — or the bare nav / sub_nav — and their _v siblings) and let Payload recreate them under the new names (you'll re-enter the nav from the admin UI).

Custom Headers with hasMany: true polymorphic relationships or localized fields will also have _rels / _locales siblings on these tables — apply the same prefix rename to those if present.

2. Custom Lexical converters must call resolveIconForRender (#44)Link to this section

Material Symbols picks are stored as identifier-only ({"id":"material-symbols:arrow_forward"} with no inline SVG) so variant swaps in payload.config.ts propagate instantly across the whole site. If your project has custom Lexical converters that read data.iconBefore, data.iconAfter, or data.icon straight into dangerouslySetInnerHTML, switch to resolveIconForRender:

+ import { resolveIconForRender } from '@systhemaui/payload/fields/iconPicker/resolveIconForRender'

  function MyCustomButton({ data }) {
    return (
      <button>
-       <span dangerouslySetInnerHTML={{ __html: data.iconBefore }} />
+       <span dangerouslySetInnerHTML={{ __html: resolveIconForRender(data.iconBefore).svg }} />
        {data.label}
      </button>
    )
  }

resolveIconForRender is synchronous, server-only, and handles all four storage formats (raw SVG legacy, hybrid FA/custom, MS identifier-only, empty). Built-in converters (button, chip, icon, footer-social) already call it internally — only custom converters need editing.

systhema upgrade does most of this for you. The bundled adopt-icon-picker codemod renames faIconPickerField → iconPickerField (imports and call sites, aliases preserved) and wraps the common converter pattern — dangerouslySetInnerHTML={{ __html: <expr>.icon | .iconBefore | .iconAfter }} — in resolveIconForRender(<expr>).svg, adding the import for you. It is conservative by design: anything it can't match with confidence (a bare __html: iconBefore variable, a computed/aliased access, or any other icon-looking value) is reported, not rewritten, and listed in the upgrade output so you can adapt it by hand using the diff above. The codemod is idempotent — re-running it changes nothing.

The legacy faIconPickerField is now @deprecated but still writes raw SVG strings as before. Prefer the new iconPickerField going forward.

3. importMap prop removed from SysthemaPage and RootPage (#72)Link to this section

<SysthemaPage importMap={importMap} /> was never read inside @systhemaui/payload (it was destructured as _importMap to silence ESLint), but importing the admin importMap from a public route was dragging the Lexical editor, DocumentDrawer, the icon-picker UI, and the rest of the admin component graph into the public Next.js bundle.

The prop is gone from the Args types. systhema upgrade ships a remove-importmap-from-systhema-page codemod that strips both the import line and the prop pass from the auto-generated app/(site)/(systhema)/[[...segments]]/page.tsx. The payload create-app-files scaffold has also been updated so subsequent --override refreshes don't undo the codemod.

If you've moved the auto-generated page.tsx to a non-standard path (e.g. under an [locale] segment for i18n), the codemod will skip it; remove the import { importMap } line and the importMap={importMap} prop manually.

4. icon.* color tokens restructured to icon.{normal,hover}.* (#55 and #43 follow-ups)Link to this section

The flat colorSystem.icon.* shape was restructured into colorSystem.icon.{normal,hover}.* to power linkable icons (see "Linkable Icons" below). Five existing keys moved under normal; five new hover keys land on top.

systhema upgrade includes a token migration that handles the rename across every mode file (colorSystem.default.tokens.json, colorSystem.dark.tokens.json, custom modes). The same upgrade also fixed a latent runner bug where renames only applied to the first file containing the source path — relevant if you've authored custom multi-mode tokens.

5. Material Symbols tab appears by default in the icon picker (#44)Link to this section

Projects that do nothing on upgrade will see a new Material Symbols tab next to Font Awesome in every icon picker. To restore the v1.5 single-pack look:

withSysthema(baseConfig, {
  icons: { materialSymbols: false },
})

No DB migration needed.

6. systhema is now the global CLI — project-local bin renamed to systhema-core (#47)Link to this section

A new global CLI ships as @systhemaui/cli, installable as systhema. The previous project-local systhema binary (shipped by @systhemaui/core) has been renamed to systhema-core. The global CLI proxies project-context commands through to systhema-core so users have a single binary to remember.

Effect on existing projects: scripts in package.json that called systhema sync / systhema migrate-tokens / etc. now need to call systhema-core <command> (or pnpm systhema-core <command>). The bundled rename-systhema-bin codemod (run by systhema upgrade) rewrites package.json scripts conservatively.

# Install the global CLI (once, per machine)
pnpm add -g @systhemaui/cli@latest

# In a project:
systhema upgrade               # runs the upgrade pipeline
systhema-core sync             # runs only the project-local sync (artifacts)
systhema sync                  # runs `systhema-core sync` + reference generation

systhema --help lists every command; project-context commands (sync, payload <sub>, migrate-tokens, create-config, etc.) are proxied to systhema-core. See docs/cli/index.md for the full reference.

7. global.emails.* capabilities renamed to tab-scoped global.general-settings.emails.* (#73)Link to this section

The Emails global has been folded into general-settings as a conditional tab (alongside the existing cookieConsent tab). The old capability strings (global.emails.read, global.emails.update) are gone.

systhema upgrade runs the rename-emails-capability-strings codemod against .ts/.tsx files. Roles defined in JSON / YAML / DB rows need a manual sweep — full mapping in #73.

Data is migrated automatically via the new post-upgrade hook (see Upgrade section below) — no manual SQL.

Pre-1.6 the cookie consent tab was unconditional. From 1.6 it's gated by generalSettings.cookieConsent.enabled. systhema upgrade runs the optin-cookie-consent-tab codemod that auto-opts in any project whose systhema.config.ts has a top-level cookieConsent block. Greenfield projects opt in explicitly:

withSysthema(baseConfig, {
  generalSettings: { enabled: true, cookieConsent: { enabled: true } },
})

9. Block slugs unified — tier-segregated and XInStack slugs collapsed to single canonical slugs (#74)Link to this section

The entire tier-segregated block surface (rootImage / regionImage / fragmentImage / nestedImage, and the video / youtube / googleMap / embed equivalents), the formBlock / nestedFormBlock pair, the nested* variants, and the whole XInStack block family are renamed to single canonical slugs. Generated payload-types.ts no longer contains the legacy type names (RootImage, RegionVideo, ButtonInStack, NestedCard, …), and @systhemaui/payload no longer exports the per-tier block defs or inStackBlockTransform.

Removed / renamedReplacement
rootImage / regionImage / fragmentImage / nestedImage (+ video / youtube / googleMap / embed)image / video / youtube / googleMap / embed
formBlock / nestedFormBlockform (interface FormBlock preserved)
nestedCard / nestedColumns / nestedCarouselcard / columns / carousel
richTextInStack / buttonInStack / chipInStack / iconInStack / avatarInStack / imageInStack / nestedImageInStackrichText / button / chip / icon / avatar / image
Generated types RootImage, ButtonInStack, NestedCard, …Image, Button, Card, …
CSS class figure-w-fullfigure-w-screen (legacy alias still safelisted + matched)

Stored content migrates automatically. systhema upgrade runs the new unify-block-slugs-data post-upgrade hook, which invokes the migrate-unify-block-slugs Payload bin script — rewriting every legacy slug in stored Lexical content (and injecting the new _tier field on every block) in place across Postgres / SQLite / Mongo, schema-independently and idempotently. No manual step, no generated src/migrations/ file. During a staged rollout the runtime walker also recognises legacy slugs, so a frontend on the same DB keeps rendering before the migration runs.

What you may still need to do by hand: if your own code — custom JSX converters, block-picker config, populateLexical* overrides, or TypeScript referencing the generated block type names — names any removed slug or type, rename those references to the unified forms. The runtime accepts both legacy and unified slugs as a seatbelt, but type-level references to the old names won't compile.

10. Icon.<tag> proxy variants removed, use the as prop (#55)Link to this section

Icon was previously a Proxy typed over every keyof JSX.IntrinsicElements, so variants such as Icon.svg, Icon.div, and Icon.span were part of its type-checked public API. Only Icon.a remains. Use the polymorphic as prop for every other element:

// Before
;<Icon.svg viewBox="0 0 24 24"><path /></Icon.svg>

// After
;<Icon as="svg" viewBox="0 0 24 24"><path /></Icon>

systhema upgrade now runs the rewrite-icon-tag-variants codemod to make this rewrite automatically.


✨ New FeaturesLink to this section

Linkable Icons (#55)Link to this section

The inline IconBlock in @systhemaui/payload now supports links, matching the linkable Media blocks from 1.5.0. Editors set a link from a new "Link Settings" collapsible (custom URL or internal page reference, with optional newTab). The Lexical converter renders the polymorphic <Icon.a> — backed by next/link in @systhemaui/next — and the icon picks up a built-in hover affordance from the new icon.hover.* foundation tokens.

Drive-by improvements:

  • Icon is no longer a Proxy. Refactored to the Object.assign({ a }) pattern used by Button.a / Chip.a / MediaWrapper.a. <Icon hasBackground> and <Icon.a href="…"> work unchanged; <Icon.span> / <Icon.div> etc. are removed. See Breaking & Behavioral Changes §10.
  • Smooth-scroll on Next. @systhemaui/next provides its own Icon override so Icon.a routes through the package's Link wrapper — footer social usages and editor-authored #section links benefit from prefetch and hash-anchor smooth scroll.
  • Shared link resolution. Image / Video / Icon converters now share a single resolveLink() helper.

.systhema/artifacts/ — generated files survive pnpm install (#46)Link to this section

systhema sync now writes its generated artifacts (tokens, tokenMap.{ts,js}, types.{ts,d.ts,js}, safelist.txt) to <project>/.systhema/artifacts/ instead of node_modules/@systhemaui/core/src/tmp/. A three-tier refresh mechanism — Tailwind plugin init, systhema sync, and a Node-side bootstrap inside handleTokens.ts — keeps @systhemaui/core/dist/tmp/ mirrored from the authoritative .systhema/artifacts/ source.

  • pnpm install no longer wipes generated artifacts.
  • Hot reload during monorepo development no longer clobbers generated files.
  • .systhema/ is inspectable at the project root and ready to grow.

Also new:

  • systhema clean — wipes .systhema/ at the project root and restores @systhemaui/core/dist/tmp/ to shipped defaults.
  • .systhema/.meta.json — version marker + manifest hash for stale-output detection.

Zero consumer API changes. All existing imports (ColorSystem, LayoutBackground, getTokens, @systhemaui/core/tmp/types, etc.) continue to work unchanged.

TOON references for agents (#54)Link to this section

The new systhema generate-references command emits agent-facing TOON files at <project>/.systhema/references/tokens/*.toon. It's automatically chained behind systhema sync, so the typical flow doesn't change — running pnpm sync regenerates both the consumer artifacts and the TOON references in one step. Direct systhema-core sync does NOT run reference generation; use the global systhema sync for the complete pipeline.

Bundled with this: a skills install CLI subcommand that copies Systhema's hand-edited skills into agent skill directories (.claude/skills/, .agents/skills/), and a new systhema:understanding-tokens skill.

@layer app for project-specific overrides (#61)Link to this section

The Systhema Tailwind entry now declares the top-level layer order as theme, base, components, utilities, app — making app the last (highest-priority) layer.

@import '@systhemaui/core/tailwind';

@layer app {
  @media (min-width: theme('screens.md')) {
    .accordion-title {
      @apply flex-row-reverse justify-end;
    }
  }
}

Rules placed inside @layer app { … } win over Tailwind utilities and every Systhema component class. The starter templates ship their example overrides moved into the new layer.

Backwards compatible. Existing unlayered CSS in your global stylesheet still beats anything inside any @layer (per the CSS spec). Migrating overrides into @layer app { … } is recommended but optional.

Form-field description support (#48)Link to this section

The form-builder integration now renders an optional description under each field. Added at both the Payload field-schema level and the Lexical-to-JSX render side. colorSystem.form.description + typography tokens come with it (the token migration handles existing projects).

slotProps on field components + TextField type override (#50)Link to this section

Every *Field component in @systhemaui/react accepts a slotProps object for spreading attributes onto internal slots (label, input, description, error). TextField additionally accepts type so callers can render email, number, tel, url, etc. without dropping back to a raw <input>.

Globals trigger full revalidation when changed (#66)Link to this section

Header, Footer, Settings, and other globals' afterChange hooks now revalidate every page (revalidatePath('/', 'layout')) instead of revalidating individual tags. Globals are shared across the whole site, so a partial revalidate would have left some routes stale. Affects only the auto-generated (systhema) catch-all routes — custom routes opting into their own caching are unchanged.

Skill ecosystem: breakpoints, foundations, layout-bg (#58)Link to this section

The bundled systhema:understanding-tokens skill gains coverage of Systhema's responsive breakpoint system, foundation token semantics, and the LayoutBackground component contract, so agents apply them correctly. Installed via systhema skills install --skill <name> --agent <agent>.

Emails moved into General Settings as a conditional tab (#73)Link to this section

The standalone Emails global is gone; its four fields now live inside general-settings as an Emails tab. Both tabs (Emails and Cookie Consent) are conditionally registered — they only appear in the admin and only create DB columns when their gating option is on. If both are off, general-settings itself isn't registered.

The Emails tab also gains a Send Test Email button that routes through Systhema's email wrapper, so the saved From Name / From Address / Reply-to / branded template are all exercised end-to-end. Success and failure messages render inline with the adapter's verbatim error for debugging.

Data migration is automatic — see Upgrade section. Capability and opt-in changes are covered in Breaking & Behavioral Changes §7 and §8.

Post-upgrade hooks in the CLI (#73)Link to this section

systhema upgrade gains a postUpgrade migration shape — declarative shell hooks that run after pnpm install, so newly-bumped packages are on disk before any data-migration script runs. The new migrate-emails-to-general-settings script (see Emails refactor above) is the first user. systhema upgrade --dry-run lists hooks; --skip <hook-name> opts out. Failed hooks leave the rest of the upgrade intact and surface the exact command to re-run manually.

systhema upgrade refreshes installed skills (#87)Link to this section

Bundled skills install separately (systhema skills install) and used to drift out of sync with the CLI until you manually re-ran the install. systhema upgrade now adds a skill-refresh step: after the dependency install and token/type sync, it detects which skills the project already has installed — across both project and user scope and every supported agent — and re-copies the upgraded CLI's bundled-current version into exactly those locations. It only refreshes skills that are already present, so it never introduces a skill you didn't opt into, and a project with none installed is a clean no-op. The step is non-fatal (a failure warns and the upgrade continues), is honored under --dry-run (the plan lists which skill + scope + agent copies would be refreshed), and can be skipped entirely with --no-skills.

Homepage picker in General Settings (#80)Link to this section

A new always-on Pages tab in general-settings exposes a single optional homepage relationship field. When set, the picked page renders at /, its own URL (e.g. /home) 308-redirects to /, and every Page-relation link across blocks, Lexical content, sitemaps, and form-submission redirects resolves to / automatically — so there's no 308 round-trip on click.

Visitor lands onConfigured homepageOutcome
/noneToday's behaviour: render Page where fullPath === '', else 404
/set, page publishedRender that Page
/set, page unpublished/trashed/deletedSilent fallback to fullPath === '', else 404
/<homepage.fullPath>set, page published, not draft mode308 permanent redirect to /
/<homepage.fullPath>draft modeRender the page directly (no redirect) so editors can preview

Internals: links flow through a new pageRefToHref helper (sync + async forms) backed by a request-scoped homepageId store, primed once per request at the root view. Lexical converters stay sync because JSXConverter's return type can't be a Promise. Adds a global.general-settings.pages.update capability — admins inherit via global.*.

The Pages tab also fixes a duplicate / entry that could appear in the sitemap when both a configured homepage and a slug='' fallback page were present — the slug='' page is now suppressed from the sitemap whenever a homepage is configured.

Stack items unified into the base blocks (#74)Link to this section

inStackBlockTransform is gone. Stack and NestedStack reference the base block defs directly; the grow / shrink / flex1 / setSelfAlign / alignSelf controls move to a reusable stackLayoutFields() builder appended to each base block inside a drawer gated on _tier === 'stack'. One block definition now renders normally at root/region/fragment, with figure-width controls at root/region, and with stack-layout controls inside a Stack — invisible everywhere it doesn't apply.

Figure breakout + sub-track-width alignment (#74)Link to this section

A real breakout CSS contract: width="container" and width="screen" figures now actually escape narrower ancestor wrappers (previously they were clamped by the column track). When a figure stays at default width but its inner element is narrower than the track (an image at 50%, a 400px embed in a 1280px column), authors get left / center / right alignment via mr-auto / mx-auto / ml-auto (all safelisted). The alignment row only appears when it would have a visible effect. The CSS class figure-w-full is renamed to figure-w-screen — the rename-figure-w-full codemod rewrites source references on upgrade, and the legacy class stays safelisted + matched so existing markup keeps rendering. Embed and form blocks now render through the React <Figure> component for consistent breakout behaviour.

systhema migrate — run a single data migration standalone (#74)Link to this section

The post-upgrade DB hooks (migrate-emails-data, migrate-header-nav-dbnames, unify-block-slugs-data) are now individually runnable, mirroring how systhema codemod exposes a single code codemod:

systhema migrate --list                            # list available data migrations
systhema migrate unify-block-slugs-data            # run one by id
systhema migrate unify-block-slugs-data --dry-run  # report only, run nothing

Each migration keeps its applicable() guard, so a standalone run against an already-migrated project, the wrong project type, or a fresh install is a clean no-op. Each is a thin wrapper around the corresponding project-local Payload bin (e.g. systhema migrate unify-block-slugs-data ≡ pnpm payload migrate-unify-block-slugs). Useful for re-running a single migration after restoring a DB snapshot without re-running dep bumps / codemods / install.

Public bundle drops 55% — admin code no longer in anonymous-visitor chunks (#72)Link to this section

The importMap prop removal (see "Breaking & Behavioral Changes") plus inlining the admin-bar and live-preview CSS via React 19's hoistable <style href="…" precedence="default"> element produces, on a real consumer homepage (production next build):

  • 27 → 12 chunks loaded (−15)
  • 5.10 MB → 2.27 MB total raw asset size (−55%)
  • Three multi-hundred-KB JS chunks of Lexical + Payload admin code dropped (importMap fix)
  • 5 → 1 CSS chunks; the remaining one is the consumer's own globals.css

CSS Modules in AdminBar and the live-preview overlay are replaced with stable prefixed BEM-ish class names (payload-admin-bar, payload-admin-bar__button, payload-admin-bar--hidden) since the runtime-injected style isn't going through CSS Modules' build-time hashing anymore.

rAF-throttled scroll listeners (#72)Link to this section

ParallaxListener, AosListener, and ScrollClassesListener (in both @systhemaui/react and @systhemaui/next) used to run handlers synchronously on every scroll / resize / touchmove / load. For ParallaxListener, the per-element loop interleaved layout reads (offsetTop, offsetHeight, scrollY) with layout writes (element.style.transform = …); each write invalidated layout, so the next read forced a synchronous reflow. PageSpeed Insights flagged ~136 ms of "forced reflow" on a real consumer site.

All three listeners now coalesce events into at most one update per animation frame. ParallaxListener.updateParallax() is split into a read phase (collect every element's position + viewport bounds into measurements[]) and a separate write phase (apply transforms). No layout reads happen during the write loop. Initial updates still run synchronously so first paint is correct; only event-driven updates are scheduled. passive: true is added to ScrollClassesListener (previously missing) and preserved everywhere else.

Modern browserslist target in templates and on upgrade (#72)Link to this section

templates/payload/package.json and templates/next/package.json now ship a browserslist targeting Chrome / Edge / Firefox 100+ and Safari 15.4+. New projects scaffolded by systhema create get this automatically.

The bundled add-browserslist-to-package-json codemod adds the same field to existing consumers' root package.json on systhema upgrade. Idempotent — it does nothing if the consumer has any browserslist configuration of their own (string, array, or production/development map).

Tab-aware General Settings revalidation (#80)Link to this section

Previously, every save on the general-settings global flushed / (the layout-level ISR cache), including pure email-config edits that have no rendered-page effect. After this release, only tabs whose content actually appears on rendered pages trigger revalidatePath('/', 'layout'):

Editor saves…Layout revalidates?
Pages tab (homepage picker changed)✅
Cookie Consent — any field✅
Emails — any field (From Address, logo, etc.)❌
Mixed (cookie consent + emails)✅
Draft save (_status !== 'published')❌

Editing the page that IS the configured homepage now also revalidates / (in addition to its own URL). Header / Footer / SEO globals continue to use the shared revalidateAllPages hook unchanged — their content is unambiguously on every page.


🐛 Fixes & Internal ImprovementsLink to this section

Bug FixesLink to this section

  • Frontend admin bar no longer leaks the admin URL to anonymous visitors — the URL is off the server→client boundary; the bar fetches an authenticated /sys/admin-bar endpoint and renders nothing until auth is confirmed, gated on a non-sensitive marker cookie so anonymous SSG pages make zero origin requests (#90)
  • modify / remove token migrations are idempotent across every mode — both now walk every mode file in one pass; modify gates on value (not existence) and can't inject $value into a group node, so a migration no longer re-appears on every upgrade (#91)
  • Upgrade reconciles pnpm.overrides for the Payload family — applyPeerBumps bumps every Payload-family override (incl. transitive-only @payloadcms/drizzle / @payloadcms/graphql) to the target range, so an overrides family-pin can't silently revert the bump (#74)
  • add-payload-instance-to-root-layout codemod handles customized layouts — reuses an existing resolved payload instance or injects the missing imports instead of blindly adding getPayload(...) (#74)
  • Balance-text + text-state round-trip — kebab-case CSS keys no longer crash React rendering, and importJSON / $toggleBalanceText preserve NodeState so custom states survive load and toggle (#68)
  • as="a" routes through Link on Button / Chip / Card in @systhemaui/next, so hash-anchor smooth-scroll and next/link prefetch work (previously only the .a subcomponents did) (#63)
  • Turbopack double-!important build error worked around — splitImportantRules() routes !important block rules through @layer base.systhema to dodge lightningcss duplicating the bang (#59)
  • Raw icon JSON no longer leaks into the DOM — header CTA, footer social, and form submit buttons resolve the icon-picker JSON through resolveIconHtml() server-side (#60)
  • iconInStack / nestedImageInStack link fields wired up — stack icons and nested images now render Icon.a / MediaWrapper.a when a link is configured (#67)
  • TOON references: spec-conformant output + rounded numbers — switched to the official @toon-format/toon encoder, kebab-cased text-style class names, dropped non-CSS metadata entries, and rounded float noise to 2 dp (#57, #62)
  • Figma asterisks no longer break Tailwind variable names — string-reference $values (e.g. "{primitives.purple.500*}") now pass through stripAsterisksDeep, so they don't yield invalid var(--…*) names (#59)
  • Form blocks nested inside Lexical container blocks now render — the JSX converter descends recursively instead of walking only the top-level block list (#75)
  • Mobile menu closes on in-menu link click — the nav drawer closes after a descendant <a> is tapped (#70)
  • Template scaffolds — fixed the first-pnpm lint ESLint crash and a PostCSS deprecation warning in templates/next / templates/payload (#64)
  • Pages embedding a form revalidate when the form changes — new afterChange / afterDelete hooks on forms scan published pages (incl. forms nested in Section / Columns) and revalidate each match (#81)
  • systhema upgrade aligns every @payloadcms/* sibling to payload — dynamic discovery bumps installed db / storage / email adapters (not peer-declared) to the resolved payload range, fixing the boot-time Mismatching "payload" dependency versions crash (#84)

Internal / MonorepoLink to this section

  • pnpm.onlyBuiltDependencies WARN spam removed — the field is stripped from source template package.jsons and re-injected at CLI bundle time, so the monorepo no longer warns while scaffolded projects still get the right array (#79)
  • Next.js / React patch bumps — next ^16.2.4 → ^16.2.6, react / react-dom ^19.2.5 → ^19.2.6, matching @next/* + eslint-config-next (#45)
  • Pre-1.6.0 broad dependency refresh — patch + minor bumps across the toolchain (tailwindcss 4.2.2 → 4.3.0, swiper, tailwind-merge, ESLint plugins); deliberate majors held back (#78)
  • @toon-format/toon 2.2 → 2.3 — decode strictness + \uXXXX escapes; reference output unchanged; unblocks a stale syncWithReferences test (#77)
  • Payload 3.84.1 → 3.85.0 + minor bumps — payload and every @payloadcms/* to 3.85.0; peer floor raised to ^3.85.0; the bundled richtext-lexical patch re-targets cleanly (byte-identical) (#82)

List of all changesLink to this section

Every commit between v1.5.0 and v1.6.0, categorized. Items already covered in the prose sections above are repeated here as a flat reference.

🚀 FeaturesLink to this section

cliLink to this section

  • feat(cli): add generate-references command and bundled skills install (#52) (49ef3157)
  • feat(cli): add systhema doctor — unified project-health checks with auto-fix (#86) (58bcf0b3)
  • feat(cli): add icon-picker adoption codemod for the 1.6.0 upgrade (#93) (16c1d77b)
  • feat(cli): refresh installed skills on systhema upgrade (#87) (39b9d07c)
  • feat(cli): redesign systhema info and gate the branded header for humans (#92) (b1cef991)

cli,payloadLink to this section

  • feat(cli,payload): round out the create flow and fix fresh-project setup (#89) (e17f1536)

cli,skillsLink to this section

  • feat(cli,skills): teach agents breakpoints, foundations, and layout-bg (#58) (8d762dba)

coreLink to this section

  • feat(core): add app cascade layer for project-specific overrides (#61) (ac048755)

docs,skills,cliLink to this section

  • feat(docs,skills,cli): docs refresh, ten-skill ecosystem, palette CLI (#54) (7607cefb)

payloadLink to this section

  • feat(payload): add homepage picker + centralise page-href helper (#80) (a25afe8e)
  • feat(payload): multi-pack icon picker — material symbols, apple emoji, font awesome, custom (#44) (9ba86c57)
  • feat(payload): revalidate all pages when globals change (#66) (2060af2b)

reactLink to this section

  • feat(react): add slotProps to all *Field components and allow type override on TextField (#50) (0957b4ae)

generalLink to this section

  • !feat: add global @systhemaui/cli; rename core bin to systhema-core (#47) (breaking) (9d7556d0)
  • feat: add link support to image and video media blocks (#43) (4e92d1f2)
  • feat(*): add cookie consent integration with scanner and Payload controls (#53) (9b18a949)
  • feat(*): add form-field description support (#48) (56093c5c)
  • feat(*): add link support to icon blocks (#55) (15cc4d1d)

♻️ RefactorsLink to this section

cli,coreLink to this section

  • refactor(cli,core): unify console output styling across commands (#85) (69800e60)

coreLink to this section

  • refactor(core): relocate project artifacts to /.systhema/ (#46) (21949e81)

payloadLink to this section

  • refactor(payload): move emails into general-settings as a conditional tab (#73) (e88eb789)

generalLink to this section

  • !refactor(*): unified tier-aware blocks, figure breakout, sub-track alignment (#74) (breaking) (a4de522c)

⚡ PerformanceLink to this section

payloadLink to this section

  • perf(payload): cut admin RSC payload via lazy-mount + schemaMap dedup patch (#71) (38dffc25)

generalLink to this section

  • perf(*): cut public bundle 55% and fix scroll-listener forced reflow (#72) (58c54d73)

🐛 Bug fixesLink to this section

ciLink to this section

  • fix(ci): make dist-tag determination flag-safe in release publish (b2160b36)

cliLink to this section

  • fix(cli): align installed @payloadcms/* siblings to payload on upgrade (#84) (3b1e452c)
  • fix(cli): emit spec-conformant TOON and align generated class names with core (#57) (26a3cbc6)
  • fix(cli): make modify and remove token migrations idempotent across every mode (#91) (286c08c8)
  • fix(cli): round numeric token values in generated .toon files to two decimals (#62) (4eb308d8)

coreLink to this section

  • fix(core): strip Figma asterisks and dodge Turbopack double-!important (#59) (4f748337)

core,next,reactLink to this section

  • fix(core,next,react): close mobile menu on in-menu link click (#70) (0c8f3b40)

nextLink to this section

  • fix(next): route polymorphic as="a" through Link in Button, Chip, Card (#63) (02974134)

payloadLink to this section

  • fix(payload): fix template eslint crash and postcss warning (#64) (c6e186f0)
  • fix(payload): populate form blocks nested inside Lexical container blocks (#75) (569f5913)
  • fix(payload): preserve text state across balance-text and normalize kebab-case CSS (#68) (b270335d)
  • fix(payload): resolve icon JSON before rendering in header CTA, footer social, and form submit (#60) (c8c6a61a)
  • fix(payload): revalidate embedding pages inline on form/component change (#88) (9bf1629d)
  • fix(payload): revalidate pages when an embedded form changes (#81) (bff6ff17)
  • fix(payload): shorten Header global's Postgres identifiers via dbName (#65) (50f6db0f)
  • fix(payload): stop frontend admin bar from leaking the admin URL (#90) (3f6fc4e5)
  • fix(payload): shorten cookieTableHeaders field name to stay under PostgreSQL's 63-char column limit (#56) (bc0ef9f5)
  • fix(payload): wire iconInStack and nestedImageInStack link fields through to render (#67) (814e9499)

payload,cliLink to this section

  • fix(payload,cli): namespace Header nav tables under header_ prefix (#94) (0038899d)

🧪 TestsLink to this section

cliLink to this section

  • test(cli): accept prerelease versions in scaffolded dep specs (#76) (ac242ecd)

🧹 ChoresLink to this section

cliLink to this section

  • chore(cli): bump @toon-format/toon to 2.3.0 (#77) (de3995b0)
  • chore(cli): strip pnpm.onlyBuiltDependencies from template sources (#79) (c8210c80)

coreLink to this section

  • chore(core): add form-description entries to legacy tokens (#51) (56e82831)

depsLink to this section

  • chore(deps): bump next 16.2.4, react 19.2.5, payload 3.84.1 (#45) (ce9c82b1)
  • chore(deps): bump patch + minor versions before 1.6.0 (#78) (90bf1a8e)
  • chore(deps): bump payload 3.84.1 → 3.85.0 and other minors (#82) (4ceceb7a)

generalLink to this section

  • chore: include ".systhema" folder in prettier ignore file (01fbf9e2)
  • chore: remove files of docs/superpowers from repository (865cafaa)
  • chore(*): release v1.6.0-canary.1 (f3558426)