@penn-libraries/web 1.4.0-dev.2 → 1.4.0-dev.4

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.
Files changed (61) hide show
  1. package/dist/cjs/{index-nu4ClYFS.js → index-BKWYkqL-.js} +4 -4
  2. package/dist/cjs/index-BKWYkqL-.js.map +1 -0
  3. package/dist/cjs/index.cjs.js +1 -1
  4. package/dist/cjs/loader.cjs.js +2 -2
  5. package/dist/cjs/pennlibs-accordion.pennlibs-expand-text.pennlibs-feedback.pennlibs-footer.pennlibs-header.entry.cjs.js.map +1 -0
  6. package/dist/cjs/{pennlibs-expand-text_4.cjs.entry.js → pennlibs-accordion_5.cjs.entry.js} +168 -2
  7. package/dist/cjs/pennlibs-autocomplete.cjs.entry.js +1 -1
  8. package/dist/cjs/pennlibs-banner.cjs.entry.js +1 -1
  9. package/dist/cjs/pennlibs-chat.cjs.entry.js +1 -1
  10. package/dist/cjs/pennlibs-fallback-img.cjs.entry.js +1 -1
  11. package/dist/cjs/pennlibs-hero.cjs.entry.js +1 -1
  12. package/dist/cjs/pennlibs-iiif-img.cjs.entry.js +1 -1
  13. package/dist/cjs/web.cjs.js +2 -2
  14. package/dist/collection/collection-manifest.json +1 -0
  15. package/dist/collection/components/pennlibs-accordion/pennlibs-accordion.css +9 -0
  16. package/dist/collection/components/pennlibs-accordion/pennlibs-accordion.js +228 -0
  17. package/dist/collection/components/pennlibs-accordion/pennlibs-accordion.js.map +1 -0
  18. package/dist/components/index.d.ts +2 -0
  19. package/dist/components/index.js +1 -0
  20. package/dist/components/index.js.map +1 -1
  21. package/dist/components/pennlibs-accordion.d.ts +11 -0
  22. package/dist/components/pennlibs-accordion.js +192 -0
  23. package/dist/components/pennlibs-accordion.js.map +1 -0
  24. package/dist/docs.json +71 -1
  25. package/dist/{web/p-DJQvPLp9.js → esm/index-Cw9fYoti.js} +4 -4
  26. package/dist/esm/index-Cw9fYoti.js.map +1 -0
  27. package/dist/esm/index.js +1 -1
  28. package/dist/esm/loader.js +3 -3
  29. package/dist/esm/pennlibs-accordion.pennlibs-expand-text.pennlibs-feedback.pennlibs-footer.pennlibs-header.entry.js.map +1 -0
  30. package/dist/{web/p-e0cb5433.entry.js → esm/pennlibs-accordion_5.entry.js} +168 -3
  31. package/dist/esm/pennlibs-autocomplete.entry.js +1 -1
  32. package/dist/esm/pennlibs-banner.entry.js +1 -1
  33. package/dist/esm/pennlibs-chat.entry.js +1 -1
  34. package/dist/esm/pennlibs-fallback-img.entry.js +1 -1
  35. package/dist/esm/pennlibs-hero.entry.js +1 -1
  36. package/dist/esm/pennlibs-iiif-img.entry.js +1 -1
  37. package/dist/esm/web.js +3 -3
  38. package/dist/types/components/pennlibs-accordion/pennlibs-accordion.d.ts +44 -0
  39. package/dist/types/components.d.ts +89 -0
  40. package/dist/web/index.esm.js +1 -1
  41. package/dist/web/{p-33f42a73.entry.js → p-0eb9e4fa.entry.js} +1 -1
  42. package/dist/web/{p-c5190071.entry.js → p-1c284e42.entry.js} +1 -1
  43. package/dist/web/{p-aac371c4.entry.js → p-4f2ff6ec.entry.js} +1 -1
  44. package/dist/web/{p-3198c994.entry.js → p-85b7d695.entry.js} +1 -1
  45. package/dist/web/{p-c0ad70c7.entry.js → p-8d7c5424.entry.js} +1 -1
  46. package/dist/{esm/index-DJQvPLp9.js → web/p-Cw9fYoti.js} +4 -4
  47. package/dist/web/p-Cw9fYoti.js.map +1 -0
  48. package/dist/web/{p-a917779a.entry.js → p-af10bd16.entry.js} +1 -1
  49. package/dist/{esm/pennlibs-expand-text_4.entry.js → web/p-ca1412b5.entry.js} +168 -3
  50. package/dist/web/pennlibs-accordion.pennlibs-expand-text.pennlibs-feedback.pennlibs-footer.pennlibs-header.entry.esm.js.map +1 -0
  51. package/dist/web/web.css +110 -0
  52. package/dist/web/web.esm.js +3 -3
  53. package/hydrate/index.js +197 -1
  54. package/hydrate/index.mjs +197 -1
  55. package/package.json +1 -1
  56. package/dist/cjs/index-nu4ClYFS.js.map +0 -1
  57. package/dist/cjs/pennlibs-expand-text.pennlibs-feedback.pennlibs-footer.pennlibs-header.entry.cjs.js.map +0 -1
  58. package/dist/esm/index-DJQvPLp9.js.map +0 -1
  59. package/dist/esm/pennlibs-expand-text.pennlibs-feedback.pennlibs-footer.pennlibs-header.entry.js.map +0 -1
  60. package/dist/web/p-DJQvPLp9.js.map +0 -1
  61. package/dist/web/pennlibs-expand-text.pennlibs-feedback.pennlibs-footer.pennlibs-header.entry.esm.js.map +0 -1
package/hydrate/index.js CHANGED
@@ -2312,7 +2312,7 @@ var updateComponent = async (hostRef, instance, isInitialLoad) => {
2312
2312
  };
2313
2313
  var callRender = (hostRef, instance, elm, isInitialLoad) => {
2314
2314
  try {
2315
- instance = instance.render() ;
2315
+ instance = instance.render && instance.render();
2316
2316
  {
2317
2317
  hostRef.$flags$ &= -17 /* isQueuedForUpdate */;
2318
2318
  }
@@ -3331,6 +3331,201 @@ var setScopedSSR = (opts) => {
3331
3331
  var needsScopedSSR = () => scopedSSR;
3332
3332
  var scopedSSR = false;
3333
3333
 
3334
+ const pennlibsAccordionCss = "pennlibs-accordion{display:contents}";
3335
+
3336
+ let accordionUid = 0;
3337
+ /**
3338
+ * Composes a group of native `<details class="pl-details">` disclosures into an
3339
+ * accordion. The markup is authored as plain styled details elements (see the
3340
+ * Details pattern) so it works and is accessible without JavaScript; once this
3341
+ * component loads it layers on the WAI-ARIA Accordion behavior: keyboard
3342
+ * navigation between headers, region wiring, and an optional single-open mode.
3343
+ *
3344
+ * To expand or collapse every item at once, add a control with
3345
+ * `data-pl-accordion-toggle`. With no value it controls the closest enclosing
3346
+ * accordion; set the value to an accordion's id to control one from elsewhere
3347
+ * on the page. Add `data-pl-accordion-toggle-deep` to also reach nested
3348
+ * accordions, or give it a number to reach that many levels down.
3349
+ * `data-pl-expand-label` and `data-pl-collapse-label` swap the control's text
3350
+ * to match the state.
3351
+ *
3352
+ * This is a light-DOM component. It does not render its own template; it only
3353
+ * enhances the `<details>` children it is given.
3354
+ *
3355
+ * @slot - One or more details items, each styled with the pl-details classes.
3356
+ */
3357
+ class Accordion {
3358
+ constructor(hostRef) {
3359
+ registerInstance(this, hostRef);
3360
+ /**
3361
+ * When set, only one panel can be open at a time, so opening one closes the
3362
+ * others, like a classic accordion.
3363
+ * @prop single
3364
+ */
3365
+ this.single = false;
3366
+ this.uid = accordionUid++;
3367
+ this.items = [];
3368
+ this.summaries = [];
3369
+ // Roving focus between headers per the WAI-ARIA Accordion keyboard model.
3370
+ // Enter/Space toggling is left to the browser's native <details> behavior.
3371
+ this.onKeydown = (event) => {
3372
+ var _a;
3373
+ const current = event.currentTarget;
3374
+ const index = this.summaries.indexOf(current);
3375
+ if (index === -1)
3376
+ return;
3377
+ const count = this.summaries.length;
3378
+ let next;
3379
+ switch (event.key) {
3380
+ case 'ArrowDown':
3381
+ next = (index + 1) % count;
3382
+ break;
3383
+ case 'ArrowUp':
3384
+ next = (index - 1 + count) % count;
3385
+ break;
3386
+ case 'Home':
3387
+ next = 0;
3388
+ break;
3389
+ case 'End':
3390
+ next = count - 1;
3391
+ break;
3392
+ default:
3393
+ return;
3394
+ }
3395
+ event.preventDefault();
3396
+ (_a = this.summaries[next]) === null || _a === void 0 ? void 0 : _a.focus();
3397
+ };
3398
+ this.onToggle = (event) => {
3399
+ if (!this.single)
3400
+ return;
3401
+ const details = event.target;
3402
+ if (!details.open)
3403
+ return;
3404
+ this.items.forEach((other) => {
3405
+ if (other !== details && other.open)
3406
+ other.open = false;
3407
+ });
3408
+ };
3409
+ }
3410
+ componentDidLoad() {
3411
+ this.collectItems();
3412
+ this.enhanceItems();
3413
+ this.syncControls(this.el, this.items.some((details) => details.open));
3414
+ }
3415
+ disconnectedCallback() {
3416
+ this.summaries.forEach((summary) => summary.removeEventListener('keydown', this.onKeydown));
3417
+ this.items.forEach((details) => details.removeEventListener('toggle', this.onToggle));
3418
+ }
3419
+ collectItems() {
3420
+ this.items = Array.from(this.el.querySelectorAll(':scope > details, :scope > .pl-details'));
3421
+ this.summaries = this.items
3422
+ .map((details) => details.querySelector(':scope > summary'))
3423
+ .filter((summary) => summary instanceof HTMLElement);
3424
+ }
3425
+ enhanceItems() {
3426
+ this.items.forEach((details, index) => {
3427
+ const summary = details.querySelector(':scope > summary');
3428
+ if (!summary)
3429
+ return;
3430
+ const panel = summary.nextElementSibling;
3431
+ const baseId = `pl-accordion-${this.uid}-${index}`;
3432
+ // Wire the summary (the disclosure button) to its panel as a labelled
3433
+ // region. Native <summary> already exposes the button role and
3434
+ // aria-expanded, so we only add what it lacks.
3435
+ if (!summary.id)
3436
+ summary.id = `${baseId}-summary`;
3437
+ if (panel) {
3438
+ if (!panel.id)
3439
+ panel.id = `${baseId}-panel`;
3440
+ if (!panel.hasAttribute('role'))
3441
+ panel.setAttribute('role', 'region');
3442
+ if (!panel.hasAttribute('aria-labelledby'))
3443
+ panel.setAttribute('aria-labelledby', summary.id);
3444
+ summary.setAttribute('aria-controls', panel.id);
3445
+ }
3446
+ summary.addEventListener('keydown', this.onKeydown);
3447
+ details.addEventListener('toggle', this.onToggle);
3448
+ });
3449
+ }
3450
+ // A declarative toggle-all control: any element marked with
3451
+ // `data-pl-accordion-toggle`. With no value it controls the closest enclosing
3452
+ // accordion; set the value to an accordion's id to control one from elsewhere
3453
+ // on the page. Add `data-pl-accordion-toggle-deep` to also reach nested
3454
+ // accordions, or a number for that many levels. `data-pl-expand-label` and
3455
+ // `data-pl-collapse-label` swap its text to match the state.
3456
+ onControlClick(event) {
3457
+ var _a;
3458
+ const control = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('[data-pl-accordion-toggle]');
3459
+ if (!control || this.controlTarget(control) !== this.el)
3460
+ return;
3461
+ event.preventDefault();
3462
+ const expanded = !this.items.some((details) => details.open);
3463
+ this.setAll(this.el, expanded, this.controlDepth(control));
3464
+ }
3465
+ // The accordion a control points at: the element whose id is in the attribute
3466
+ // value, or the closest enclosing accordion when the value is empty.
3467
+ controlTarget(control) {
3468
+ const id = control.getAttribute('data-pl-accordion-toggle');
3469
+ return id ? document.getElementById(id) : control.closest('pennlibs-accordion');
3470
+ }
3471
+ // How many nested levels a control reaches: absent is none, a bare attribute
3472
+ // is every level, and a non-negative number is that many levels down.
3473
+ controlDepth(control) {
3474
+ if (!control.hasAttribute('data-pl-accordion-toggle-deep'))
3475
+ return 0;
3476
+ const raw = control.getAttribute('data-pl-accordion-toggle-deep');
3477
+ if (raw === null || raw === '')
3478
+ return Infinity;
3479
+ const levels = parseInt(raw, 10);
3480
+ return Number.isFinite(levels) && levels >= 0 ? levels : Infinity;
3481
+ }
3482
+ // Expand or collapse every item in an accordion, pushing the same direction
3483
+ // `levels` deep. Each level reads its own single-open rule, so nothing forces
3484
+ // two panels open in a single-open group.
3485
+ setAll(accordion, expanded, levels) {
3486
+ const items = Array.from(accordion.querySelectorAll(':scope > details, :scope > .pl-details'));
3487
+ if (expanded && accordion.single) {
3488
+ items.forEach((details, index) => (details.open = index === 0));
3489
+ }
3490
+ else {
3491
+ items.forEach((details) => (details.open = expanded));
3492
+ }
3493
+ this.syncControls(accordion, expanded);
3494
+ if (levels > 0) {
3495
+ this.nestedAccordions(accordion).forEach((nested) => this.setAll(nested, expanded, levels - 1));
3496
+ }
3497
+ }
3498
+ // The accordions one level down from the given one (not their descendants).
3499
+ nestedAccordions(accordion) {
3500
+ return Array.from(accordion.querySelectorAll('pennlibs-accordion')).filter((node) => { var _a; return ((_a = node.parentElement) === null || _a === void 0 ? void 0 : _a.closest('pennlibs-accordion')) === accordion; });
3501
+ }
3502
+ // Keep every control pointing at an accordion in sync with its state.
3503
+ syncControls(accordion, expanded) {
3504
+ document.querySelectorAll('[data-pl-accordion-toggle]').forEach((control) => {
3505
+ if (this.controlTarget(control) !== accordion)
3506
+ return;
3507
+ control.setAttribute('aria-expanded', String(expanded));
3508
+ const label = expanded
3509
+ ? control.getAttribute('data-pl-collapse-label')
3510
+ : control.getAttribute('data-pl-expand-label');
3511
+ if (label)
3512
+ control.textContent = label;
3513
+ });
3514
+ }
3515
+ get el() { return getElement(this); }
3516
+ static get style() { return pennlibsAccordionCss; }
3517
+ static get cmpMeta() { return {
3518
+ "$flags$": 0,
3519
+ "$tagName$": "pennlibs-accordion",
3520
+ "$members$": {
3521
+ "single": [4]
3522
+ },
3523
+ "$listeners$": [[4, "click", "onControlClick"]],
3524
+ "$lazyBundleId$": "-",
3525
+ "$attrsToReflect$": []
3526
+ }; }
3527
+ }
3528
+
3334
3529
  const pennlibsAutocompleteCss = ":host {\n display: block;\n width: 100%;\n border-radius: 1.25rem;\n padding: 0;\n border-top: 0;\n position: relative;\n}\n\n[role=listbox] {\n position: absolute;\n margin-top: var(--pl-space-xs);\n background: var(--pl-color-bg-default);\n border-radius: 1.25rem;\n box-shadow: rgba(140, 149, 159, 0.3) 0px 8px 24px 0px;\n width: 100%;\n overflow: hidden;\n z-index: 1;\n\n display: flex;\n flex-direction: column;\n}\n\np {\n margin: 0;\n font-size: var(--pl-font-size-s);\n color: var(--pl-color-fg-subtle);\n padding: var(--pl-space-xs) calc(var(--pl-space-m) + var(--pl-space-2xs));\n font-size: var(--pl-font-size-s);\n order: 2;\n font-weight: 500;\n background: var(--pl-color-bg-subtle);\n border-radius: 0 0 1.25rem 1.25rem;\n\n display: flex;\n gap: var(--pl-space-s) var(--pl-space-l);\n flex-wrap: wrap;\n}\n\nol {\n list-style: none;\n margin: 0;\n padding: var(--pl-space-xs) 0;\n order: 1;\n}\n\n[role=option] {\n color: var(--pl-color-fg-default);\n padding: var(--pl-space-s) calc(var(--pl-space-m) + var(--pl-space-2xs));\n text-decoration: none;\n font-weight: 700; \n\n &:hover {\n cursor: pointer;\n }\n\n &:hover,\n &:focus {\n text-decoration-thickness: 2px;\n text-underline-offset: 2px;\n text-decoration: underline;\n }\n}\n\n[aria-selected=true] {\n text-decoration-thickness: 2px;\n text-underline-offset: 2px;\n text-decoration: underline;\n}\n\nmark,\nb {\n background: none;\n font-weight: 400;\n}\n\n.suggestion--border {\n border-bottom: solid 1px rgb(from var(--pl-color-fg-default) r g b / 0.2);\n padding-bottom: calc(var(--pl-space-2xs) + var(--pl-space-s));\n margin-bottom: var(--pl-space-2xs);\n}";
3335
3530
 
3336
3531
  /**
@@ -4422,6 +4617,7 @@ class PennlibsFeedback {
4422
4617
  }
4423
4618
 
4424
4619
  registerComponents([
4620
+ Accordion,
4425
4621
  Autocomplete,
4426
4622
  Banner,
4427
4623
  Chat,
package/hydrate/index.mjs CHANGED
@@ -2310,7 +2310,7 @@ var updateComponent = async (hostRef, instance, isInitialLoad) => {
2310
2310
  };
2311
2311
  var callRender = (hostRef, instance, elm, isInitialLoad) => {
2312
2312
  try {
2313
- instance = instance.render() ;
2313
+ instance = instance.render && instance.render();
2314
2314
  {
2315
2315
  hostRef.$flags$ &= -17 /* isQueuedForUpdate */;
2316
2316
  }
@@ -3329,6 +3329,201 @@ var setScopedSSR = (opts) => {
3329
3329
  var needsScopedSSR = () => scopedSSR;
3330
3330
  var scopedSSR = false;
3331
3331
 
3332
+ const pennlibsAccordionCss = "pennlibs-accordion{display:contents}";
3333
+
3334
+ let accordionUid = 0;
3335
+ /**
3336
+ * Composes a group of native `<details class="pl-details">` disclosures into an
3337
+ * accordion. The markup is authored as plain styled details elements (see the
3338
+ * Details pattern) so it works and is accessible without JavaScript; once this
3339
+ * component loads it layers on the WAI-ARIA Accordion behavior: keyboard
3340
+ * navigation between headers, region wiring, and an optional single-open mode.
3341
+ *
3342
+ * To expand or collapse every item at once, add a control with
3343
+ * `data-pl-accordion-toggle`. With no value it controls the closest enclosing
3344
+ * accordion; set the value to an accordion's id to control one from elsewhere
3345
+ * on the page. Add `data-pl-accordion-toggle-deep` to also reach nested
3346
+ * accordions, or give it a number to reach that many levels down.
3347
+ * `data-pl-expand-label` and `data-pl-collapse-label` swap the control's text
3348
+ * to match the state.
3349
+ *
3350
+ * This is a light-DOM component. It does not render its own template; it only
3351
+ * enhances the `<details>` children it is given.
3352
+ *
3353
+ * @slot - One or more details items, each styled with the pl-details classes.
3354
+ */
3355
+ class Accordion {
3356
+ constructor(hostRef) {
3357
+ registerInstance(this, hostRef);
3358
+ /**
3359
+ * When set, only one panel can be open at a time, so opening one closes the
3360
+ * others, like a classic accordion.
3361
+ * @prop single
3362
+ */
3363
+ this.single = false;
3364
+ this.uid = accordionUid++;
3365
+ this.items = [];
3366
+ this.summaries = [];
3367
+ // Roving focus between headers per the WAI-ARIA Accordion keyboard model.
3368
+ // Enter/Space toggling is left to the browser's native <details> behavior.
3369
+ this.onKeydown = (event) => {
3370
+ var _a;
3371
+ const current = event.currentTarget;
3372
+ const index = this.summaries.indexOf(current);
3373
+ if (index === -1)
3374
+ return;
3375
+ const count = this.summaries.length;
3376
+ let next;
3377
+ switch (event.key) {
3378
+ case 'ArrowDown':
3379
+ next = (index + 1) % count;
3380
+ break;
3381
+ case 'ArrowUp':
3382
+ next = (index - 1 + count) % count;
3383
+ break;
3384
+ case 'Home':
3385
+ next = 0;
3386
+ break;
3387
+ case 'End':
3388
+ next = count - 1;
3389
+ break;
3390
+ default:
3391
+ return;
3392
+ }
3393
+ event.preventDefault();
3394
+ (_a = this.summaries[next]) === null || _a === void 0 ? void 0 : _a.focus();
3395
+ };
3396
+ this.onToggle = (event) => {
3397
+ if (!this.single)
3398
+ return;
3399
+ const details = event.target;
3400
+ if (!details.open)
3401
+ return;
3402
+ this.items.forEach((other) => {
3403
+ if (other !== details && other.open)
3404
+ other.open = false;
3405
+ });
3406
+ };
3407
+ }
3408
+ componentDidLoad() {
3409
+ this.collectItems();
3410
+ this.enhanceItems();
3411
+ this.syncControls(this.el, this.items.some((details) => details.open));
3412
+ }
3413
+ disconnectedCallback() {
3414
+ this.summaries.forEach((summary) => summary.removeEventListener('keydown', this.onKeydown));
3415
+ this.items.forEach((details) => details.removeEventListener('toggle', this.onToggle));
3416
+ }
3417
+ collectItems() {
3418
+ this.items = Array.from(this.el.querySelectorAll(':scope > details, :scope > .pl-details'));
3419
+ this.summaries = this.items
3420
+ .map((details) => details.querySelector(':scope > summary'))
3421
+ .filter((summary) => summary instanceof HTMLElement);
3422
+ }
3423
+ enhanceItems() {
3424
+ this.items.forEach((details, index) => {
3425
+ const summary = details.querySelector(':scope > summary');
3426
+ if (!summary)
3427
+ return;
3428
+ const panel = summary.nextElementSibling;
3429
+ const baseId = `pl-accordion-${this.uid}-${index}`;
3430
+ // Wire the summary (the disclosure button) to its panel as a labelled
3431
+ // region. Native <summary> already exposes the button role and
3432
+ // aria-expanded, so we only add what it lacks.
3433
+ if (!summary.id)
3434
+ summary.id = `${baseId}-summary`;
3435
+ if (panel) {
3436
+ if (!panel.id)
3437
+ panel.id = `${baseId}-panel`;
3438
+ if (!panel.hasAttribute('role'))
3439
+ panel.setAttribute('role', 'region');
3440
+ if (!panel.hasAttribute('aria-labelledby'))
3441
+ panel.setAttribute('aria-labelledby', summary.id);
3442
+ summary.setAttribute('aria-controls', panel.id);
3443
+ }
3444
+ summary.addEventListener('keydown', this.onKeydown);
3445
+ details.addEventListener('toggle', this.onToggle);
3446
+ });
3447
+ }
3448
+ // A declarative toggle-all control: any element marked with
3449
+ // `data-pl-accordion-toggle`. With no value it controls the closest enclosing
3450
+ // accordion; set the value to an accordion's id to control one from elsewhere
3451
+ // on the page. Add `data-pl-accordion-toggle-deep` to also reach nested
3452
+ // accordions, or a number for that many levels. `data-pl-expand-label` and
3453
+ // `data-pl-collapse-label` swap its text to match the state.
3454
+ onControlClick(event) {
3455
+ var _a;
3456
+ const control = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('[data-pl-accordion-toggle]');
3457
+ if (!control || this.controlTarget(control) !== this.el)
3458
+ return;
3459
+ event.preventDefault();
3460
+ const expanded = !this.items.some((details) => details.open);
3461
+ this.setAll(this.el, expanded, this.controlDepth(control));
3462
+ }
3463
+ // The accordion a control points at: the element whose id is in the attribute
3464
+ // value, or the closest enclosing accordion when the value is empty.
3465
+ controlTarget(control) {
3466
+ const id = control.getAttribute('data-pl-accordion-toggle');
3467
+ return id ? document.getElementById(id) : control.closest('pennlibs-accordion');
3468
+ }
3469
+ // How many nested levels a control reaches: absent is none, a bare attribute
3470
+ // is every level, and a non-negative number is that many levels down.
3471
+ controlDepth(control) {
3472
+ if (!control.hasAttribute('data-pl-accordion-toggle-deep'))
3473
+ return 0;
3474
+ const raw = control.getAttribute('data-pl-accordion-toggle-deep');
3475
+ if (raw === null || raw === '')
3476
+ return Infinity;
3477
+ const levels = parseInt(raw, 10);
3478
+ return Number.isFinite(levels) && levels >= 0 ? levels : Infinity;
3479
+ }
3480
+ // Expand or collapse every item in an accordion, pushing the same direction
3481
+ // `levels` deep. Each level reads its own single-open rule, so nothing forces
3482
+ // two panels open in a single-open group.
3483
+ setAll(accordion, expanded, levels) {
3484
+ const items = Array.from(accordion.querySelectorAll(':scope > details, :scope > .pl-details'));
3485
+ if (expanded && accordion.single) {
3486
+ items.forEach((details, index) => (details.open = index === 0));
3487
+ }
3488
+ else {
3489
+ items.forEach((details) => (details.open = expanded));
3490
+ }
3491
+ this.syncControls(accordion, expanded);
3492
+ if (levels > 0) {
3493
+ this.nestedAccordions(accordion).forEach((nested) => this.setAll(nested, expanded, levels - 1));
3494
+ }
3495
+ }
3496
+ // The accordions one level down from the given one (not their descendants).
3497
+ nestedAccordions(accordion) {
3498
+ return Array.from(accordion.querySelectorAll('pennlibs-accordion')).filter((node) => { var _a; return ((_a = node.parentElement) === null || _a === void 0 ? void 0 : _a.closest('pennlibs-accordion')) === accordion; });
3499
+ }
3500
+ // Keep every control pointing at an accordion in sync with its state.
3501
+ syncControls(accordion, expanded) {
3502
+ document.querySelectorAll('[data-pl-accordion-toggle]').forEach((control) => {
3503
+ if (this.controlTarget(control) !== accordion)
3504
+ return;
3505
+ control.setAttribute('aria-expanded', String(expanded));
3506
+ const label = expanded
3507
+ ? control.getAttribute('data-pl-collapse-label')
3508
+ : control.getAttribute('data-pl-expand-label');
3509
+ if (label)
3510
+ control.textContent = label;
3511
+ });
3512
+ }
3513
+ get el() { return getElement(this); }
3514
+ static get style() { return pennlibsAccordionCss; }
3515
+ static get cmpMeta() { return {
3516
+ "$flags$": 0,
3517
+ "$tagName$": "pennlibs-accordion",
3518
+ "$members$": {
3519
+ "single": [4]
3520
+ },
3521
+ "$listeners$": [[4, "click", "onControlClick"]],
3522
+ "$lazyBundleId$": "-",
3523
+ "$attrsToReflect$": []
3524
+ }; }
3525
+ }
3526
+
3332
3527
  const pennlibsAutocompleteCss = ":host {\n display: block;\n width: 100%;\n border-radius: 1.25rem;\n padding: 0;\n border-top: 0;\n position: relative;\n}\n\n[role=listbox] {\n position: absolute;\n margin-top: var(--pl-space-xs);\n background: var(--pl-color-bg-default);\n border-radius: 1.25rem;\n box-shadow: rgba(140, 149, 159, 0.3) 0px 8px 24px 0px;\n width: 100%;\n overflow: hidden;\n z-index: 1;\n\n display: flex;\n flex-direction: column;\n}\n\np {\n margin: 0;\n font-size: var(--pl-font-size-s);\n color: var(--pl-color-fg-subtle);\n padding: var(--pl-space-xs) calc(var(--pl-space-m) + var(--pl-space-2xs));\n font-size: var(--pl-font-size-s);\n order: 2;\n font-weight: 500;\n background: var(--pl-color-bg-subtle);\n border-radius: 0 0 1.25rem 1.25rem;\n\n display: flex;\n gap: var(--pl-space-s) var(--pl-space-l);\n flex-wrap: wrap;\n}\n\nol {\n list-style: none;\n margin: 0;\n padding: var(--pl-space-xs) 0;\n order: 1;\n}\n\n[role=option] {\n color: var(--pl-color-fg-default);\n padding: var(--pl-space-s) calc(var(--pl-space-m) + var(--pl-space-2xs));\n text-decoration: none;\n font-weight: 700; \n\n &:hover {\n cursor: pointer;\n }\n\n &:hover,\n &:focus {\n text-decoration-thickness: 2px;\n text-underline-offset: 2px;\n text-decoration: underline;\n }\n}\n\n[aria-selected=true] {\n text-decoration-thickness: 2px;\n text-underline-offset: 2px;\n text-decoration: underline;\n}\n\nmark,\nb {\n background: none;\n font-weight: 400;\n}\n\n.suggestion--border {\n border-bottom: solid 1px rgb(from var(--pl-color-fg-default) r g b / 0.2);\n padding-bottom: calc(var(--pl-space-2xs) + var(--pl-space-s));\n margin-bottom: var(--pl-space-2xs);\n}";
3333
3528
 
3334
3529
  /**
@@ -4420,6 +4615,7 @@ class PennlibsFeedback {
4420
4615
  }
4421
4616
 
4422
4617
  registerComponents([
4618
+ Accordion,
4423
4619
  Autocomplete,
4424
4620
  Banner,
4425
4621
  Chat,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@penn-libraries/web",
3
- "version": "1.4.0-dev.2",
3
+ "version": "1.4.0-dev.4",
4
4
  "description": "Reusable web designs for University of Pennsylvania Libraries websites. This package contains our code for shared Web Components and Cascading Style Sheets (CSS).",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.js",