@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; }

NucleusKit

The whole Nucleus Stack in one package — every element, provider, Quark, and Valence.css behind a single import.

Features

  • One JS import Registers every NucleusKit element
  • One CSS import Valence.css theme + shared element styles (basic.css)
  • App-ready Routing, sheets, forms, drawers, providers, and more
  • À la carte Every package is published on its own; if you only use a few elements, install just those

Installation

This package is available in the NucleusKit. Or it can be used by itself:

CDN Package Manager
<script src="https://unpkg.com/@excom/nucleus-kit@0.5.1/dist/index.umd.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@excom/nucleus-kit@0.5.1/dist/basic.css">
npm install @excom/nucleus-kit
HTML Imports JS / CSS Imports
import "@excom/nucleus-kit";
@import "@excom/nucleus-kit/basic.css";
Peer dependencies (0)

Packages a consumer must install alongside this one. Workspace deps are bundled.

Package Version
View Dist Files

All exports:

.
  • types: ./dist/index.d.ts
  • import: ./dist/index.js
  • default: ./dist/index.js
./basic.bundle.min.css
  • default: ./dist/basic.bundle.min.css
./basic.css
  • default: ./dist/basic.css
./basic.min.css
  • default: ./dist/basic.min.css
./custom-elements.bundle.min.css
  • default: ./dist/custom-elements.bundle.min.css
./custom-elements.css
  • default: ./dist/custom-elements.css
./custom-elements.min.css
  • default: ./dist/custom-elements.min.css
./index
  • types: ./dist/index.d.ts
  • import: ./dist/index.js
  • default: ./dist/index.js
./index.js
  • types: ./dist/index.d.ts
  • import: ./dist/index.js
  • default: ./dist/index.js
./index.min
  • types: ./dist/index.d.ts
  • import: ./dist/index.min.js
  • default: ./dist/index.min.js
./index.min.js
  • types: ./dist/index.d.ts
  • import: ./dist/index.min.js
  • default: ./dist/index.min.js
./index.umd.min
  • types: ./dist/index.d.ts
  • default: ./dist/index.umd.min.js
./index.umd.min.js
  • types: ./dist/index.d.ts
  • default: ./dist/index.umd.min.js
./nucleus-kit.progressive
  • types: ./dist/nucleus-kit.progressive.d.ts
  • import: ./dist/nucleus-kit.progressive.min.js
  • default: ./dist/nucleus-kit.progressive.min.js
./nucleus-kit.progressive.js
  • types: ./dist/nucleus-kit.progressive.d.ts
  • import: ./dist/nucleus-kit.progressive.min.js
  • default: ./dist/nucleus-kit.progressive.min.js
./nucleus-kit.progressive.min
  • types: ./dist/nucleus-kit.progressive.d.ts
  • import: ./dist/nucleus-kit.progressive.min.js
  • default: ./dist/nucleus-kit.progressive.min.js
./nucleus-kit.progressive.min.js
  • types: ./dist/nucleus-kit.progressive.d.ts
  • import: ./dist/nucleus-kit.progressive.min.js
  • default: ./dist/nucleus-kit.progressive.min.js
./server
  • types: ./dist/server.d.ts
  • import: ./dist/server.js
  • default: ./dist/server.js
./server.js
  • types: ./dist/server.d.ts
  • import: ./dist/server.js
  • default: ./dist/server.js
./server.min
  • types: ./dist/server.d.ts
  • import: ./dist/server.min.js
  • default: ./dist/server.min.js
./server.min.js
  • types: ./dist/server.d.ts
  • import: ./dist/server.min.js
  • default: ./dist/server.min.js

Usage

import "@excom/nucleus-kit";
@import "@excom/nucleus-kit/basic.css";

That loads Valence.css (basic theme) plus element CSS and registers the packages below.

Progressive bundle (experimental)

nucleus-kit.progressive.min.js registers nothing up front. It watches the document for NucleusKit element tags and imports each element's package the first time its tag appears (initial scan, then every inserted subtree), so a page pays only for the elements it uses. Packages shared by several elements (neutron, kit-utils, quark, the element bases) are separate chunks under dist/progressive/, fetched once. The entry is 3.5 kB; a page using quark-sheet and content-drawer loads ~55 kB gzip less than the all-in build.

<script type="module" src="/node_modules/@excom/nucleus-kit/dist/nucleus-kit.progressive.min.js"></script>

With a bundler, import "@excom/nucleus-kit/nucleus-kit.progressive"; loads the same entry.

Trade-offs: elements upgrade one network round-trip later (style the pre-upgrade state with :not(:defined)), it is ES modules only, and elements inside a shadow root need observeElements(shadowRoot) from the same module. The all-in index.umd.min.js and the ESM index.js are unchanged.

On a prerendered page, the packages for the tags present at startup load before hydration ends, so those elements keep the prerendered markup.

Idle loading

Prefetch the remaining packages once the page has loaded, so later views, dialogs and SPA navigations upgrade instantly with no round-trip. Packages load one per browser idle period; tags already on the page load first and are never fetched twice.

Opt in on <body> (works with inline / bundled imports) or on the entry's own <script>:

<body nucleus-kit-idle>                                    <!-- every package -->
<body nucleus-kit-idle="spa-route super-form data-table">  <!-- only these -->
​
<script type="module" src="/node_modules/@excom/nucleus-kit/dist/nucleus-kit.progressive.min.js" data-idle></script>

An empty value loads everything; a space-separated list loads only the packages behind those tags (unknown tags log a warning). data-idle is read only from the <script> whose src is the entry itself and wins over the body attribute. Nothing is prefetched in data-saver mode (Save-Data). From JS, idleLoadElements(tags?) does the same.

Server entry

@excom/nucleus-kit/server is the kit for prerendering in Node: every export of the main entry except the elements that read the device or the person (detect-browser, detect-features, detect-media, gesture-handler, network-status, provider-geolocation, provider-orientation, provider-storage, service-worker, web-authn). Those stay as written in the prerendered page and upgrade in the browser; SERVER_EXCLUDED_TAGS lists their tags. It also exports the hooks a prerender runs around each page (beforeRender, settle, afterRender), so it is the whole entry of a nucleus-ssr config: entry: () => import("@excom/nucleus-kit/server"). ES modules only, not for the browser.

À la carte

NucleusKit is a convenience, not a requirement. If you find you are not using most of its elements, install the packages you do use individually and drop it:

npm install @excom/quark-sheet @excom/provider-fetch @excom/content-drawer
import "@excom/quark-sheet";
import "@excom/provider-fetch";
import "@excom/content-drawer";

Each package is self-contained — same elements, same versions, same CSS hooks — and its README documents the slim install. Valence.css is @excom/valence.

TypeScript

Generally, you are advised to avoid TypeScript unless your app starts having a lot of complex JS customization. In that case, NucleusKit elements declare global types: their HTML…Element interfaces, HTMLElementTagNameMap entries (so querySelector("spa-manager") and closest(…) are typed) and Quark's element.quark. TypeScript loads them only when it sees an import of the package, so an app that loads NucleusKit from a <script type="module"> or a CDN gets "Property does not exist" on element.closest("spa-manager")?.router or element.quark.

Add one declaration file that the app's tsconfig.json includes:

// globals.d.ts
import "@excom/nucleus-kit";

A .d.ts file emits nothing: no runtime import, no bundle cost. À la carte apps import each package they use instead (import "@excom/spa-route"; import "@excom/quark";). If a TypeScript file in the app already imports NucleusKit or the packages, nothing is needed.

What's included

Core

  • neutron — custom element factory
  • quark / quark-sheet — DOM orchestration
  • valence — Valence.css, semantic CSS (via basic.css)

Layout / chrome

  • content-carousel
  • content-drawer
  • content-tabs
  • dialog-anchor
  • dismiss-watcher
  • data-table

Content / routing

  • include-content
  • spa-route
  • scroll-into-view

Forms / auth

  • super-form
  • super-input
  • web-authn

Providers

  • provider-fetch
  • provider-geolocation
  • provider-orientation
  • provider-storage

Platform

  • detect-browser
  • detect-features
  • detect-media
  • dom-observer
  • event-handler
  • gesture-handler
  • network-status
  • service-worker

Not in NucleusKit

Install separately when needed: mapbox-view, super-img, element bases (abortable-element, fetchable-element, …), and editor tooling such as nucleus-quark-highlighter.

Release notes (6)

0.5.1

  • Fix sheets that @use one module with a top-level await on Safari: on a page's first load the module's functions could run before it had finished loading (a highlighter that was not ready, so no highlighted code until the next navigation)

0.5.0

  • Add the prerender hooks to @excom/nucleus-kit/server (beforeRender, settle, afterRender): a kit app prerenders with entry: () => import("@excom/nucleus-kit/server") and no entry module of its own

0.4.0

  • Add @excom/nucleus-kit/server: the Kit without the elements that read the device or the person, for prerendering with @excom/nucleus-ssr; SERVER_EXCLUDED_TAGS lists their tags
  • Improve the progressive entry for prerendered pages: hydration stays open until the packages for the Kit tags present at startup are imported, so those elements keep their prerendered content
  • Add the @excom/nucleus-kit/nucleus-kit.progressive import path: the progressive entry is exported under its plain name as well as nucleus-kit.progressive.min

0.3.0

  • Add Quark.meter, the engine's work counters (counts, reset(), scopeSelectors()), always on, for complexity snapshots with @excom/nucleus-test
  • Add a warning when a spa-manager update settles by render-timeout while a route is still pending, so an empty first paint has a cause in the console at log level 2
  • Update content-carousel in slide-animation="track" to render only the active slide and its two DOM neighbours (a wrap with three or more slides cuts instead of sliding), and to animate nothing before the first move, so an initial is-active appears in place
  • Update gesture-handler pan-x / pan-y handlers to hold the page still for the rest of a touch whose first 3 px run along the handler's axis, through a non-passive touchmove listener; touch-action is unchanged, a touch that starts across the axis scrolls as before, and a same-axis scroller under the finger keeps scrolling
  • Fix web-authn after a failed ceremony or options request: is-loading is cleared, is-error is set with the message in provision, and a retry clears is-error
  • Fix an issue where an is-fallback route stayed off after leaving a nested route for a URL no route matches: the fallback now checks which routes match the path, so its position among its siblings no longer matters, a page without a spa-manager gets a working fallback, and a fallback inside a layout can activate on the layout's unmatched child paths
  • Fix the published type declarations of neutron, quark, quark-parser, quark-formatter and kit-devtools, which pointed at a folder outside their tarballs, so the Kit's re-exported types resolve instead of any

0.2.0

  • Add idle loading to the progressive entry: opt in with <body nucleus-kit-idle> or data-idle on its <script> (empty = every package, a space-separated tag list = those packages) to prefetch packages one per idle period after page load; tags already on the page load first and are never fetched twice, data-saver mode is respected, and idleLoadElements(tags?) does the same from JS
  • Remove promise support from content: in Quark sheets: a module function returns a value or a node (see Asynchronous work in the Quark docs)
  • Update handle: in @on: event and target are not in scope inside a handle: expression, and the listener receives the event
  • Add KitLogger to the exports of Nucleus Kit and its progressive entry: KitLogger.level = 2 shows the warnings Nucleus Kit elements log
  • Add QuarkLogger to the exports of Nucleus Kit, its ESM entry and its UMD global, next to KitLogger: QuarkLogger.level = 2 shows the warnings Quark logs, independently of KitLogger.level; the progressive entry does not export it, as that would load all of Quark up front
  • Add did-load to provider-fetch, super-form, web-authn and quark-sheet: set on the first success, kept while a refresh loads, cleared on an error, so content can stay on screen with [did-load] instead of [is-success]
  • Add relative route-href values to spa-route and spa-a: checkout resolves against the page's <base>
  • Add the key names Space / Spacebar and plus to Quark's key: and to keycode-filter (Shift+Space)
  • Add absolute http(s) URLs to Quark's @use: the module loads from that URL, and any other scheme (data:, blob:) is refused with a logged error
  • Update match-nested on spa-route and spa-a to match its own path as well as child paths: route-href="/users" is active at /users, so a separate route for that path renders beside it and a 404 fallback stays off there
  • Update a reload to report move: null in the route data: no spa-manager-<move> event fires for it, and the saved scroll offset is restored
  • Update scroll handling so the outermost spa-manager owns it: the browser's own scroll restoration is off while one is connected (the page's value returns after the last one disconnects), a spa-route without a spa-manager no longer scrolls, and spa-route.setScroll() is removed
  • Update scroll reset to happen only when a route rendered: a push or replace that renders nothing (a query change on a reuse route) keeps its position, and a #fragment target wins over the reset
  • Update nested spa-manager elements to join the outermost manager's View Transition: one transition per navigation, and only the outermost manager fires spa-manager-will-transition and spa-manager-transition and carries is-transitioning
  • Update spa-manager-will-transition and spa-manager-rendered to fire when no View Transition runs too (reduced motion, no API, no-transition, first paint), once per update chain, as a navigation that arrives before the update settles joins it; a nested manager's spa-manager-rendered no longer bubbles, so a listener on the outermost manager or above gets one per chain, and preventDefault() on spa-manager-will-transition holds the update, the first paint included, until updateRoutes(true) resumes it (with a View Transition where the page allows one) or updateRoutes() runs it without one
  • Update a same-route="reuse" param change to run no View Transition: an update that only changes provisions no longer animates
  • Update a query change to provision the route again: spa-route-provision fires, the provision carries a new query object, and a same-route="refresh" route renders again
  • Update has-rendered on spa-manager to be set once the routes of the first update have rendered, inside that update: a loading shell hidden on [has-rendered] stays until the first view is on screen (it was set when the first update started)
  • Update a closed content-drawer to be visibility: hidden once its slide-out ends: a custom transition on it must keep visibility 0s <duration>, and a closed drawer shown in the layout needs visibility: visible
  • Update content-drawer so only an .absolute drawer styles its parent, as position: relative; overflow: clip (was every drawer, with overflow: hidden)
  • Update failed-request logging to one line: an error status logs a warning, which the default KitLogger level hides, and a network or parse failure logs an error
  • Update a template or sheet URL that answers with an error status to give is-error and paint nothing: the error page is no longer rendered as content
  • Update a keycode-filter holding only spaces to match no key and warn once; write space for the space bar
  • Fix an issue where a rule reading a $binding or prop() did not apply its value again when an attribute or child change made it match again, so an inverse rule left the old value in place
  • Fix scroll restoration on back and forward: it no longer snaps before the transition, a restore follows late content for about 2 seconds or until the user scrolls, taps, types or drags the scrollbar, and a #fragment target rendered by a route is scrolled to
  • Fix an issue where a navigation started inside a spa-manager-rendered listener was dropped
  • Fix an issue where spa-manager hung in a browser whose startViewTransition() throws: the update now runs without a transition
  • Fix provision.params on a route-regex route: named groups ((?<id>\d+)) give params.id, URL-decoded, unmatched groups are left out, and unnamed groups are read from provision.match (every group used to land under the key undefined)
  • Fix provision.params on a spa-route whose route-href holds a capture group of its own, such as /(a|b)/:id: placeholders keep their names (the group shifted them, so a param landed under the key undefined), a group the author names, such as (?<lang>en|de), becomes a param, and a placeholder that did not take part in the match is left out (it was the string "undefined")
  • Fix an issue where a route-href with a non-capturing group, such as /(?:a|b)/:id, made spa-route and spa-a throw
  • Fix an issue where include-content and spa-route elements on the same template URL stayed is-loading when one of them disconnected, reloaded or changed template-ref during the load: the shared request now finishes for the others
  • Add a TypeScript section to the README: an app that loads Nucleus Kit from HTML or a CDN gets its global element types from a globals.d.ts containing import "@excom/nucleus-kit";

0.1.1

  • Fix the types condition of the ./nucleus-kit.progressive.min export to point at dist/nucleus-kit.progressive.d.ts instead of a dist/nucleus-kit.d.ts that is never published
  • Ship only dist (and declared extras) in the npm tarball; drop build logs, tests and sources
View Source

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.