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

dismiss-watcher

One dismissal Adapter for drawers, menus, dialogs and popovers — Escape / back gesture and outside click, without each panel element owning the logic.

Features

  • Escape / back gesture Through a CloseWatcher (Android back, Escape) via watch-escape
  • Outside click A mouseup outside the target via watch-outside-click
  • One event dismiss-watcher-dismiss with detail.reason; cancel it with preventDefault()
  • Default action command-name commands invoked on the target (--close), or fire-event names dispatched at it
  • Any target The parent by default, or target-ref (:scope-relative)
  • Gated by State Live only while is-active — set it from Quark on the panel's open state

Installation

This package is available in the NucleusKit. Or it can be used by itself:

CDN Package Manager
<script src="https://unpkg.com/@excom/kit-utils@0.3.0/dist/index.umd.min.js"></script>
<script src="https://unpkg.com/@excom/neutron@0.2.0/dist/index.umd.min.js"></script>
<script src="https://unpkg.com/@excom/dismiss-watcher@0.1.8/dist/index.umd.min.js"></script>
npm install @excom/dismiss-watcher
HTML Imports JS / CSS Imports
<!-- import path to `node_modules` will depend on your build setup -->
<script type="module" src="/node_modules/@excom/dismiss-watcher"></script>
<link rel="stylesheet" href="/node_modules/@excom/dismiss-watcher">
import "@excom/dismiss-watcher";
Peer dependencies (0)

Packages a consumer must install alongside this one. Workspace deps are bundled.

Package Version
View Dist Files

All exports:

.
  • types: ./dist/index.d.ts
  • import: ./dist/index.js
  • default: ./dist/index.js
./dismiss-watcher
  • types: ./dist/dismiss-watcher.d.ts
  • import: ./dist/dismiss-watcher.js
  • default: ./dist/dismiss-watcher.js
./dismiss-watcher.js
  • types: ./dist/dismiss-watcher.d.ts
  • import: ./dist/dismiss-watcher.js
  • default: ./dist/dismiss-watcher.js
./dismiss-watcher.min
  • types: ./dist/dismiss-watcher.d.ts
  • import: ./dist/dismiss-watcher.min.js
  • default: ./dist/dismiss-watcher.min.js
./dismiss-watcher.min.js
  • types: ./dist/dismiss-watcher.d.ts
  • import: ./dist/dismiss-watcher.min.js
  • default: ./dist/dismiss-watcher.min.js
./dismiss-watcher.umd.min
  • types: ./dist/dismiss-watcher.d.ts
  • default: ./dist/dismiss-watcher.umd.min.js
./dismiss-watcher.umd.min.js
  • types: ./dist/dismiss-watcher.d.ts
  • default: ./dist/dismiss-watcher.umd.min.js
./index
  • types: ./dist/index.d.ts
  • import: ./dist/index.js
  • default: ./dist/index.js
./index.js
  • types: ./dist/index.d.ts
  • import: ./dist/index.js
  • default: ./dist/index.js
./index.min
  • types: ./dist/index.d.ts
  • import: ./dist/index.min.js
  • default: ./dist/index.min.js
./index.min.js
  • types: ./dist/index.d.ts
  • import: ./dist/index.min.js
  • default: ./dist/index.min.js
./index.umd.min
  • types: ./dist/index.d.ts
  • default: ./dist/index.umd.min.js
./index.umd.min.js
  • types: ./dist/index.d.ts
  • default: ./dist/index.umd.min.js

Usage

Drop it inside the element being dismissed, gate is-active on that element's open state, and either react to dismiss-watcher-dismiss or let command-name / fire-event close the panel for you. When neither watch-escape nor watch-outside-click is present, both watchers are on.

<nav>
  <dismiss-watcher fire-event="menu-close"></dismiss-watcher>
  …
</nav>
:scope {
  @on menu-open { data-is-open: ""; }
  @on menu-close { data-is-open: none; }
  &[data-is-open] dismiss-watcher { is-active: ""; }
  &:not([data-is-open]) dismiss-watcher { is-active: none; }
}

Why a separate element: dismissal is the same request whether the panel is a drawer, a menu, a dialog or a popover. Panel elements keep their own state (is-open); this Adapter only asks them to close. It renders nothing, listens only while is-active, and tears down when unset or removed. Write the inverse rule for is-active — Quark rules do not revert.

dismiss-watcher-dismiss bubbles and is cancelable. detail.reason is "escape" / "outside-click". The default action invokes each command-name on the target (a command event, as a <button command commandfor> would) and dispatches each fire-event name at it as a bubbling CustomEvent; preventDefault() keeps the panel open (an unsaved-changes guard, for instance).

API Reference

dismiss-watcher

Attributes (6) Role — option is configurable, state is managed by the element (read-only), or hybrid which is both. Role Attribute Type Values Prop Default option command-name
Commands invoked on the target as the default action of dismiss-watcher-dismiss — --close for a <content-drawer>, or any --verb the panel handles. commandName
tokenlist <command>… null
option fire-event
Event names dispatched at the target (bubbling CustomEvents) as the default action of dismiss-watcher-dismiss, for panels driven by events rather than commands (menu-close). fireEvent
tokenlist null
option target-ref
Selector for the element being dismissed, :scope-relative (:scope ~ nav). Unset = the parent element. targetRef
string null
option watch-escape
Watch Escape / the back gesture through a CloseWatcher. When neither watch-* attribute is present both watchers are on. watchEscape
boolean false
option watch-outside-click
Watch for a mouseup on the document outside the target. When neither watch-* attribute is present both watchers are on. watchOutsideClick
boolean false
hybrid is-active
Watchers are live while set. Gate it from Quark on the panel's open state, and write the inverse rule. isActive
boolean false
Provision (0) This provision property lives on the DOM node. Read / watch it with Quark’s prop("provision"), or listen for the neutron-provision event from app JS. Property Type
Events (1)
Dispatches — events that dismiss-watcher fires and their default actions. Dispatch Default Action
A "default action" is subsequent logic executed by the element if e.preventDefault() is not synchronously called on the event.
Name dismiss-watcher-dismiss
A dismissal was requested; detail.reason is "escape" or "outside-click". Default action: invoke each command-name on the target and dispatch each fire-event name at it as a bubbling CustomEvent. preventDefault() skips both.

Type DismissWatcherDismissEvent
Listeners — events that dismiss-watcher listens for which will trigger subsequent actions. Listener Type Action
Commands — verbs dismiss-watcher accepts as a command event (HTML Command API): from a <button command commandfor>, an <event-handler command-name>, or a CommandEvent. Command Action
Recognized Elements (0) Child or descendant elements are recognized by dismiss-watcher and relevant to its functionality. Element Relationship Required
Styles (0)
Classes — optional classes that change the appearance of dismiss-watcher. Class Description
Variables — public CSS variables for theming dismiss-watcher. Opt-out:
If you wish to opt-out of these styles on a case-to-case basis, use property all: revert-layer. Valence.css ships this as .unstyled and .unstyled-all for the entire tree.
Variable Syntax Default
Aliases — @custom-selector synonyms so custom elements can share semantic styles. Element aliases match the host tag / .tag-* class; state aliases match attributes (nested under the element alias). Alias Kind Matches
Release notes (1)

0.1.1

  • Ship only dist (and declared extras) in the npm tarball; drop build logs, tests and sources
View Source

Examples

Menu panel

Both watchers on (neither watch-* set), target = the parent <nav>. The sheet opens on menu-open, closes on dismiss-watcher-dismiss, and gates is-active on data-is-open with its inverse rule.

Content drawer

First child of <content-drawer>, command-name="--close" — the drawer needs no dismissal logic of its own. is-active follows content-drawer[is-open].

Open with --open, not --toggle: an outside mouseup on the button closes the drawer first, then the button's click re-opens it. With --toggle the same click would close it again.

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.