diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c1fce02..7c880f3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -48,12 +48,19 @@ jobs:
- name: Type-check
run: npx tsc --noEmit
- - name: Test
- run: npm test -- --run
-
+ # BUILD BEFORE TEST, deliberately. `tests/cdn-artifacts.test.ts` asserts
+ # properties of the published artifacts that source cannot show — that
+ # SibuJS is absent from the CDN bundle's bytes, that diagnostics are
+ # compiled out of the production build, that the stylesheet carries the
+ # utilities the components use. With no `dist/` those tests have nothing
+ # to read, and they are written to FAIL rather than skip on CI, because a
+ # silent skip is how the gap they cover would come back.
- name: Build
run: npm run build
+ - name: Test
+ run: npm test -- --run
+
- name: Packaging check
run: npm pack --dry-run
@@ -101,12 +108,15 @@ jobs:
- name: Type-check against SibuJS ${{ matrix.sibujs }}
run: npx tsc --noEmit
- - name: Test against SibuJS ${{ matrix.sibujs }}
- run: npm test -- --run
-
+ # Built first for the same reason as above: the artifact tests read
+ # `dist/`, and here they also run the CDN bundle against THIS matrix
+ # entry's runtime, which is the point of the job.
- name: Build against SibuJS ${{ matrix.sibujs }}
run: npm run build
+ - name: Test against SibuJS ${{ matrix.sibujs }}
+ run: npm test -- --run
+
audit:
name: Audit (production deps)
runs-on: ubuntu-latest
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a83e9da..58ea3b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,76 @@ This project follows [Semantic Versioning](https://semver.org/).
---
+## [1.6.0] — 2026-09-07
+
+### Added — a CDN build
+
+`sibujs-ui` had no no-build story: the package shipped ESM and CJS only, so a
+page without a bundler could use the SibuJS runtime from a `
+
+
+```
+
+New export paths: `sibujs-ui/cdn`, `sibujs-ui/cdn-dev` and `sibujs-ui/cdn-css`.
+`npm run build` produces all three; `npm run build:cdn` and `npm run build:css`
+build them separately.
+
+**A compiled stylesheet ships with it, and it is not optional.** Every component
+carries Tailwind utility classes — 29 on a single `Button` — and the theme files
+are custom properties only, so a page that loaded just the script tag got
+correct behaviour and raw browser defaults: `display: inline-block`,
+`background: rgb(240,240,240)`, `padding: 6px`, no radius. Nothing threw, which
+makes that harder to diagnose, not easier. A no-build consumer has no build step
+by definition, so `dist/sibujs-ui.css` is now compiled from the components'
+actual class usage plus the base and default themes — 113.7 KB, 17.7 KB gzip.
+With it linked, the same `Button` renders `inline-flex`, `oklch(0.205 0 0)`,
+16px padding, 8px radius, 36px tall, and dark mode inverts correctly.
+
+It includes Tailwind's Preflight, matching what bundler consumers get from
+`@import "tailwindcss"` — the components are designed against that reset, and
+without it buttons keep their native chrome. It therefore restyles the host
+page, which the README states plainly.
+
+**SibuJS is not bundled in.** It stays a peer dependency in this artifact too:
+the build resolves `sibujs` to the `window.Sibu` that the runtime tag installs,
+so a page that already loaded the framework does not download it again. Loading
+`sibujs-ui`'s tag without the runtime throws a message naming the problem and
+the fix, rather than surfacing later as an undefined property inside whichever
+component ran first.
+
+| bundle | size |
+| --- | --- |
+| `cdn.global.js` | 573.5 KB raw / 132.2 KB gzip |
+| `cdn.dev.global.js` | 574.1 KB raw / 132.4 KB gzip |
+
+The CDN build carries the whole package — every component and the full icon
+set — because a `
+
+
+```
+
+That is the whole setup — no build step, and no separate theme import.
+
+This is what makes the components usable from a SibuJS **island**: build them
+inside a `registerIsland` setup and append them to the server-rendered markup.
+Components create their signals through the same runtime the island uses, so a
+click handler on a `Button` drives the island's own state with no wiring.
+
+SibuJS is **not** bundled into `sibujs-ui`'s CDN file. It stays a peer
+dependency there too: the build resolves `sibujs` to the `window.Sibu` that the
+runtime tag installs, so a page never downloads the framework twice. That is
+also why the order matters — loading `sibujs-ui` alone throws an error saying
+exactly that, rather than failing later inside a component.
+
+Use `cdn.dev.global.js` while developing to get the package's warnings; the
+production file has them compiled out, not merely disabled. Both are also
+reachable as `sibujs-ui/cdn` and `sibujs-ui/cdn-dev`.
+
+The CDN build carries the whole package — every component **and** the full icon
+set — because a `
+//
+//
+//
+// SibuJS IS NOT BUNDLED IN HERE. `sibujs` is a peer dependency, and the build
+// resolves it to the `window.Sibu` the runtime tag installed (see
+// `tsup.cdn.config.ts`). Bundling it would ship a second copy of the framework
+// to every page that already loaded one — the components would still work,
+// because the runtime shares its reactive API through a global registry, but
+// the page would pay for the bytes twice.
+//
+// That is also why the tags are ordered: this file reads `window.Sibu` while it
+// evaluates, so the runtime has to be there already. Loading it alone throws
+// with a message that says so, rather than failing later as an undefined
+// property somewhere inside a component.
+// ---------------------------------------------------------------------------
+
+import * as components from "./index";
+
+// `globalThis` rather than `window`, so the bundle also self-registers in a
+// worker. In a browser they are the same object.
+//
+// Written by hand instead of via esbuild's `globalName`: that option emits
+// `var SibuUI = (() => { … })()`, which lands AFTER the module body and would
+// overwrite whatever the body installed with the module's own export namespace.
+// Harmless while the two are identical, and a silent bug the moment they are
+// not. See `tsup.cdn.config.ts`.
+if (typeof globalThis !== "undefined") {
+ (globalThis as unknown as Record).SibuUI = { ...components };
+}
diff --git a/src/components/types.ts b/src/components/types.ts
index 5a1aabd..36e0084 100644
--- a/src/components/types.ts
+++ b/src/components/types.ts
@@ -1,11 +1,26 @@
import type { NodeChild, NodeChildren } from "sibujs";
+declare const __SIBU_DEV__: boolean | undefined;
+
// Dev-mode check, mirroring sibujs core's tree-shakeable `__SIBU_DEV__`
// convention. Off in production browsers, on in test/dev Node.
+//
+// THE BARE IDENTIFIER MUST COME FIRST. Only a bare `__SIBU_DEV__` is a `define`
+// target — `globalThis.__SIBU_DEV__` is a member expression, which a bundler
+// cannot replace, so leading with it left every branch live and carried the
+// warning text into builds that could never print it. Leading with the bare
+// name lets `define: { __SIBU_DEV__: "false" }` fold this to `false` and drop
+// the diagnostics entirely, which is what the CDN build relies on. When no
+// define is applied the identifier resolves to the global of that name, so the
+// first branch reads what the second would — hence the `!!` coercion.
const _isDev: boolean =
- typeof (globalThis as { __SIBU_DEV__?: boolean }).__SIBU_DEV__ !== "undefined"
- ? !!(globalThis as { __SIBU_DEV__?: boolean }).__SIBU_DEV__
- : typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
+ typeof __SIBU_DEV__ !== "undefined"
+ ? !!__SIBU_DEV__
+ : typeof (globalThis as { __SIBU_DEV__?: boolean }).__SIBU_DEV__ !==
+ "undefined"
+ ? !!(globalThis as { __SIBU_DEV__?: boolean }).__SIBU_DEV__
+ : typeof process !== "undefined" &&
+ process.env?.NODE_ENV !== "production";
// Heuristic mirror of sibujs core's tagFactory check: does a lone string look
// like a CSS class list rather than text? Used ONLY to warn — behavior is
diff --git a/styles/cdn.css b/styles/cdn.css
new file mode 100644
index 0000000..d387eff
--- /dev/null
+++ b/styles/cdn.css
@@ -0,0 +1,28 @@
+/**
+ * The stylesheet published alongside the CDN bundle.
+ *
+ * Compiled to `dist/sibujs-ui.css` by `npm run build:css`, and served as
+ * `sibujs-ui/cdn-css`. A page that loads the CDN