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

Props

Declare an element's state once; primitives become dashed attributes that CSS and Quark select on, rich values stay on the instance.

Declaring props

Shorthand constructors reflect primitives to dashed attributes. Rich config adds defaults, validation, storage, and custom serialize / deserialize.

import { Neutron, TokenList } from "@excom/neutron";
​
Neutron({
  tag: "usage-meter",
  props: {
    unitLabel: String, // reflects ↔ `unit-label`
    maxCount: Number, // reflects ↔ `max-count`
    isOpen: Boolean, // presence attribute `is-open`
    featureTags: TokenList, // space-separated tokens ↔ `feature-tags`, read as `string[]`
    // non-reflecting by default:
    payload: Object,
    items: Array,
    srcPromise: Promise,
    // rich config:
    maxValue: {
      type: Number,
      defaultValue: () => 100,
      isValid: (n) => n > 0,
    },
    // element references are always weak — never pin another node:
    inputEl: { type: HTMLInputElement, store: "weak" },
  },
});

Rich config keys

Key Purpose
type Constructor. String / Number / Boolean / TokenList reflect to an attribute; everything else is instance-only.
defaultValue () => value, returned when the prop is nullish or invalid.
isValid (value) => boolean. Invalid values fall back to the default; for TokenList the invalid tokens are filtered out instead.
attr Override the attribute name, or false to keep a primitive off the attribute.
store "weak" holds the value in a WeakRef and derefs on read. Required for every prop that references another element, so a removed node can be collected.
serialize / deserialize Transform on write / read.

TokenList

TokenList is exported by this package. It marks a space-separated attribute (feature-tags="a b") whose property value is a plain string[] — the element-side equivalent of class. Prefer it over Array whenever the list belongs in the document, so CSS and Quark can select on it (usage-meter[feature-tags~="a"]).

Built-in instance props

isMounted, isMoving, isAdopted, wasMounted exist on every element. They are instance-only; list them in reflectDefaultProps: ["isMounted"] to reflect them as attributes (is-mounted). The element writes these attributes and never reads them: an is-mounted in markup or on a clone mounts nothing.

Naming rules

Enforced at definition time or by convention:

  • Custom attributes must contain a dash (max-count, is-open), so they can never collide with a native attribute — now or in the future. Dev mode warns on dash-less, data-*, and aria-* attributes.
  • Booleans read as assertions: is-loading, did-fail, has-rendered, should-fetch.
  • Events are tag-prefixed (press-tracker-press), never bare (change).
  • Attribute names may not start with q-, n-, on-, or off- (reserved by Quark and Neutron).
  • Prop names may not shadow Neutron internals or effect keywords (returns, content, lifecycle names, _n_, _q_).
  • Private state and methods take a leading underscore.
  • Loosely couple: element-typed props use store: "weak", and anything else that holds a node is cleared in onDisconnected.

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.