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

@dispatch / @command

@dispatch sends a custom event and @command invokes a command, from inside an @on block. They are the outgoing half of @on: the sheet heard an event, wrote State, and now tells another element.

todo-item {
  @on click (target: "[data-remove]") {
    @dispatch todo-remove (detail: (id: attr("data-id")));
  }
}
provider-fetch {
  @on super-form-success { @command --fetch; }
}
button[data-help] {
  @on click { @command toggle-popover (target: element.nextElementSibling); }
}

Where they may appear

Inside an @on … { } block, its nested rules (then the event goes out from each matching descendant) and a @delay block within it. Not at rule level and not at sheet level: a rule matching is not an occurrence, so a rule cannot announce one. The runtime logs an error and drops such a statement.

When they run

At the end of the block, after every write in it has been queued and before those writes paint — synchronously, like a handler. An event is an occurrence, not a delivery of State: a listener that needs the block's writes should react to the State the block wrote (an attribute the recipient's own rule selects on), not to the event. Dispatching the enclosing @on event type is refused. Every dispatch is one loop-guard hop, so an event cycle (@on a { @dispatch b } … @on b { @dispatch a }) is cut like a write cycle.

@dispatch

@dispatch <event>[, <event>] [(options)]; — a CustomEvent per name. Options are evaluated per event, in the block's scope (event, target, element, $bindings):

Option Effect
detail: <expression> The event's detail — a map (id: $id, at: event.timeStamp), a $binding, anything.
target: "<selector>" Dispatch on every element matching the selector in the element's document (or shadow root). :scope is the block's element — the one the @on matched — not the sheet's :scope host. Resolved exactly as <event-handler target-ref> is (selectAll from kit-utils): provider-fetch:has(+ :scope) is the provider-fetch right before the element, :scope + dialog the dialog right after it. Default: the block's element.
target: <element> / <list> An element or list of elements from an expression: closest("provider-fetch"), element.nextElementSibling, closest("section").children.
host: window / host: document Dispatch on the window / document instead.
form: "<selector>" / form: <form> The form's field values (as formToJson reads them) become the detail; an explicit detail map merges over them.
bubbles / cancelable / composed Event flags. Bare means true; bubbles: false switches one off. Defaults: bubbles and cancelable on, composed off.

An unmatched target warns once per element and sends nothing. Events bubble by default, so an ancestor's sheet hears a dispatch from a descendant without any target.

@command

@command <name>[, <name>] [(target: …)]; — invokes each command on the target elements the way a <button command="…" commandfor="…"> would. Native commands — show-modal, close, request-close, show-popover, hide-popover, toggle-popover — and custom ones, which start with -- and reach the target as a command event (event.command; event.source is the invoker — under the Command API the hidden button Quark clicks, so a recipient should read State from itself or the sheet rather than from source; without the API, the block's element).

[data-open-help] { @on click { @command show-modal (target: "#help"); } }
#help { @on keydown (key: "Escape") { @command close; } }
[data-refresh] { @on click { @command --refresh (target: "#feed"); } }

Where the browser has the Invoker Commands API, a hidden invoker button carries the command so native behaviour and event.source are exactly the platform's. Elsewhere, custom commands are dispatched as a synthetic command event and native ones call the element's method (showModal(), togglePopover(), …); an unknown native command warns once. target is the only option and resolves like @dispatch's: the whole document, :scope = the block's element (not the sheet host).

:scope {
  button {
    @on click {
      /* the provider-fetch right before this button */
      @command --fetch (target: "provider-fetch:has(+ :scope)");
    }
  }
}

A nested rule starting with a sibling combinator is the other spelling: + provider-fetch { @command --fetch; } inside the block runs against the provider-fetch right after the button, with no target to resolve.

:scope {
  button {
    @on click {
      + provider-fetch { @command --fetch; }
    }
  }
}

Replacing <event-handler>

@on with @dispatch / @command covers what <event-handler> wires: listen-for is the event list, selector-filter / keycode-filter / is-debounced / host-ref are options, fire-event + detail-* + form-ref are @dispatch (detail: …, form: …), target-ref is target:, command-name is @command. Prefer the sheet when the page has one; keep the element for markup without a sheet.

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.