@use "/shell" as *; @use "quark:list" as list; #release-notice { /* opens once: sheet re-runs restart a delay, so gate it on the fact it writes */ &:not([data-did-open]) { @delay 7000 { is-open: ""; data-did-open: ""; } } /* prevent clicks from bubbling to the sheet */ @on mouseup, click (stop-propagation); } provider-fetch[api-url*="package-metas/index.json"][is-success] { $package-indices: prop("provision").body; /* `excom.navGroup` moves a package out of its type's list, into that group */ $ungrouped: list.reject($package-indices.packages, "navGroup"); .package-links { [bind-elements] ul:not(ul ul) { content: iterate(getPackagesByType($ungrouped, "kit-element")); } [bind-element-bases] ul:not(ul ul) { content: iterate(getPackagesByType($ungrouped, "element-base")); } [bind-tools] ul:not(ul ul) { content: iterate(getPackagesByType($ungrouped, "tool")); } /* the standalone libraries: same rows, rendered flat (shell.css) */ [bind-libraries] ul:not(ul ul) { content: iterate(getPackagesByType($ungrouped, "library")); } [bind-library-group] ul:not(ul ul) { content: iterate(list.filter($package-indices.packages, "navGroup", "libraries")); } ul:not(ul ul) > li { $pkg: item.shortName; > spa-a { route-href: "/packages/#{item.shortName}"; } } /* one `content` rule per link: a second one would rewrite the first's text on every pass */ ul:not(ul ul, [bind-libraries] ul) > li > spa-a { content: item.shortName; } [bind-libraries] ul:not(ul ul) > li > spa-a { content: displayName(item.shortName); } /* packages whose docs span several pages: one collapsible group per section */ [bind-sections] { content: iterate(item.docSections); summary { content: item.title; } details > ul { content: iterate(item.docs); spa-a { route-href: "/packages/#{$pkg}/#{item.name}"; content: item.title; } } details:not(:has(spa-a[is-active])) { open: none; } } } /* page titles: the page, then the site. The docs home keeps the title in the document head; the other routes carry theirs in markup */ spa-route[is-active] { /* a site guide */ &[route-href$=":name"] { $title-guide: list.find($package-indices.docs, "name", $route.params.name); document-title: "#{$title-guide.title or $route.params.name} · Nucleus · docs"; } /* a package README */ &[route-href$=":packageName"] { document-title: "#{displayName($route.params.packageName)} · Nucleus · docs"; } /* one page of a package's docs */ &[route-href$=":docName"] { $title-package: list.find($package-indices.packages, "shortName", $route.params.packageName); document-title: "#{docTitle($title-package, $route.params.docName)} · #{displayName($route.params.packageName)} · Nucleus · docs"; } } /* the page's markdown file, for the footer link: the docs home and each guide the index lists, and a package's page whose index entry says `markdown` (build-docs-index sets it; the dev server's index has none, so no link there). siteDocHref and the path check say which URL is the page's own: the 404 and a package's doc pages have none */ > spa-manager[active-url] { $page-path: attr("active-url").split("#").at(0).split("?").at(0); $page-name: if($page-path == SITE_HOME: SITE_HOME_DOC; else: $page-path.split("/").at(-1)); $page-guide: list.find($package-indices.docs, "name", $page-name); $page-package: list.find($package-indices.packages, "shortName", $page-name); $page-markdown: if($page-guide and siteDocHref($page-guide.name) == $page-path: "/docs/#{$page-guide.name}.md"; $page-package.markdown and "#{SITE_BASE}/packages/#{$page-package.shortName}" == $page-path: "/#{$page-package.shortName}.md"); footer [bind-page-markdown] { href: $page-markdown; content: ternary($page-markdown, "Markdown version of this page"); } } } spa-route { $route: prop("provision"); /* paramless routes name their guide in markup (the docs home, `/`); /docs/:name gets it from params */ $route-doc-name: attr("data-doc-name"); } /* the desktop aside and the mobile sheet stamp the same nav template */ [data-site-nav] { details:has(spa-a[is-active]) { /* not kosher */ open: ""; } spa-a[is-active] { aria-current: ""; } spa-a:not([is-active]) { aria-current: none; } } /* mobile sheet: Escape / back gesture while open; a tapped link closes it */ #site-menu { &[is-open] dismiss-watcher { is-active: ""; } &:not([is-open]) dismiss-watcher { is-active: none; } @on click (target: "spa-a") { is-open: none; } } #site-menu-gesture { &:has(> #site-menu[is-open]) { progress-offset: 1; } &:not(:has(> #site-menu[is-open])) { progress-offset: 0; } @on gesture-handler-start { #site-menu { is-scrubbing: ""; } } @on gesture-handler-end { #site-menu { is-open: event.detail.snap == 1; is-scrubbing: none; } } } main { @on copy-source (handle: copySource); } #search-dialog[open] { > include-content { is-active: ""; /* for some reason this is necessary on first render */ @on include-content-did-render (handle: focusInput); } input[type="search"] { /* works on all subsequent opens */ autofocus: ""; } } #search-dialog:not([open]) input[type="search"] { autofocus: none; }

Core Concepts

The Nucleus Stack implements an application architecture named ASO: Adapter, State, Orchestrator. Three roles.

The driving idea

The live HTML document is the application's state. Not a rendering of that state, and not a projection of it. Facts live in the document, and the document is also what the user sees. There is no separate memory model to keep aligned, because a second copy does not exist.

Everything else follows from that.

The three roles

Adapters

An Adapter is an element that bridges the State and one protocol outside it — a server, a sensor, a store, or the person using the page. It carries that protocol's state as attributes on itself, announces what happens through events, and leaves all coordination to the Orchestrator.

<provider-fetch api-url="/api/user"></provider-fetch>

provider-fetch bridges the network. Lifecycle shows up as attributes (is-loading, is-success, is-error); the response is published as data; events fire. It does not decide what happens next. It knows about nothing else.

Some native elements are Adapters as well. <input> bridges the keyboard: it owns its pseudoclasses like :valid and fires events like change. <form> bridges submission: it validates and fires submit. Nucleus Stack Adapters, through the Custom Element API, simply extend that same contract to protocols the platform does not cover yet.

State

The State is the document: every element, attribute, and text node, plus the rich data some Adapters publish on themselves. Three properties let it function as state:

  • Structured — nesting carries meaning. Rules scope themselves by ancestry.
  • Addressable — any part can be named with a selector.
  • Serializable — the whole thing can be written out, inspected, and shipped fully formed.

Anything you would put in a store belongs on an element instead: <main data-mode="edit">, <li data-done>, <dialog open>.

Orchestrator

The Orchestrator watches the State and writes the State. In this stack that role is Quark, a CSS-derived language of rules:

main[data-mode="edit"] [bind-toolbar] { content: template("#edit-tools"); }
main:not([data-mode="edit"]) [bind-toolbar] { content: none; }

A rule names a condition (a selector) and the writes to perform while that condition holds (attributes, content, listeners, variables). The Orchestrator stores no state of its own. Whatever it knows, it reads from the DOM; whatever it decides, it writes back.

The loop

  1. An Adapter does its job — something happens on its protocol — and reflects the result on itself as an attribute or event.
  2. The Orchestrator notices the change and applies matching rules, writing into other parts of the document.
  3. Those writes drive other Adapters, which act on their protocols and reflect results. Those writes can also notify other relevant Orchestrator rules.
  4. Repeat until the document settles.

Nothing directly calls anything else. Adapters hold no references to one another. The Orchestrator holds no references to Adapters. Coordination happens entirely through what is visible in the document.

No components

This stack has no component concept. Like the native platform, elements are generic Lego pieces, not custom bricks: they have no intrinsic knowledge of any other elements outside of their own family, so you compose them the way you compose native HTML.

When you need a reusable chunk of UI with its own behavior, you write a view: an HTML fragment that optionally carries its own CSS and Quark sheet, loaded where you need it by include-content or spa-route. Views belong to you. Elements stay generic.

Where the JavaScript goes

Most pages need none. When a calculation outgrows Quark expressions, the sheet imports a module and calls its functions:

@use "/pricing.js" as pricing;
[bind-total] { content: pricing.total($items, $tax-rate); }

A module should be pure business logic: it takes values and returns a value. It is also the exit for anything Quark cannot yet declare. Bridging a protocol, such as the network, storage, the clock or a person, belongs to an Adapter. Quark leans that way on purpose: it calls module functions synchronously and does not await what they return, so a fetch inside a module is deliberately awkward. A function may build and return a node it owns, such as a chart; it should still leave the document around it alone.

Quick reference

You want to… Reach for
Bridge a protocol — a browser API, a store, a person's interaction (tabs, drawers) — configurably A NucleusKit element
React to state, bind data, render lists, wire an event A Quark rule
Compute or format a value A pure function via @use
Style something CSS / Valence.css, keyed to state attributes
Package a chunk of UI A view (HTML + CSS + Quark)

Next steps

  • Quick Start - A working page, in five minutes.
  • Core Concepts - The mental model, in one sitting.
  • Using Elements - The NucleusKit catalog and how elements behave.
  • Orchestrating - Get familiar with Quark.
  • Styling - Valence.css themes, tokens, and state-driven CSS.
  • Building Views - Structure a real app: routes, views, lazy loading.
  • Other Guides - Business Logic, Creating Elements, Best Practices, Troubleshooting, Debugging with Agents
  • Diving Deeper - The architecture behind it all, for the curious and the skeptical.

Beta. The Nucleus Stack is in beta for a few weeks until features are stabilized and optimized.

Thanks — we'll email you when it ships.

Something went wrong. Please try again.