# @nibcss/runtime

The browser half of NibCSS: it reads `nib="…"` attributes, generates the CSS
they need, and keeps one stylesheet up to date. Every framework binding wraps
this package; you reach for it directly on a page with no framework at all, or
when you want a second instance scoped to part of the document.

```html
<script src="https://cdn.jsdelivr.net/npm/@nibcss/runtime"></script>
<div nib="d=flex; g=8px; p=$space.md; hover:bgc=#eef2ff">styled</div>
```

---

## 1. Create a project

Nothing to create: a single HTML file is enough. With a bundler, any setup you
already have.

## 2. Install

```bash
npm i @nibcss/runtime
```

Or from a CDN, with no install at all:

```html
<script src="https://cdn.jsdelivr.net/npm/@nibcss/runtime"></script>
```

That build boots the moment it executes and puts the runtime on `window.Nib`.
Put it in `<head>` and do **not** add `defer`, `async` or `type="module"`: all
three postpone execution until after parsing, and the page paints unstyled
first. Booting synchronously installs the observer before `<body>` is parsed, so
elements are styled in the same checkpoint the parser inserts them in.

## 3. Configure it

The runtime takes the same options as `nibcss.config.ts` — it *is* that config,
resolved. Three ways to hand it over, in the order the CDN build looks:

```html
<!-- 1. a global, which is what @nibcss/cli generates for you -->
<script src="assets/nib.config.js"></script>   <!-- sets window.__NIB__ -->

<!-- 2. inline JSON -->
<script type="application/json" data-nib-config>
    { "prefix": "nib-", "tokens": { "color": { "brand": "#4f46e5" } } }
</script>

<!-- 3. a few attributes on the script tag itself -->
<script src="…/nib.global.js" data-nib-prefix="app-" data-nib-dark="class" data-nib-debug="true"></script>
```

With a module build, pass it in code:

```ts
import { init } from "@nibcss/runtime";
import nibConfig from "@nibcss/config";   // written by the plugin or the CLI

const nib = init(nibConfig);
```

> Give it the real config. Without it the runtime uses defaults, and with a
> custom `prefix` **nothing** it generates matches a stylesheet built from your
> config.

## 4. A mini project

```html
<!doctype html>
<html lang="en">
<head>
    <script src="https://cdn.jsdelivr.net/npm/@nibcss/runtime"></script>
    <script type="application/json" data-nib-config>
        {
            "darkMode": "class",
            "tokens": {
                "color": { "brand": "#4f46e5", "ink": "#0f172a" },
                "space": { "md": "16px", "lg": "24px", "xl": "40px" },
                "radius": { "md": "12px" }
            },
            "components": {
                "btn": "d=inline-flex; ali=center; p=10px 18px; bdrd=$radius.md; bd=none; cur=pointer; ftw=600; bgc=$color.brand; cl=#fff"
            }
        }
    </script>
    <style>
        html, body { margin: 0; font-family: system-ui; background: #f8fafc; }
        html.dark body { background: #0b0f19; color: #e2e8f0; }
    </style>
</head>
<body>
    <main nib="mxw=720px; mi=auto; p=$space.xl $space.md; d=flex; flxd=column; g=$space.lg">
        <h1 nib="m=0; fts=28px; ftw=800; cl=$color.ink; dark:cl=#f8fafc">Dashboard</h1>
        <button type="button" id="theme" nib="btn">Dark mode</button>
        <div id="box" nib="p=$space.md; bd=1px solid #e2e8f0; bdrd=$radius.md">16px</div>
        <input type="range" id="pad" min="4" max="48" value="16" nib="w=100%; acc=$color.brand" />
    </main>

    <script>
        theme.onclick = () => Nib.toggleDarkMode();
        pad.oninput = (e) => {
            // either set the attribute — the observer resolves it…
            box.setAttribute("nib", `p=${e.target.value}px; bdrd=$radius.md; bd=1px solid #e2e8f0`);
            // …or take the class string: box.className = Nib.classesFor(…)
            box.textContent = `${e.target.value}px`;
        };
    </script>
</body>
</html>
```

No build step, no stylesheet file: every rule on that page was generated in the
browser. Add [`@nibcss/cli`](https://www.npmjs.com/package/@nibcss/cli) when you
want the static rules in a real `.css` file instead.

## 5. The API

```ts
import { init, getRuntime, shareRuntime, resetRuntime, NibRuntime } from "@nibcss/runtime";
```

| Export | What it is |
|---|---|
| `init(options?)` | create, start and share the page's runtime (idempotent) |
| `getRuntime()` | the shared runtime, or `null` |
| `shareRuntime(rt)` | register an instance you created yourself, without starting it |
| `resetRuntime()` | forget the shared instance (tests) |
| `NibRuntime` | the class — `new NibRuntime({ root, watch, gc, …config })` |
| `darkModeBootScript()` | the dark-mode decision as a string, to inline in `<head>` before the first paint |
| `DARK_STORAGE_KEY` | `"nib-dark"`, the key the choice is stored under |
| `readConfigFromDocument()` | the config discovery the CDN build uses |
| `StyleSink`, `DarkModeController` | the pieces, if you are building something unusual |

An instance:

| Member | What it does |
|---|---|
| `start()` / `destroy()` | scan the root and observe it / stop and remove the stylesheet |
| `classesFor(attr)` | the class string for an attribute, generating the CSS |
| `configure(partial)` | merge new config in — one token, a recipe, `prefix` — and re-resolve the document |
| `flush()` | apply what the observer has queued, now (tests, or after bulk DOM changes) |
| `toggleDarkMode()`, `darkMode.isDark()`, `darkMode.enable()`, `darkMode.disable()`, `darkMode.useSystem()` | the `.dark` class, persisted in `localStorage` |
| `engine.config`, `engine.css()`, `engine.stats()` | the resolved config, the CSS so far, rule and class counts |
| `scan(root?)`, `gc()` | re-scan a subtree, collect rules no element uses any more |

Construction-only options: `root` (default `document.documentElement`), `watch`
(observe mutations — `false` for one-shot SSR), `gc`, `nativeValidation`
(validate values with `CSS.supports`).

## 6. Diagnostics in the browser

With `debug: true`, every problem is dispatched on `document`:

```ts
document.addEventListener("nib:warning", (e) => {
    console.warn(e.detail.attr, e.detail.messages, e.detail.diagnostics);
});
```

Subscribe **early** — in `<head>`, right after the runtime tag. The runtime
styles the document as the parser builds it, so warnings for your static markup
are dispatched before a script at the end of `<body>` runs.

`document` also receives `nib:config` whenever `configure()` runs, with the
resolved config as its detail.

## 7. Server-side rendering

`watch: false` and a root you provide, then collect the CSS:

```ts
const rt = new NibRuntime({ ...config, root: dom.window.document, watch: false });
rt.start();
const css = rt.engine.css();
```

The framework bindings do this for you — `<NibStyles />` in React,
`collectNibStyles(rt)` in React and Vue.

---

## `nibcss.config.ts`

One file, read by three consumers — the build plugin, the CLI, and the browser
runtime — so a change lands everywhere at once. It is created for you on the
first run, with every option documented inside. Every field is optional.

```ts
import { defineConfig } from "@nibcss/core";

export default defineConfig({
    // ── Dark mode ──────────────────────────────────────────────────────
    // "class"  →  .dark .nib-x { … }   toggle with runtime.toggleDarkMode()
    // "media"  →  @media (prefers-color-scheme: dark) { … }
    // false    →  every `dark:` rule is dropped
    darkMode: "class",

    // ── Diagnostics ────────────────────────────────────────────────────
    // Report unknown utilities, unknown tokens and invalid values, with the
    // file and line. With a bundler plugin this also mounts the browser
    // overlay. Turning it off and on again needs no restart.
    debug: true,

    // Language for every message, terminal and overlay alike.
    // Built in: uz, en, ru, tr, es, de, zh. Default: "uz".
    language: "en",

    // Add or re-word messages — your own language, or your team's wording.
    // A key you leave out falls back to English.
    messages: {
        fr: {
            "invalid-value": "« {value} » est invalide pour {property}",
            accepts: "valeurs acceptées : {examples}",
        },
    },

    // true  → an invalid declaration is dropped instead of emitted (CI)
    // false → it is written out and the browser ignores it (iterating)
    strict: false,

    // ── Base reset ─────────────────────────────────────────────────────
    // true → the built-in reset, false → none, string → your own CSS.
    // Emitted inside `@layer nib-preflight`, so your stylesheet always wins
    // over it and utilities always win over both.
    preflight: true,

    // ── Class names ────────────────────────────────────────────────────
    // Prefix for every generated class. A class name is a hash of the rule,
    // so the same rule always produces the same name — on every machine.
    prefix: "nib-",

    // Allow `hover:`, `focus:`, `before:`, `nth-child:` … variants.
    pseudos: true,

    // ── Breakpoints — merged over the eight built-ins ───────────────────
    // Built in: xs 480, sm 576, md 768, lg 992, xl 1024, 2xl 1200,
    //           3xl 1440, 4xl 1536.
    breakpoints: {
        tablet: { min: 768, max: 1023 },   // ranges are allowed
        "3xl": { min: 1600 },              // override one
    },

    // ── Design tokens ──────────────────────────────────────────────────
    // Referenced in any value as `$group.key` or `$group-key`. The groups
    // are arbitrary; these are a starting point, not a schema.
    tokens: {
        color: {
            brand: "#4f46e5",
            "brand-hover": "#4338ca",
            ink: "#0f172a",
            "ink-soft": "#64748b",
            surface: "#ffffff",
            "surface-dark": "#0f172a",
            border: "#e2e8f0",
            "border-dark": "#1e293b",
        },
        space: { xs: "4px", sm: "8px", md: "16px", lg: "24px", xl: "40px" },
        radius: { sm: "8px", md: "12px", lg: "20px", pill: "999px" },
        // any CSS value works, gradients and shadows included
        gradient: { brand: "linear-gradient(135deg, #4f46e5, #ec4899)" },
    },

    // ── Component recipes ──────────────────────────────────────────────
    // A named set of utilities, used as a bare word (no `=`):
    //   nib="btn"            the whole recipe
    //   nib="btn; fts=16px"  the recipe plus an override
    //   nib="md:card"        the prefix flows into every rule in it
    // A recipe may extend another by naming it first, as "card-hover" does.
    components: {
        btn:
            "d=inline-flex; ali=center; jsc=center; g=8px; p=10px 18px; " +
            "bdrd=$radius.sm; bd=none; cur=pointer; ftw=600; fts=14px; " +
            "bgc=$color.brand; cl=#fff; ts=background-color .15s; " +
            "hover:bgc=$color.brand-hover",

        card:
            "d=flex; flxd=column; g=$space.md; p=$space.lg; bdrd=$radius.lg; " +
            "bgc=$color.surface; bd=1px solid $color.border; " +
            "dark:bgc=$color.surface-dark; dark:bd=1px solid $color.border-dark",

        "card-hover": "card; ts=transform .2s; hover:tf=translateY(-4px)",
    },

    // ── Custom shorthands — merged over the ~290 built-ins ──────────────
    // Map your own key to one or more CSS *property names*:
    shortcuts: {
        size: "width height",                // nib="size=40px" → both
        tap: "-webkit-tap-highlight-color",  // shorten a vendor property
    },
});
```

> `shortcuts` maps a key to property **names**. A bundle of declarations belongs
> in `components`: `center: "d=flex; ali=center; jsc=space-between"`, used as
> `nib="center"`.

### Where the file lives

Beside `package.json`, at the project root — the same place as
`vite.config.ts`, `next.config.ts` or `nuxt.config.ts`. `nibcss.config.js`,
`.mjs`, `.cjs` and `.mts` work too, and `nib.config.*` is accepted as a second
name.

---

## The attribute syntax

One attribute, semicolon-separated declarations:

```html
<div nib="d=flex; ali=center; g=8px; p=$space.md; hover:bgc=#eef2ff">
```

### A declaration

`shorthand=value`. The shorthand is the CSS property with the vowels removed —
`d` for `display`, `bgc` for `background-color`, `fts` for `font-size`,
`bdrd` for `border-radius`. About 290 are built in, and the full property name
always works too: `background-color=red` is the same as `bgc=red`.

| Written | CSS |
|---|---|
| `fts=14px` | `font-size: 14px` |
| `p=10px 18px` | `padding: 10px 18px` |
| `bd=1px solid #eee` | `border: 1px solid #eee` |
| `bgi=linear-gradient(90deg, red, blue)` | `background-image: …` |
| `ts=transform .25s ease` | `transition: …` |
| `cl=red!important` | `color: red !important` |

Values are passed through as written, so `calc()`, `var()`, `clamp()`,
`rgb(… / .5)`, `conic-gradient(…)` and custom properties all work. Commas and
spaces inside a value need no escaping; a literal **semicolon** does: write
`\;`.

```html
<span nib="after:cnt=' \; done'">
```

### Tokens

`$group.key` or `$group-key`, resolved from `tokens` in the config:

```html
<div nib="p=$space.lg; bgc=$color.brand; bd=1px solid $color.border">
```

An unknown token is reported with the nearest match rather than silently
producing nothing.

### Recipes

A bare word with no `=` is a name from `components`:

```html
<button nib="btn">Save</button>
<button nib="btn; fts=16px; px=24px">Save</button>   <!-- plus overrides -->
<article nib="card-hover; flg=1">…</article>
```

A recipe name is **not** a real CSS class: `nib="btn"` works, `class="btn"` does
not. Ask the runtime for the string when you need one.

### Variants

Prefixes stack, separated by `:` — breakpoint, then state, then the
declaration:

| Prefix | Example | Meaning |
|---|---|---|
| breakpoint | `md:d=flex` | from 768px up |
| custom breakpoint | `tablet:d=grid` | whatever you defined |
| pseudo-class | `hover:bgc=#333`, `focus-visible:otl=2px solid` | state |
| form state | `required:bdl=4px solid #f59e0b`, `disabled:opc=.5`, `user-invalid:bdc=#b91c1c` | |
| structural | `first-child:ftw=700`, `nth-child(2):bgc=#eee`, `empty:h=20px` | |
| pseudo-element | `before:cnt='▸'`, `after:cl=#15803d`, `first-letter:fts=34px`, `marker:cl=red`, `selection:bgc=#4f46e5` | |
| vendor | `-webkit-scrollbar:w=10px`, `-webkit-scrollbar-thumb:bgc=#888` | |
| dark | `dark:bgc=#0b0f19` | per `darkMode` |
| combined | `md:hover:bgc=#333`, `dark:hover:bgc=#1e1b4b` | |

A variant in front of a recipe applies to the whole recipe: `md:btn`.

### Static or dynamic

A plain string attribute is **static**: the build resolves it, writes the CSS,
and compiles the class name into the markup, so the first paint is styled and no
JavaScript is needed for it.

Anything the build cannot read — a template literal, an expression, a value your
code assembles — is **dynamic**: the browser runtime resolves it, adds the rule
before the next frame, and keeps it in its own stylesheet.

```html
<!-- static: resolved at build time -->
<div nib="p=16px">

<!-- dynamic: resolved in the browser -->
<div nib={`p=${pad}px`}>
```

---

## License

MIT
