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

At-rules

Seven at-rules run: @use imports modules, @scope anchors rules, @on wires listeners, @dispatch / @command send events and commands from within an @on block, @view-transition animates a block's writes, @delay defers a block, and @warn / @debug / @error report from a rule.

Reference

@on takes a comma list of event names, an optional options group (a map: name: value entries and bare flags) and a block applied once per event — or just the options. @dispatch / @command take a name list and options, inside @on blocks only. @delay takes a duration expression and a block; @warn / @debug / @error take an expression. Generated.

At-rule Effect
@use "url" [as name | as *]; Imports a JS module of pure functions (a default export as default) anywhere in the sheet. The namespace defaults to the URL's last path segment without its extension; as * merges exports into the bare scope, last import winning. The first rule run waits for the imports. A path resolves against the site root; an absolute http(s) URL loads as it is.
@scope { … } Rules inside stay anchored to the host in a global sheet (the implicit wrapper of a scoped sheet). It takes no prelude.
@on <event>[, <event>] [(options)] { … } Inside a rule: listens for the events (bare names such as click or super-form-success, or strings; a comma list shares one listener) on the matched element and applies the block once per event — a one-shot transaction. The block is an ordinary rule body: declarations write the matched element (attributes, $variables, --props, content), nested rules write its matching descendants, or its siblings when the nested selector starts with + / ~, @dispatch / @command statements fire after those writes are queued. event names the DOM event and target the delegate (or event.target) inside the block and in its target / key / debounce / throttle options. @on inside a block is not supported.
@on <event> (option, option: value) … An options group after the events gates and configures the listener; with it the block is optional (@on submit (prevent-default);). A bare name is a flag. Filters: target: "<selector>" (delegation — fires only when the event target is inside a matching descendant; that element is target in the block), self (only when the event target is the matched element), key: "Escape" / "Shift+K" (keyboard chords; space-separated alternatives; the space bar is Space / Spacebar, the plus key plus). Event flags: prevent-default, stop-propagation, stop-immediate-propagation. Timing: debounce: <ms>, throttle: <ms>. JS: handle: fn — a function reference, a list (a, b), or a call that returns the listener; each listener is called with the event before the block, this being the element. Registration: once (removed after the first event that passes the filters), passive, capture, host: window / host: document (listen there while the element is connected; target then resolves against the whole document). target, key, debounce and throttle are evaluated when the event fires, in the block's scope; handle is evaluated then too, without event / target, so a call in it runs on every event; the rest once per match. Two @ons for one event may coexist when their options differ.
@dispatch <event>[, <event>] [(options)]; Inside an @on block (or a nested rule / @delay block within one): dispatches a CustomEvent of each name from the block's element after the block's writes are queued — synchronously, before they paint, so the event is an occurrence, not a delivery of State. Options, evaluated per event: detail: <expression>; target: "<selector>" (every match in the element's document; :scope = the block's element, not the sheet host — resolved as <event-handler target-ref> is) or target: <element | list> (closest("provider-fetch")); host: window / host: document; form: "<selector>" or form: <form> (its field values become the detail, an explicit detail map merges over them); the flags bubbles (default true), cancelable (default true), composed (default false), each settable to false. Dispatching the enclosing @on event is refused; every dispatch is one loop-guard hop, so an event cycle is cut. Not allowed at rule level: a rule matching is not an occurrence.
@command <name>[, <name>] [(target: …)]; Inside an @on block: invokes each command on the target elements (the block's element by default; target as for @dispatch) the way a <button command commandfor> would — native commands (show-modal, close, request-close, show-popover, hide-popover, toggle-popover) and custom --names, which reach the target as a command event. Where the browser lacks the Invoker Commands API, custom commands are dispatched as a synthetic command event and native ones call the element's method. Only target is an option.
@view-transition [(options)] { … } Inside a rule, around rules, or inside an @on block: every paint of the writes in the block — its declarations (on the rule's element) and its nested rules' — commits inside document.startViewTransition(), so CSS animates the change (view-transition-name, ::view-transition-*). It scopes how writes land, never when rules run. The transition waits for Quark to settle before the new state is captured, so writes that react to these land in the same cut. Committed without a transition: writes that change nothing, the sheet's first render, prefers-reduced-motion: reduce, browsers without the API, and writes while another view transition is active.
@view-transition (option, option: value) { … } types: "a b" names the transition for :active-view-transition-type() (a string or a list). timeout: <ms> caps the settle wait (default 300). delay: <ms> holds these writes back first. first-render also animates the sheet's first render. if-active: skip | replace: while another transition runs, commit unanimated (default) or start anyway, which skips the running one. until: "<selector>" keeps the transition open until the block's element matches the selector, until: <promise> until it settles (default timeout 1000; the page is frozen meanwhile, so for short waits only). Values are evaluated per write.
@delay <ms> { … } Inside a rule or an @on / @delay block: applies the block once, <ms> milliseconds (an expression) after the rule applied or the event fired — provided the element is still in the document and the rule still matches; otherwise the block is dropped. Applying the rule again restarts the timer (one per element). The block is an ordinary rule body (declarations write the matched element, nested rules its descendants; event / target are kept inside an @on block). Timers keep the loop guard's causal depth and are cleared when the sheet unregisters.
@warn <expression>; / @debug <expression>; / @error <expression>; Inside a rule or a block: evaluates the expression on the matched element and reports it — to the console at that level (@debug is silent below debug logging) and to DevTools as quark/diagnostic. The selector is the condition (img:not([alt]) { @warn "img needs alt"; }). @warn / @error speak once per element and rule; @debug speaks on every application, so it re-logs when a binding or prop() it reads changes. A comma list reports one value per item.
form {
  @on submit (prevent-default) { is-submitted: ""; }
  @on keydown (key: "Escape") { is-editing: none; }
  @on reset { @dispatch draft-cleared (target: "#status"); }
  &:not([is-locked]) { @on input (debounce: 200) { data-draft: event.target.value; } }
  button[data-copy] { @on click { data-copied: ""; @delay 2000 { data-copied: none; } } }
  img:not([alt]) { @warn "img needs alt"; }
}

Pages

  • @use — JS modules, asynchronous work
  • @on — events, blocks, options
  • @dispatch / @command — outgoing events and commands
  • @view-transition — animated writes
  • @delay — deferred writes
  • @warn / @debug / @error — diagnostics
  • @scope — see Sheets & scoping

That is the whole set. A sheet containing an at-rule Quark does not have (@media, @keyframes, @supports, …) fails to load, with @media is not a Quark at-rule: media queries, keyframes and the rest of the browser's style engine stay in your stylesheet.

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.