@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; }
  • Renamed web-authn attrs
  • Command-style events became native commands (2026-09-13): provider-fetch-trigger → --fetch; super-form-trigger / web-authn-trigger → --submit; content-drawer-open / -close / -toggle → --open / --close / --toggle (per-open props from the invoker's data-*, not detail); content-carousel-next / -back → --next / --back; include-content-reload / spa-route-reload / quark-sheet-reload → --reload; provider-geolocation-request / provider-orientation-request → --request; data-table-export → --export (options as data-file-type / data-file-name / data-full on the invoker); data-table-filter removed — write filter-value / filter-casing. Invoke with <button command="--verb" commandfor="id">, <event-handler command-name="--verb" target-ref="…">, <dismiss-watcher command-name="--close">, or new CommandEvent().
  • content: stopped awaiting promises from module functions (2026-09-28): return a value or a node; for work that finishes later see Asynchronous work
  • handle: names the listener (2026-09-28): event / target are not in scope inside a handle: call, which runs on every event and returns the listener; the listener receives the event
  • match-nested matches its own path too (2026-09-29): route-href="/users" match-nested is active at /users, so a separate route for that path renders beside it and a 404 fallback stays off there
  • A reload reports no move (2026-09-29): route data has move: null after a reload (was the last move before it), as on any page load that is not back / forward; no spa-manager-<move> event fires and the saved scroll offset is restored
  • The outermost spa-manager owns scroll (2026-09-29): the browser's own scroll restoration is off while it is connected; a spa-route without a manager no longer scrolls, and spa-route.setScroll() is gone
  • Scroll resets only when a route rendered (2026-09-29): a push / replace that renders nothing (a param change on a reuse route) keeps its position, and a #fragment target wins over the reset
  • One View Transition per navigation (2026-09-29): nested managers join the outermost manager's; only that one fires spa-manager-will-transition / spa-manager-transition and carries is-transitioning
  • spa-manager-will-transition and spa-manager-rendered also fire without a View Transition (2026-09-29): reduced motion, no API, no-transition, first paint. preventDefault() on spa-manager-will-transition holds the update, the first paint included, until updateRoutes(true) is called (updateRoutes() runs it without a View Transition); a nested manager's spa-manager-rendered does not bubble
  • An update that only changes provisions runs no View Transition (2026-09-29): a same-route="reuse" param change no longer animates
  • A query change provisions the route again (2026-09-29): spa-route-provision fires, the provision carries query, and a same-route="refresh" route renders again
  • A closed content-drawer is visibility: hidden after its slide-out (2026-09-29): a custom transition on it must keep visibility 0s <duration>; a closed drawer shown in the layout needs visibility: visible
  • Only an .absolute content-drawer styles its parent (2026-09-29): position: relative; overflow: clip (was every drawer, with overflow: hidden)
  • A failed request logs one line (2026-09-29): an error status logs a warning, which the default log level hides; a network or parse failure logs an error
  • A template or sheet URL that answers with an error status gives is-error and paints nothing (2026-09-29): the error page is no longer rendered as content
  • keycode-filter holding only spaces matches no key (2026-09-29): write space for the space bar
  • has-rendered waits for the first render (2026-09-29): spa-manager sets it once the first update with a render has settled: rendered, failed or hit render-timeout (was when that update started), so a loading shell hidden on it stays until then
  • params holds placeholders and named groups only (2026-09-29): a capture group in a path pattern no longer shifts the names (/(a|b)/:id gives params.id its own segment), and a placeholder that did not match is left out (was the string "undefined")
  • A template URL request outlives the element that started it (2026-09-29): an element that deactivates, reloads, disconnects or changes template-ref mid-load stops waiting; the request completes and fills the cache, and other elements on that URL finish loading (were left is-loading)
  • The internal content-carousel prop autoPlayIntervalId is now _autoPlayIntervalId (2026-09-29): pause auto-play with auto-play-stopped
  • is-fallback decides from the path (2026-09-30): it is on only when no other route in its spa-manager matches the current path; without a manager it now checks its document (or shadow root) instead of always activating, and another fallback, its own layout or a route inside its template no longer hold it off
  • spa-manager keeps the fallback title in default-title (2026-09-30): the attribute appears on the element once a route retitles the page, and may be authored; the internal _defaultTitle prop is gone
  • render-timeout shows a route still waiting for its ready-on event (2026-09-30): when it settles an update, delaying-ready is removed from that route and its ready waiters are released (the route stayed hidden until the event fired)
  • A first mount into a host-ref selector no longer clears the host (2026-09-30): what the host already holds stays until the first render replaces it, and no did-unrender fires for it
  • A rule no longer rewrites an attribute that already reads the same (2026-09-30): values are compared as written, so data-n: 0; over data-n="0" writes nothing: no mutation record, no attributeChangedCallback
  • A preserve run no longer uses up @warn / @error (2026-09-30): the one report per element is left for a run with a value
  • A property set before upgrade is kept (2026-09-30): a declared Neutron prop assigned before the element's definition loaded is applied at upgrade (was dropped)
  • Element authors (2026-09-30): replaceNonTemplateChildren from kit-utils is typed boolean | "adopted", and an override of isResponsibleForReady gets two more callers to answer true for: "adopted" (prerendered content kept) and, on a route, "render-timeout"
  • scopeQueriesToDocument is gone from @excom/nucleus-dom (2026-10-01): installShims() covers what it did; if you installed it on its own, call supportSelectors(window)

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.