@campminder/components 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # @campminder/components
2
2
 
3
+ > Published on public npm. Install and render: [`docs/consuming-the-shell.md`](../../docs/consuming-the-shell.md).
4
+ > Working in here: [`CONTRIBUTING.md`](../../CONTRIBUTING.md).
5
+
3
6
  The Campminder chrome: left nav, top bar, super-user banner. Lit web components,
4
7
  composition-agnostic, rendering only what they are handed.
5
8
 
@@ -196,3 +199,36 @@ fails loudly rather than silently rendering an empty banner.
196
199
 
197
200
  > Components render themselves; the shell owns layout. No component may touch
198
201
  > `document.body`, `document.head`, or query for a sibling.
202
+
203
+ ## Dismissing the side-nav flyout
204
+
205
+ `<cm-side-nav>` exposes exactly one dismissal entry point:
206
+
207
+ ```ts
208
+ dismissFlyout(restoreFocus: boolean): void
209
+ ```
210
+
211
+ It closes the flyout when one is open and is a no-op when none is. Every trigger
212
+ routes through it — the flyout's own Escape and shift-Tab paths, the navigation's
213
+ mouseleave, the `cm-mega-menu-close` event (which now carries
214
+ `detail.restoreFocus`), and the host shell's page-level Escape and outside pointer
215
+ press — so the focus rule below is applied in one place and no trigger can bypass
216
+ it.
217
+
218
+ **The focus rule.** Focus moves only when `restoreFocus` is true _and_ focus is
219
+ currently inside the flyout; then it goes to the category control that opened the
220
+ flyout, falling back to the navigation's collapse control when that category
221
+ control no longer exists. Focus is never left on the document body, and is never
222
+ pulled into the navigation from somewhere else on the page — a person typing in a
223
+ form field, or pressing a pointer outside the flyout, keeps the focus they have.
224
+
225
+ Two consequences worth keeping in mind when changing this code:
226
+
227
+ - The focus target is read **before** the open-category state is cleared. The
228
+ target is found via the `category-hovered` class, which is derived from that
229
+ state; clearing first leaves nothing to find and silently reintroduces the
230
+ dropped-focus bug.
231
+ - Page-level Escape and outside-pointer listening belongs to the **shell**, which
232
+ calls `dismissFlyout` on each slotted navigation — `true` for Escape, `false`
233
+ for an outside pointer press. This component registers nothing on `document` or
234
+ `window`, per the rule above, and `mega-menu-isolation.test.ts` observes that.
@@ -22,7 +22,7 @@ let it = class {
22
22
  return this.cssText;
23
23
  }
24
24
  };
25
- const at = (r) => new it(typeof r == "string" ? r : r + "", void 0, B), Ot = (r, ...t) => {
25
+ const rt = (r) => new it(typeof r == "string" ? r : r + "", void 0, B), Mt = (r, ...t) => {
26
26
  const e = r.length === 1 ? r[0] : t.reduce((s, i, o) => s + ((n) => {
27
27
  if (n._$cssResult$ === !0) return n.cssText;
28
28
  if (typeof n == "number") return n;
@@ -35,17 +35,17 @@ const at = (r) => new it(typeof r == "string" ? r : r + "", void 0, B), Ot = (r,
35
35
  const s = document.createElement("style"), i = H.litNonce;
36
36
  i !== void 0 && s.setAttribute("nonce", i), s.textContent = e.cssText, r.appendChild(s);
37
37
  }
38
- }, Z = I ? (r) => r : (r) => r instanceof CSSStyleSheet ? ((t) => {
38
+ }, F = I ? (r) => r : (r) => r instanceof CSSStyleSheet ? ((t) => {
39
39
  let e = "";
40
40
  for (const s of t.cssRules) e += s.cssText;
41
- return at(e);
41
+ return rt(e);
42
42
  })(r) : r;
43
43
  /**
44
44
  * @license
45
45
  * Copyright 2017 Google LLC
46
46
  * SPDX-License-Identifier: BSD-3-Clause
47
47
  */
48
- const { is: ct, defineProperty: dt, getOwnPropertyDescriptor: pt, getOwnPropertyNames: ut, getOwnPropertySymbols: $t, getPrototypeOf: _t } = Object, f = globalThis, J = f.trustedTypes, ft = J ? J.emptyScript : "", D = f.reactiveElementPolyfillSupport, w = (r, t) => r, N = { toAttribute(r, t) {
48
+ const { is: ct, defineProperty: dt, getOwnPropertyDescriptor: pt, getOwnPropertyNames: ut, getOwnPropertySymbols: $t, getPrototypeOf: _t } = Object, f = globalThis, Z = f.trustedTypes, ft = Z ? Z.emptyScript : "", k = f.reactiveElementPolyfillSupport, w = (r, t) => r, N = { toAttribute(r, t) {
49
49
  switch (t) {
50
50
  case Boolean:
51
51
  r = r ? ft : null;
@@ -73,7 +73,7 @@ const { is: ct, defineProperty: dt, getOwnPropertyDescriptor: pt, getOwnProperty
73
73
  }
74
74
  }
75
75
  return e;
76
- } }, q = (r, t) => !ct(r, t), K = { attribute: !0, type: String, converter: N, reflect: !1, useDefault: !1, hasChanged: q };
76
+ } }, V = (r, t) => !ct(r, t), J = { attribute: !0, type: String, converter: N, reflect: !1, useDefault: !1, hasChanged: V };
77
77
  Symbol.metadata ?? (Symbol.metadata = Symbol("metadata")), f.litPropertyMetadata ?? (f.litPropertyMetadata = /* @__PURE__ */ new WeakMap());
78
78
  let v = class extends HTMLElement {
79
79
  static addInitializer(t) {
@@ -82,7 +82,7 @@ let v = class extends HTMLElement {
82
82
  static get observedAttributes() {
83
83
  return this.finalize(), this._$Eh && [...this._$Eh.keys()];
84
84
  }
85
- static createProperty(t, e = K) {
85
+ static createProperty(t, e = J) {
86
86
  if (e.state && (e.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(t) && ((e = Object.create(e)).wrapped = !0), this.elementProperties.set(t, e), !e.noAccessor) {
87
87
  const s = Symbol(), i = this.getPropertyDescriptor(t, s, e);
88
88
  i !== void 0 && dt(this.prototype, t, i);
@@ -100,7 +100,7 @@ let v = class extends HTMLElement {
100
100
  }, configurable: !0, enumerable: !0 };
101
101
  }
102
102
  static getPropertyOptions(t) {
103
- return this.elementProperties.get(t) ?? K;
103
+ return this.elementProperties.get(t) ?? J;
104
104
  }
105
105
  static _$Ei() {
106
106
  if (this.hasOwnProperty(w("elementProperties"))) return;
@@ -129,8 +129,8 @@ let v = class extends HTMLElement {
129
129
  const e = [];
130
130
  if (Array.isArray(t)) {
131
131
  const s = new Set(t.flat(1 / 0).reverse());
132
- for (const i of s) e.unshift(Z(i));
133
- } else t !== void 0 && e.push(Z(t));
132
+ for (const i of s) e.unshift(F(i));
133
+ } else t !== void 0 && e.push(F(t));
134
134
  return e;
135
135
  }
136
136
  static _$Eu(t, e) {
@@ -202,7 +202,7 @@ let v = class extends HTMLElement {
202
202
  var n;
203
203
  if (t !== void 0) {
204
204
  const a = this.constructor;
205
- if (i === !1 && (o = this[t]), s ?? (s = a.getPropertyOptions(t)), !((s.hasChanged ?? q)(o, e) || s.useDefault && s.reflect && o === ((n = this._$Ej) == null ? void 0 : n.get(t)) && !this.hasAttribute(a._$Eu(t, s)))) return;
205
+ if (i === !1 && (o = this[t]), s ?? (s = a.getPropertyOptions(t)), !((s.hasChanged ?? V)(o, e) || s.useDefault && s.reflect && o === ((n = this._$Ej) == null ? void 0 : n.get(t)) && !this.hasAttribute(a._$Eu(t, s)))) return;
206
206
  this.C(t, e, s);
207
207
  }
208
208
  this.isUpdatePending === !1 && (this._$ES = this._$EP());
@@ -278,18 +278,18 @@ let v = class extends HTMLElement {
278
278
  firstUpdated(t) {
279
279
  }
280
280
  };
281
- v.elementStyles = [], v.shadowRootOptions = { mode: "open" }, v[w("elementProperties")] = /* @__PURE__ */ new Map(), v[w("finalized")] = /* @__PURE__ */ new Map(), D == null || D({ ReactiveElement: v }), (f.reactiveElementVersions ?? (f.reactiveElementVersions = [])).push("2.1.2");
281
+ v.elementStyles = [], v.shadowRootOptions = { mode: "open" }, v[w("elementProperties")] = /* @__PURE__ */ new Map(), v[w("finalized")] = /* @__PURE__ */ new Map(), k == null || k({ ReactiveElement: v }), (f.reactiveElementVersions ?? (f.reactiveElementVersions = [])).push("2.1.2");
282
282
  /**
283
283
  * @license
284
284
  * Copyright 2017 Google LLC
285
285
  * SPDX-License-Identifier: BSD-3-Clause
286
286
  */
287
- const C = globalThis, F = (r) => r, R = C.trustedTypes, G = R ? R.createPolicy("lit-html", { createHTML: (r) => r }) : void 0, rt = "$lit$", _ = `lit$${Math.random().toFixed(9).slice(2)}$`, nt = "?" + _, At = `<${nt}>`, g = document, P = () => g.createComment(""), U = (r) => r === null || typeof r != "object" && typeof r != "function", V = Array.isArray, mt = (r) => V(r) || typeof (r == null ? void 0 : r[Symbol.iterator]) == "function", j = `[
288
- \f\r]`, b = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, Q = /-->/g, X = />/g, A = RegExp(`>|${j}(?:([^\\s"'>=/]+)(${j}*=${j}*(?:[^
289
- \f\r"'\`<>=]|("|')|))|$)`, "g"), Y = /'/g, tt = /"/g, ot = /^(?:script|style|textarea|title)$/i, yt = (r) => (t, ...e) => ({ _$litType$: r, strings: t, values: e }), Ht = yt(1), E = Symbol.for("lit-noChange"), d = Symbol.for("lit-nothing"), et = /* @__PURE__ */ new WeakMap(), m = g.createTreeWalker(g, 129);
290
- function ht(r, t) {
291
- if (!V(r) || !r.hasOwnProperty("raw")) throw Error("invalid template strings array");
292
- return G !== void 0 ? G.createHTML(t) : t;
287
+ const C = globalThis, K = (r) => r, R = C.trustedTypes, Y = R ? R.createPolicy("lit-html", { createHTML: (r) => r }) : void 0, nt = "$lit$", _ = `lit$${Math.random().toFixed(9).slice(2)}$`, ot = "?" + _, At = `<${ot}>`, g = document, P = () => g.createComment(""), U = (r) => r === null || typeof r != "object" && typeof r != "function", q = Array.isArray, mt = (r) => q(r) || typeof (r == null ? void 0 : r[Symbol.iterator]) == "function", D = `[
288
+ \f\r]`, b = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, G = /-->/g, Q = />/g, A = RegExp(`>|${D}(?:([^\\s"'>=/]+)(${D}*=${D}*(?:[^
289
+ \f\r"'\`<>=]|("|')|))|$)`, "g"), X = /'/g, tt = /"/g, ht = /^(?:script|style|textarea|title)$/i, yt = (r) => (t, ...e) => ({ _$litType$: r, strings: t, values: e }), Tt = yt(1), E = Symbol.for("lit-noChange"), d = Symbol.for("lit-nothing"), et = /* @__PURE__ */ new WeakMap(), m = g.createTreeWalker(g, 129);
290
+ function at(r, t) {
291
+ if (!q(r) || !r.hasOwnProperty("raw")) throw Error("invalid template strings array");
292
+ return Y !== void 0 ? Y.createHTML(t) : t;
293
293
  }
294
294
  const gt = (r, t) => {
295
295
  const e = r.length - 1, s = [];
@@ -297,11 +297,11 @@ const gt = (r, t) => {
297
297
  for (let a = 0; a < e; a++) {
298
298
  const h = r[a];
299
299
  let c, p, l = -1, u = 0;
300
- for (; u < h.length && (n.lastIndex = u, p = n.exec(h), p !== null); ) u = n.lastIndex, n === b ? p[1] === "!--" ? n = Q : p[1] !== void 0 ? n = X : p[2] !== void 0 ? (ot.test(p[2]) && (i = RegExp("</" + p[2], "g")), n = A) : p[3] !== void 0 && (n = A) : n === A ? p[0] === ">" ? (n = i ?? b, l = -1) : p[1] === void 0 ? l = -2 : (l = n.lastIndex - p[2].length, c = p[1], n = p[3] === void 0 ? A : p[3] === '"' ? tt : Y) : n === tt || n === Y ? n = A : n === Q || n === X ? n = b : (n = A, i = void 0);
300
+ for (; u < h.length && (n.lastIndex = u, p = n.exec(h), p !== null); ) u = n.lastIndex, n === b ? p[1] === "!--" ? n = G : p[1] !== void 0 ? n = Q : p[2] !== void 0 ? (ht.test(p[2]) && (i = RegExp("</" + p[2], "g")), n = A) : p[3] !== void 0 && (n = A) : n === A ? p[0] === ">" ? (n = i ?? b, l = -1) : p[1] === void 0 ? l = -2 : (l = n.lastIndex - p[2].length, c = p[1], n = p[3] === void 0 ? A : p[3] === '"' ? tt : X) : n === tt || n === X ? n = A : n === G || n === Q ? n = b : (n = A, i = void 0);
301
301
  const $ = n === A && r[a + 1].startsWith("/>") ? " " : "";
302
- o += n === b ? h + At : l >= 0 ? (s.push(c), h.slice(0, l) + rt + h.slice(l) + _ + $) : h + _ + (l === -2 ? a : $);
302
+ o += n === b ? h + At : l >= 0 ? (s.push(c), h.slice(0, l) + nt + h.slice(l) + _ + $) : h + _ + (l === -2 ? a : $);
303
303
  }
304
- return [ht(r, o + (r[e] || "<?>") + (t === 2 ? "</svg>" : t === 3 ? "</math>" : "")), s];
304
+ return [at(r, o + (r[e] || "<?>") + (t === 2 ? "</svg>" : t === 3 ? "</math>" : "")), s];
305
305
  };
306
306
  class x {
307
307
  constructor({ strings: t, _$litType$: e }, s) {
@@ -315,11 +315,11 @@ class x {
315
315
  }
316
316
  for (; (i = m.nextNode()) !== null && h.length < a; ) {
317
317
  if (i.nodeType === 1) {
318
- if (i.hasAttributes()) for (const l of i.getAttributeNames()) if (l.endsWith(rt)) {
318
+ if (i.hasAttributes()) for (const l of i.getAttributeNames()) if (l.endsWith(nt)) {
319
319
  const u = p[n++], $ = i.getAttribute(l).split(_), M = /([.?@])?(.*)/.exec(u);
320
- h.push({ type: 1, index: o, name: M[2], strings: $, ctor: M[1] === "." ? Et : M[1] === "?" ? St : M[1] === "@" ? bt : k }), i.removeAttribute(l);
320
+ h.push({ type: 1, index: o, name: M[2], strings: $, ctor: M[1] === "." ? Et : M[1] === "?" ? St : M[1] === "@" ? bt : L }), i.removeAttribute(l);
321
321
  } else l.startsWith(_) && (h.push({ type: 6, index: o }), i.removeAttribute(l));
322
- if (ot.test(i.tagName)) {
322
+ if (ht.test(i.tagName)) {
323
323
  const l = i.textContent.split(_), u = l.length - 1;
324
324
  if (u > 0) {
325
325
  i.textContent = R ? R.emptyScript : "";
@@ -327,7 +327,7 @@ class x {
327
327
  i.append(l[u], P());
328
328
  }
329
329
  }
330
- } else if (i.nodeType === 8) if (i.data === nt) h.push({ type: 2, index: o });
330
+ } else if (i.nodeType === 8) if (i.data === ot) h.push({ type: 2, index: o });
331
331
  else {
332
332
  let l = -1;
333
333
  for (; (l = i.data.indexOf(_, l + 1)) !== -1; ) h.push({ type: 7, index: o }), l += _.length - 1;
@@ -408,7 +408,7 @@ class O {
408
408
  }
409
409
  $(t) {
410
410
  var o;
411
- const { values: e, _$litType$: s } = t, i = typeof s == "number" ? this._$AC(t) : (s.el === void 0 && (s.el = x.createElement(ht(s.h, s.h[0]), this.options)), s);
411
+ const { values: e, _$litType$: s } = t, i = typeof s == "number" ? this._$AC(t) : (s.el === void 0 && (s.el = x.createElement(at(s.h, s.h[0]), this.options)), s);
412
412
  if (((o = this._$AH) == null ? void 0 : o._$AD) === i) this._$AH.p(e);
413
413
  else {
414
414
  const n = new vt(i, this), a = n.u(this.options);
@@ -420,7 +420,7 @@ class O {
420
420
  return e === void 0 && et.set(t.strings, e = new x(t)), e;
421
421
  }
422
422
  k(t) {
423
- V(this._$AH) || (this._$AH = [], this._$AR());
423
+ q(this._$AH) || (this._$AH = [], this._$AR());
424
424
  const e = this._$AH;
425
425
  let s, i = 0;
426
426
  for (const o of t) i === e.length ? e.push(s = new O(this.O(P()), this.O(P()), this, this.options)) : s = e[i], s._$AI(o), i++;
@@ -429,8 +429,8 @@ class O {
429
429
  _$AR(t = this._$AA.nextSibling, e) {
430
430
  var s;
431
431
  for ((s = this._$AP) == null ? void 0 : s.call(this, !1, !0, e); t !== this._$AB; ) {
432
- const i = F(t).nextSibling;
433
- F(t).remove(), t = i;
432
+ const i = K(t).nextSibling;
433
+ K(t).remove(), t = i;
434
434
  }
435
435
  }
436
436
  setConnected(t) {
@@ -438,7 +438,7 @@ class O {
438
438
  this._$AM === void 0 && (this._$Cv = t, (e = this._$AP) == null || e.call(this, t));
439
439
  }
440
440
  }
441
- class k {
441
+ class L {
442
442
  get tagName() {
443
443
  return this.element.tagName;
444
444
  }
@@ -463,7 +463,7 @@ class k {
463
463
  t === d ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t ?? "");
464
464
  }
465
465
  }
466
- class Et extends k {
466
+ class Et extends L {
467
467
  constructor() {
468
468
  super(...arguments), this.type = 3;
469
469
  }
@@ -471,7 +471,7 @@ class Et extends k {
471
471
  this.element[this.name] = t === d ? void 0 : t;
472
472
  }
473
473
  }
474
- class St extends k {
474
+ class St extends L {
475
475
  constructor() {
476
476
  super(...arguments), this.type = 4;
477
477
  }
@@ -479,7 +479,7 @@ class St extends k {
479
479
  this.element.toggleAttribute(this.name, !!t && t !== d);
480
480
  }
481
481
  }
482
- class bt extends k {
482
+ class bt extends L {
483
483
  constructor(t, e, s, i, o) {
484
484
  super(t, e, s, i, o), this.type = 5;
485
485
  }
@@ -504,8 +504,8 @@ class wt {
504
504
  S(this, t);
505
505
  }
506
506
  }
507
- const z = C.litHtmlPolyfillSupport;
508
- z == null || z(x, O), (C.litHtmlVersions ?? (C.litHtmlVersions = [])).push("3.3.3");
507
+ const j = C.litHtmlPolyfillSupport;
508
+ j == null || j(x, O), (C.litHtmlVersions ?? (C.litHtmlVersions = [])).push("3.3.3");
509
509
  const Ct = (r, t, e) => {
510
510
  const s = (e == null ? void 0 : e.renderBefore) ?? t;
511
511
  let i = s._$litPart$;
@@ -548,15 +548,15 @@ class T extends v {
548
548
  }
549
549
  var st;
550
550
  T._$litElement$ = !0, T.finalized = !0, (st = y.litElementHydrateSupport) == null || st.call(y, { LitElement: T });
551
- const L = y.litElementPolyfillSupport;
552
- L == null || L({ LitElement: T });
551
+ const z = y.litElementPolyfillSupport;
552
+ z == null || z({ LitElement: T });
553
553
  (y.litElementVersions ?? (y.litElementVersions = [])).push("4.2.2");
554
554
  /**
555
555
  * @license
556
556
  * Copyright 2017 Google LLC
557
557
  * SPDX-License-Identifier: BSD-3-Clause
558
558
  */
559
- const Tt = (r) => (t, e) => {
559
+ const Nt = (r) => (t, e) => {
560
560
  e !== void 0 ? e.addInitializer(() => {
561
561
  customElements.define(r, t);
562
562
  }) : customElements.define(r, t);
@@ -566,7 +566,7 @@ const Tt = (r) => (t, e) => {
566
566
  * Copyright 2017 Google LLC
567
567
  * SPDX-License-Identifier: BSD-3-Clause
568
568
  */
569
- const Pt = { attribute: !0, type: String, converter: N, reflect: !1, hasChanged: q }, Ut = (r = Pt, t, e) => {
569
+ const Pt = { attribute: !0, type: String, converter: N, reflect: !1, hasChanged: V }, Ut = (r = Pt, t, e) => {
570
570
  const { kind: s, metadata: i } = e;
571
571
  let o = globalThis.litPropertyMetadata.get(i);
572
572
  if (o === void 0 && globalThis.litPropertyMetadata.set(i, o = /* @__PURE__ */ new Map()), s === "setter" && ((r = Object.create(r)).wrapped = !0), o.set(e.name, r), s === "accessor") {
@@ -587,17 +587,21 @@ const Pt = { attribute: !0, type: String, converter: N, reflect: !1, hasChanged:
587
587
  }
588
588
  throw Error("Unsupported decorator location: " + s);
589
589
  };
590
- function Nt(r) {
590
+ function Rt(r) {
591
591
  return (t, e) => typeof e == "object" ? Ut(r, t, e) : ((s, i, o) => {
592
592
  const n = i.hasOwnProperty(o);
593
593
  return i.constructor.createProperty(o, s), n ? Object.getOwnPropertyDescriptor(i, o) : void 0;
594
594
  })(r, t, e);
595
595
  }
596
+ const xt = "--cm-shell-font-family", Lt = rt(
597
+ `var(${xt}, 'General Sans', system-ui, sans-serif)`
598
+ );
596
599
  export {
597
600
  d as A,
601
+ Lt as S,
598
602
  T as a,
599
- Ht as b,
600
- Ot as i,
601
- Nt as n,
602
- Tt as t
603
+ Tt as b,
604
+ Mt as i,
605
+ Rt as n,
606
+ Nt as t
603
607
  };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shell-wide design tokens shared by every chrome component's stylesheet.
3
+ *
4
+ * `mega-menu-layout.ts` is the precedent: a value two or more stylesheets must
5
+ * agree on gets exactly one home, because the failure mode of disagreement is
6
+ * silent. A font is worse than a width that way — a stale copy renders, it just
7
+ * renders wrong, and nothing in the build notices.
8
+ */
9
+ /**
10
+ * The custom property a host document sets to choose the shell's typeface.
11
+ *
12
+ * Exported as a string (not just baked into {@link SHELL_FONT_FAMILY}) so the
13
+ * guard script and tests can assert against one spelling instead of a literal
14
+ * they'd have to keep in sync by hand.
15
+ */
16
+ export declare const SHELL_FONT_FAMILY_VAR = "--cm-shell-font-family";
17
+ /**
18
+ * The shell's font stack, as every component's `:host` should declare it.
19
+ *
20
+ * Deliberately a `var()` with a fallback chain rather than a literal. Two
21
+ * properties of that shape are load-bearing:
22
+ *
23
+ * 1. **It only ever reads `--cm-shell-font-family`, never assigns it.** A host
24
+ * that has not loaded General Sans sets the property and wins. If any
25
+ * `:host` rule in this package assigned it instead, that declaration would
26
+ * beat the host's and the override would be dead on arrival.
27
+ * 2. **Naming General Sans here, rather than shipping the font, is the whole
28
+ * mechanism.** `@font-face` is document-scoped, not shadow-scoped, so faces
29
+ * the host registers are already reachable from inside these shadow roots —
30
+ * the shell only has to ask for them by name. This is the same conclusion
31
+ * `SideNav.styles.ts`'s port notes reached when they deleted the upstream
32
+ * `@import url(fonts.googleapis.com/...)`: a font is something the shell
33
+ * should provide, not something a component fetches for itself.
34
+ *
35
+ * Vendoring the font files here would undo both. The consumer already
36
+ * self-hosts them, and a second copy re-inherits the base-path 404 that
37
+ * AB#26011 fixed downstream.
38
+ */
39
+ export declare const SHELL_FONT_FAMILY: import('lit').CSSResult;
@@ -0,0 +1 @@
1
+
@@ -25,6 +25,15 @@ export declare class MegaMenu extends LitElement {
25
25
  disconnectedCallback(): void;
26
26
  private handleKeyDown;
27
27
  private get categoryData();
28
+ /**
29
+ * The close event keeps its name, `bubbles` and `composed` flags; `detail` is
30
+ * new and carries the caller's focus intent onward to `<cm-side-nav>`'s
31
+ * `dismissFlyout()`, which is the one place the focus rule is applied. Both
32
+ * keyboard paths ask for restoration — they are the paths where focus is inside
33
+ * the flyout and would otherwise be dropped on the document body. The close
34
+ * button asks too; it is harmless when focus is not inside, because
35
+ * `dismissFlyout` only moves focus that actually sits in the flyout.
36
+ */
28
37
  private handleCloseMegaMenu;
29
38
  /**
30
39
  * Packs menu items into as many 248px columns as the container's own
@@ -40,7 +49,6 @@ export declare class MegaMenu extends LitElement {
40
49
  */
41
50
  private setMenuItemsContainerWidth;
42
51
  updated(changedProperties: Map<string | number | symbol, unknown>): void;
43
- private renderLeaf;
44
52
  render(): import('lit').TemplateResult<1> | null;
45
53
  }
46
54
  declare global {
@@ -0,0 +1,46 @@
1
+ import { LitElement } from 'lit';
2
+ import { MenuTree } from '@campminder/menu-tree';
3
+
4
+ /**
5
+ * `<cm-mega-menu-mobile>` — the mobile drill-down for a category with
6
+ * sub-categories, built new for Floor 5D (AB#26231) rather than ported.
7
+ *
8
+ * Upstream's `MegaMenuMobile` drove this through `MobileModalController`
9
+ * (rewrites `meta[name=viewport]`, five `document`-level touch listeners,
10
+ * `document.body` scroll-lock) and `showMobileSubmenu` / `activeMobileCategory`
11
+ * state that Floor 4 never carried over. None of that comes across — see
12
+ * `AGENTS.md`'s one rule and the plan's Floor 5D section. This component only
13
+ * renders the content for whichever category `<cm-side-nav>` has drilled into;
14
+ * `<cm-side-nav>` owns the `activeMobileCategoryId` state and the overlay/backdrop
15
+ * chrome around it, the same split `<cm-mega-menu>` has with its hover state.
16
+ *
17
+ * `tree` + `categoryId` match `<cm-mega-menu>`'s shape deliberately — matching on
18
+ * `id`, never on the terminology-resolved `label`.
19
+ */
20
+ export declare class MegaMenuMobile extends LitElement {
21
+ static readonly styles: import('lit').CSSResult[];
22
+ tree: MenuTree;
23
+ categoryId: string;
24
+ connectedCallback(): void;
25
+ disconnectedCallback(): void;
26
+ private handleKeyDown;
27
+ private get categoryData();
28
+ /**
29
+ * Requests the host go back to the main mobile menu list. Named `-back`, not
30
+ * `-close`, because it does not dismiss the mobile overlay itself — only
31
+ * `<cm-side-nav>`'s own toggle/backdrop does that (see its doc comment).
32
+ */
33
+ private handleBack;
34
+ /**
35
+ * Required, not decorative: drilling in removes the category button focus
36
+ * was on from the DOM (it belonged to the main list, now unmounted), so
37
+ * without this, focus is dropped onto the document body.
38
+ */
39
+ firstUpdated(): void;
40
+ render(): import('lit').TemplateResult<1> | null;
41
+ }
42
+ declare global {
43
+ interface HTMLElementTagNameMap {
44
+ 'cm-mega-menu-mobile': MegaMenuMobile;
45
+ }
46
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Styles for `<cm-mega-menu-mobile>`, new for Floor 5D (AB#26231) — see that
3
+ * file's doc comment for why this is a rewrite, not a port.
4
+ *
5
+ * `--color-section-bg` is one of the four tokens the plan reserved on
6
+ * `SideNavStyles`'s `:host` for this floor (verified 2026-09-04 that upstream's
7
+ * only consumers of it are the mobile stylesheets); it pierces the shadow
8
+ * boundary from `<cm-side-nav>` the same way `--cm-side-nav-width` does for
9
+ * `MegaMenuStyles`.
10
+ */
11
+ export declare const MegaMenuMobileStyles: import('lit').CSSResult;
@@ -5,13 +5,58 @@ import { MenuTree } from '@campminder/menu-tree';
5
5
  * `<cm-side-nav>` — the left navigation.
6
6
  *
7
7
  * Ported 2026-09-02 from core-js's `SideNav.ts` (`<side-nav>`), core rendering path,
8
- * with the desktop mega-menu flyout added 2026-09-04 (Floor 5) — mounted inside
8
+ * with the desktop mega-menu flyout added 2026-09-04 (Floor 5), and the mobile
9
+ * drill-down variant added 2026-09-17 (Floor 5D, AB#26231) — both mounted inside
9
10
  * this component's own shadow root, not slotted; see the render() seam note below.
10
- * Mobile and the search-bar integration remain out of scope for both floors.
11
+ * The search-bar integration remains out of scope.
12
+ *
13
+ * Floor 5D renders both the desktop flyout and the mobile drill-down on every
14
+ * render and lets `SideNavStyles`'s `@media (max-width: ${MOBILE_BREAKPOINT_PX}px)`
15
+ * decide which is displayed — see that file's header. `mega-menu-layout.ts`'s
16
+ * `MOBILE_BREAKPOINT_PX` is the one place that number lives; both that CSS rule and
17
+ * the `matchMedia` query below read it, so they cannot silently disagree about
18
+ * where the line is.
19
+ *
20
+ * The inactive variant gets `aria-hidden` from `isNarrowViewport`, which
21
+ * `initMobileMediaQuery()`/`teardownMobileMediaQuery()` (below) keep live via a
22
+ * `matchMedia` `change` listener — a first pass at this floor used a one-time
23
+ * `window.innerWidth` read instead, which is wrong: CSS re-evaluates its media
24
+ * query continuously, so a viewport crossing the breakpoint after first render
25
+ * (a desktop resize, a tablet rotation) would have left `aria-hidden` pointing at
26
+ * the *previous* state, exposing the hidden variant to assistive technology and
27
+ * suppressing the visible one — worse than omitting the attribute. `matchMedia`
28
+ * here drives only that attribute; it does not gate which variant is
29
+ * *constructed* (both still always render) and is not what decides which one is
30
+ * visually shown (the CSS media query still is). Guarded for `matchMedia` being
31
+ * absent — true of this repo's jsdom, and kept defensive elsewhere — by falling
32
+ * back to the same static `window.innerWidth` read, un-updating, rather than
33
+ * throwing. The listener is added in `connectedCallback` and removed in
34
+ * `disconnectedCallback`, so it never outlives the element and does not touch
35
+ * `document`/`window`/`document.body` — it targets the `MediaQueryList` object
36
+ * `matchMedia()` returns, not those globals, so it does not trip AC-8's
37
+ * isolation guard (`mega-menu-isolation.test.ts`).
11
38
  */
12
39
  export declare class SideNav extends LitElement {
13
40
  static readonly styles: import('lit').CSSResult[];
14
41
  private hoveredCategory;
42
+ /**
43
+ * Floor 5D's drill-down state machine, built from scratch — Floor 4 never
44
+ * ported `showMobileSubmenu` / `activeMobileCategory`, so there was nothing
45
+ * here to adapt. `mobileMenuOpen` is the mobile overlay itself (opened from
46
+ * `.menu-toggle`); `activeMobileCategoryId` is which category, if any, the
47
+ * overlay has drilled into. Deliberately two separate booleans/ids rather
48
+ * than one union, so "overlay open, showing the main list" and "overlay
49
+ * open, showing a drill-down" are both representable without a sentinel.
50
+ */
51
+ private mobileMenuOpen;
52
+ private activeMobileCategoryId;
53
+ /**
54
+ * Whether the mobile variant is the one CSS is currently showing. Kept live by
55
+ * `matchMedia`'s `change` event where available (see this file's header
56
+ * comment); the initializer is also the fallback value used when it is not.
57
+ */
58
+ private isNarrowViewport;
59
+ private mobileMediaQuery;
15
60
  private hoverTimeout;
16
61
  /**
17
62
  * Collapsed state lives on `<campminder-app-shell>`, keyed per portal — see its
@@ -60,24 +105,87 @@ export declare class SideNav extends LitElement {
60
105
  * getter needs to cache.
61
106
  */
62
107
  private get selectedCategory();
108
+ connectedCallback(): void;
63
109
  disconnectedCallback(): void;
110
+ /** The fallback used where `matchMedia` is unavailable — see the class doc comment. */
111
+ private static readViewportWidthFallback;
112
+ private initMobileMediaQuery;
113
+ private teardownMobileMediaQuery;
114
+ private handleMobileMediaChange;
64
115
  private toggleSidenav;
65
116
  private setSelectedCategory;
66
117
  private handleMenuItemHover;
67
118
  private clearHoverTimeout;
68
119
  private handleMouseEnter;
69
120
  private handleMouseLeave;
121
+ /**
122
+ * The single dismissal entry point for the flyout — every trigger routes through
123
+ * here (the flyout's own Escape and shift-Tab paths, the nav's mouseleave, and
124
+ * the shell's page-level Escape / outside pointer press) so the focus rule is
125
+ * applied in exactly one place and no trigger can bypass it.
126
+ *
127
+ * `restoreFocus` is the caller's *intent*; focus only actually moves when it is
128
+ * also true that focus is currently inside the flyout. A person typing in a form
129
+ * field while the flyout is dismissed keeps their cursor where it is, and a
130
+ * pointer press outside keeps the focus the press itself produces.
131
+ *
132
+ * The focus target is read BEFORE `hoveredCategory` is cleared, and that order is
133
+ * load-bearing: the target is found via the `category-hovered` class, which is
134
+ * derived from `hoveredCategory` in `buildCategoryClass`. Clearing first would
135
+ * leave nothing to find and silently drop focus onto the document body — the very
136
+ * defect this method exists to fix.
137
+ */
138
+ dismissFlyout(restoreFocus: boolean): void;
139
+ private focusIsInsideFlyout;
140
+ /**
141
+ * The category control the flyout was opened from, or the collapse control when
142
+ * that category control no longer exists — it always does, so focus can never
143
+ * land on the document body.
144
+ */
145
+ private focusRestoreTarget;
70
146
  private get hamburgerIcon();
71
147
  /**
72
148
  * The category the mega-menu flyout is open for, or `undefined` when none
73
149
  * is. Guards on `renderableSubCategories(category).length > 0` (the same
74
- * predicate `hasSubmenus` uses for the chevron above) rather than on
150
+ * predicate `hasSubmenus` uses to render a category as a button) rather than on
75
151
  * `hoveredCategory` alone — `hoveredCategory` can also be the literal
76
152
  * string `'sidebar-toggle'` (see `handleMouseEnter` call sites), and a
77
153
  * category with no renderable sub-categories must not open onto nothing.
78
154
  */
79
155
  private get openCategory();
80
156
  private buildCategoryClass;
157
+ /** The category the mobile drill-down is showing, mirroring `openCategory`. */
158
+ private get openMobileCategory();
159
+ private toggleMobileMenu;
160
+ /**
161
+ * The single dismissal entry point for the mobile overlay — every trigger
162
+ * (the backdrop click, `.menu-toggle`, the overlay's own Escape) routes
163
+ * through here, mirroring `dismissFlyout`'s reasoning, so the focus rule is
164
+ * applied in exactly one place and no trigger can bypass it.
165
+ *
166
+ * `restoreFocus` is the caller's intent; focus only actually moves when it is
167
+ * also true that focus is currently inside the overlay. The target is read
168
+ * BEFORE state is cleared, same load-bearing order as `dismissFlyout` — the
169
+ * restore target is `.menu-toggle`, which stays in the DOM regardless of
170
+ * `mobileMenuOpen`, but focus-inside-overlay is only answerable while the
171
+ * overlay is still open.
172
+ */
173
+ dismissMobileMenu(restoreFocus: boolean): void;
174
+ private focusIsInsideMobileOverlay;
175
+ private openMobileSubmenu;
176
+ private closeMobileSubmenu;
177
+ private handleMobileOverlayKeyDown;
178
+ private buildMobileCategoryClass;
179
+ private getMobileCategoryHtml;
180
+ private renderMobileMainMenu;
181
+ /**
182
+ * The mobile overlay's body — either the main category list, or the
183
+ * drill-down for whichever category is active. `cm-mega-menu-mobile-back`
184
+ * is the drill-down asking to return to the list; it does not close the
185
+ * overlay (that's `.menu-toggle` / the backdrop), so it only clears
186
+ * `activeMobileCategoryId`.
187
+ */
188
+ private renderMobileOverlayBody;
81
189
  private getCategoryHtml;
82
190
  private renderMainMenu;
83
191
  render(): import('lit').TemplateResult<1>;
@@ -38,5 +38,14 @@
38
38
  * 2026-09-04, Floor 5 added `position: relative` to `.nav-container` as the
39
39
  * mega-menu flyout's positioning context. `.nav-main-content` was deliberately
40
40
  * left at `overflow: hidden` — see that rule's own comment below for why.
41
+ *
42
+ * 2026-09-17, Floor 5D (AB#26231) added the `@media (max-width: 530px)` block
43
+ * that picks the mobile variant over the desktop one — the whole of what makes
44
+ * "both variants render, CSS chooses" true — and ungated `.menu-toggle` /
45
+ * `.toggle-mobile-btn` (parked since Floor 5) and `.chevron-right` (parked
46
+ * since Floor 4) for the mobile category list's own use. `--overlay-menu-width`,
47
+ * `--overlay-menu-max-width`, `--color-backdrop` and `--color-section-bg` were
48
+ * reserved on `:host` since Floor 5 for exactly this floor; they now size the
49
+ * mobile overlay panel and its backdrop.
41
50
  */
42
51
  export declare const SideNavStyles: import('lit').CSSResult;
@@ -0,0 +1,3 @@
1
+ import { MenuLeaf } from '@campminder/menu-tree';
2
+
3
+ export declare function renderMenuLeaf(leaf: MenuLeaf): import('lit').TemplateResult<1>;
@@ -7,3 +7,14 @@
7
7
  */
8
8
  export declare const MENU_ITEM_WIDTH_PX = 248;
9
9
  export declare const MENU_ITEM_GUTTER_PX = 16;
10
+ /**
11
+ * The desktop-flyout / mobile-drill-down breakpoint, Floor 5D (AB#26231).
12
+ * `SideNav.styles.ts`'s `@media (max-width: ${MOBILE_BREAKPOINT_PX}px)` (which
13
+ * variant is actually shown) and `SideNav.ts`'s `matchMedia` query (which one
14
+ * is marked `aria-hidden`) both read this one constant, so the two cannot
15
+ * silently disagree about where the line is — the same drift class the
16
+ * mega-menu constants above already guard against. Kept at upstream's `530`;
17
+ * revisit when `campminder-ui`'s breakpoints land (per the plan's Floor 5D
18
+ * ruling 3).
19
+ */
20
+ export declare const MOBILE_BREAKPOINT_PX = 530;
@@ -0,0 +1 @@
1
+ export {};