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

Syntax

Quark is a derivative of CSS, written in CSS syntax. Selectors, nesting, declarations and comments are CSS's own. The few additions Quark makes — $variables, expressions, #{…} interpolation inside strings, and at-rules of its own (@use, @scope, @on, @dispatch / @command, @view-transition, @delay, @warn / @debug / @error) — stay compatible with that syntax, so anyone who can read a stylesheet can read a sheet. What differs is the runtime: a stylesheet paints, a Quark sheet writes State. Features of the browser's style engine, such as media queries and keyframes, stay in your stylesheet.

Essentials

The full grammar (EBNF, precedence, disambiguation rules) is the quark-parser package's Language reference; the essentials:

  • Rules selector { … } nest. A nested selector is a descendant of its parent unless it uses & (&[open], &-active). Lists use ,; combinators are whitespace, >, +, ~.
  • Declarations key: value; — the ; is optional before }, and a declaration takes no !important. Keys are attribute names, $variables, --css-vars, content, class, dataset, ariaset.
  • At-rules @use "url" as *; imports; @on click, change (options) { … } wires listeners inside a rule (a comma list of event names, an optional (options) map, then a block applied once per event, or just the options — a JS listener is handle: fn in the map); @dispatch / @command send events from within an @on block; @scope { … } anchors rules to the host.
  • Literals "strings" / 'strings' with \ escapes and #{$interpolation}; numbers 42, 1.5, 10px (a unit makes it a string); #ccc colors (strings); true, false, null. Bare words are value keywords, @use exports, or built-ins.
  • Operators, loosest to tightest: or · and · not · == != · < > <= >= · + - · * / % · unary - + · . […] (…). Parentheses group.
  • Accessors and calls $obj.field, $list[0], $obj["key"], ns.$var; methods item.name.trim(); calls fn($a, $b), named fn($opt: 1), spread fn($args...). Only bare names and member chains are callable, not $variables.
  • Conditionals if($cond: a; $other: b; else: c) or ternary($cond, a, b).
  • Lists and maps 1, 2, 3 and 1px solid red both evaluate to arrays; (a: 1, b: 2) is a map. Spacing around a sign matters: $x +1 is a two-item list, $x + 1 / $x+1 add.
  • Comments /* … */ only, anywhere whitespace is allowed. // is not a comment in CSS, so it is not one in a sheet either: a // line is a parse error.
  • Not supported ? :, ?., ??, ===, ||, &&, arrow functions — parse errors by design.

Example

/* a comment */
[data-cart] {
  $items: prop("provision").items;
  $total: $items.length or 0;
  data-is-empty: $total == 0; /* a boolean writes "" or removes the attribute */
  [bind-summary] { content: if($total == 1: "1 item"; else: "#{$total} items"); }
  &[data-is-empty] [bind-summary] { content: "Empty"; }
  &[data-is-empty] button[data-action="clear"] { disabled: ""; }
  &:not([data-is-empty]) button[data-action="clear"] { disabled: none; }
  button[data-action="clear"] { @on click { @dispatch cart-clear; } }
}

How each declaration key is interpreted: Declaration kinds. How values evaluate: Expressions.

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.