@phcdevworks/spectre-components 1.11.0 → 1.12.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
@@ -16,7 +16,7 @@ themselves — can consume Spectre without a framework-specific adapter.
16
16
  | Project team | `project-design` |
17
17
  | Repository role | Spectre L3a Lit web component layer |
18
18
  | Package/artifact | `@phcdevworks/spectre-components` |
19
- | Current version/status | 1.11.0 |
19
+ | Current version/status | 1.12.0 |
20
20
 
21
21
  ## Standard Workflow
22
22
 
@@ -906,6 +906,48 @@ etc.).
906
906
 
907
907
  ---
908
908
 
909
+ ### sp-nav-item
910
+
911
+ Renders a nav link that can optionally become a dropdown trigger, backed by
912
+ the same Spectre dropdown recipe as `sp-dropdown`. Place it inside `sp-nav`
913
+ alongside plain `<a>` links.
914
+
915
+ **Attributes**
916
+
917
+ | Attribute | Type | Default | Description |
918
+ | ------------------------- | ------- | ---------------- | ------------------------------------------------------------------- |
919
+ | `dropdown` | boolean | `false` | Renders a dropdown trigger + menu instead of a plain `<a>` |
920
+ | `href` | string | — | Link target when `dropdown` is `false` |
921
+ | `label` | string | — | Trigger/link text when no content is projected |
922
+ | `open` | boolean | `false` | Open/closed menu state (dropdown mode only) |
923
+ | `placement` | string | `'bottom-start'` | Menu position: `bottom-start`, `bottom-end`, `top-start`, `top-end` |
924
+ | `id` / `title` / `aria-*` | string | — | Forwarded to the rendered `<a>` or trigger `<button>` |
925
+
926
+ **Content projection** — in dropdown mode, children become the menu content;
927
+ a child with `slot="trigger"` becomes the trigger content instead of `label`.
928
+ Menu content can be a plain list of links or an `sp-grid` for a full mega-menu
929
+ layout. In link mode, children become the link's content.
930
+
931
+ **Events** — `sp-open` and `sp-close` (dropdown mode only), both `bubbles`.
932
+
933
+ **Internal targets** — `[data-sp-nav-item-trigger]` selects the trigger
934
+ button; `[data-sp-nav-item-menu]` selects the menu panel.
935
+
936
+ ```html
937
+ <sp-nav>
938
+ <sp-nav-item href="/">Home</sp-nav-item>
939
+ <sp-nav-item dropdown label="Products">
940
+ <sp-grid columns="3" gap="lg">
941
+ <div>Column one</div>
942
+ <div>Column two</div>
943
+ <div>Column three</div>
944
+ </sp-grid>
945
+ </sp-nav-item>
946
+ </sp-nav>
947
+ ```
948
+
949
+ ---
950
+
909
951
  ### sp-sidebar
910
952
 
911
953
  Renders an off-canvas `<aside>` with a toggle button and backdrop, backed by the
@@ -1169,6 +1211,7 @@ Each entry point registers only that component and exports only its surface:
1169
1211
  | `.../section` | `sp-section` | `defineSpectreSection`, `SpectreSectionElement`, `SpectreSectionProps` |
1170
1212
  | `.../stack` | `sp-stack` | `defineSpectreStack`, `SpectreStackElement`, stack constants and types |
1171
1213
  | `.../nav` | `sp-nav` | `defineSpectreNav`, `SpectreNavElement`, `SpectreNavProps` |
1214
+ | `.../nav-item` | `sp-nav-item` | `defineSpectreNavItem`, `SpectreNavItemElement`, `SpectreNavItemProps` |
1172
1215
  | `.../sidebar` | `sp-sidebar` | `defineSpectreSidebar`, `SpectreSidebarElement`, `SpectreSidebarProps` |
1173
1216
  | `.../dropdown` | `sp-dropdown` | `defineSpectreDropdown`, `SpectreDropdownElement`, dropdown constants and types |
1174
1217
  | `.../footer` | `sp-footer` | `defineSpectreFooter`, `SpectreFooterElement`, `SpectreFooterProps` |
package/dist/index.cjs CHANGED
@@ -3452,6 +3452,208 @@ function defineSpectreNav(tagName = "sp-nav") {
3452
3452
  customElements.define(tagName, SpectreNavElement);
3453
3453
  return SpectreNavElement;
3454
3454
  }
3455
+ var SpectreNavItemElement = class extends SpectreBaseElement {
3456
+ constructor() {
3457
+ super(...arguments);
3458
+ this.dropdown = false;
3459
+ this.href = void 0;
3460
+ this.label = void 0;
3461
+ this.open = false;
3462
+ this.placement = "bottom-start";
3463
+ this.triggerContent = [];
3464
+ this.menuContent = [];
3465
+ this.handleDocumentClick = (event) => {
3466
+ if (!this.open) {
3467
+ return;
3468
+ }
3469
+ if (!event.composedPath().includes(this)) {
3470
+ this.close();
3471
+ }
3472
+ };
3473
+ this.handleDocumentKeydown = (event) => {
3474
+ if (event.key === "Escape" && this.open) {
3475
+ this.close();
3476
+ this.triggerElement?.focus();
3477
+ }
3478
+ };
3479
+ }
3480
+ get id() {
3481
+ return super.id;
3482
+ }
3483
+ set id(value) {
3484
+ super.id = value;
3485
+ }
3486
+ get title() {
3487
+ return super.title;
3488
+ }
3489
+ set title(value) {
3490
+ super.title = value;
3491
+ }
3492
+ isInternalNode(node) {
3493
+ if (node.nodeType !== Node.ELEMENT_NODE) {
3494
+ return false;
3495
+ }
3496
+ const el = node;
3497
+ return el.hasAttribute("data-sp-nav-item-menu") || el.hasAttribute("data-sp-nav-item-trigger");
3498
+ }
3499
+ syncProjectedContent() {
3500
+ const nextTriggerContent = [];
3501
+ const nextMenuContent = [];
3502
+ this.childNodes.forEach((node) => {
3503
+ if (this.isInternalNode(node)) {
3504
+ return;
3505
+ }
3506
+ const isTriggerSlot = node.nodeType === Node.ELEMENT_NODE && node.getAttribute("slot") === "trigger";
3507
+ if (isTriggerSlot) {
3508
+ nextTriggerContent.push(node);
3509
+ } else {
3510
+ nextMenuContent.push(node);
3511
+ }
3512
+ });
3513
+ const changed = nextTriggerContent.length !== this.triggerContent.length || nextTriggerContent.some((n, i) => n !== this.triggerContent[i]) || nextMenuContent.length !== this.menuContent.length || nextMenuContent.some((n, i) => n !== this.menuContent[i]);
3514
+ if (changed) {
3515
+ this.triggerContent = nextTriggerContent;
3516
+ this.menuContent = nextMenuContent;
3517
+ }
3518
+ return changed;
3519
+ }
3520
+ get hasTriggerContent() {
3521
+ return hasMeaningfulContent(this.triggerContent);
3522
+ }
3523
+ get hasMenuContent() {
3524
+ return hasMeaningfulContent(this.menuContent);
3525
+ }
3526
+ connectedCallback() {
3527
+ super.connectedCallback();
3528
+ this.syncProjectedContent();
3529
+ this.startContentObserver();
3530
+ document.addEventListener("click", this.handleDocumentClick);
3531
+ document.addEventListener("keydown", this.handleDocumentKeydown);
3532
+ }
3533
+ disconnectedCallback() {
3534
+ this.stopContentObserver();
3535
+ document.removeEventListener("click", this.handleDocumentClick);
3536
+ document.removeEventListener("keydown", this.handleDocumentKeydown);
3537
+ super.disconnectedCallback();
3538
+ }
3539
+ update(changedProperties) {
3540
+ this.stopContentObserver();
3541
+ super.update(changedProperties);
3542
+ this.startContentObserver();
3543
+ }
3544
+ startContentObserver() {
3545
+ if (this.contentObserver) {
3546
+ return;
3547
+ }
3548
+ this.contentObserver = new MutationObserver(() => {
3549
+ if (this.syncProjectedContent()) {
3550
+ this.requestUpdate();
3551
+ }
3552
+ });
3553
+ this.contentObserver.observe(this, { childList: true });
3554
+ }
3555
+ stopContentObserver() {
3556
+ this.contentObserver?.disconnect();
3557
+ this.contentObserver = void 0;
3558
+ }
3559
+ willUpdate(changedProperties) {
3560
+ if (changedProperties.has("dropdown") && this.dropdown == null) {
3561
+ this.dropdown = false;
3562
+ }
3563
+ if (changedProperties.has("open") && this.open == null) {
3564
+ this.open = false;
3565
+ }
3566
+ if (changedProperties.has("placement") && (this.placement == null || !isDropdownPlacement(this.placement))) {
3567
+ this.placement = "bottom-start";
3568
+ }
3569
+ }
3570
+ get triggerElement() {
3571
+ return this.querySelector("[data-sp-nav-item-trigger]");
3572
+ }
3573
+ toggle() {
3574
+ if (this.open) {
3575
+ this.close();
3576
+ } else {
3577
+ this.openMenu();
3578
+ }
3579
+ }
3580
+ openMenu() {
3581
+ if (this.open) {
3582
+ return;
3583
+ }
3584
+ this.open = true;
3585
+ this.dispatchEvent(new CustomEvent("sp-open", { bubbles: true }));
3586
+ }
3587
+ close() {
3588
+ if (!this.open) {
3589
+ return;
3590
+ }
3591
+ this.open = false;
3592
+ this.dispatchEvent(new CustomEvent("sp-close", { bubbles: true }));
3593
+ }
3594
+ get wrapperClasses() {
3595
+ return spectreUi.getDropdownClasses();
3596
+ }
3597
+ get menuClasses() {
3598
+ return spectreUi.getDropdownMenuClasses({
3599
+ open: this.open ?? false,
3600
+ placement: this.placement
3601
+ });
3602
+ }
3603
+ get navLinkClasses() {
3604
+ return spectreUi.getNavLinkClasses();
3605
+ }
3606
+ renderLink() {
3607
+ return lit.html`<a
3608
+ aria-label="${ifDefined_js.ifDefined(this.forwardedAriaLabel)}"
3609
+ class="${this.navLinkClasses}"
3610
+ href="${ifDefined_js.ifDefined(this.href)}"
3611
+ id="${ifDefined_js.ifDefined(this.id || void 0)}"
3612
+ title="${ifDefined_js.ifDefined(this.title || void 0)}"
3613
+ >${this.hasMenuContent ? this.menuContent : this.label}</a
3614
+ >`;
3615
+ }
3616
+ renderDropdown() {
3617
+ return lit.html`<div
3618
+ class="${this.wrapperClasses}"
3619
+ id="${ifDefined_js.ifDefined(this.id || void 0)}"
3620
+ title="${ifDefined_js.ifDefined(this.title || void 0)}"
3621
+ >
3622
+ <button
3623
+ aria-expanded="${this.open ? "true" : "false"}"
3624
+ aria-haspopup="true"
3625
+ aria-label="${ifDefined_js.ifDefined(this.forwardedAriaLabel ?? this.label)}"
3626
+ class="${this.navLinkClasses}"
3627
+ data-sp-nav-item-trigger
3628
+ type="button"
3629
+ @click="${() => this.toggle()}"
3630
+ >
3631
+ ${this.hasTriggerContent ? this.triggerContent : this.label}
3632
+ </button>
3633
+ <div class="${this.menuClasses}" data-sp-nav-item-menu>
3634
+ ${this.hasMenuContent ? this.menuContent : lit.nothing}
3635
+ </div>
3636
+ </div>`;
3637
+ }
3638
+ render() {
3639
+ return this.dropdown ? this.renderDropdown() : this.renderLink();
3640
+ }
3641
+ };
3642
+ SpectreNavItemElement.properties = {
3643
+ dropdown: { type: Boolean, reflect: true },
3644
+ href: { type: String },
3645
+ label: { type: String },
3646
+ open: { type: Boolean, reflect: true },
3647
+ placement: { type: String, reflect: true }
3648
+ };
3649
+ function defineSpectreNavItem(tagName = "sp-nav-item") {
3650
+ const existingElement = customElements.get(tagName);
3651
+ if (existingElement) {
3652
+ return existingElement;
3653
+ }
3654
+ customElements.define(tagName, SpectreNavItemElement);
3655
+ return SpectreNavItemElement;
3656
+ }
3455
3657
  var SpectreSidebarElement = class extends SpectreProjectableElement {
3456
3658
  constructor() {
3457
3659
  super(...arguments);
@@ -4012,6 +4214,7 @@ function defineSpectreComponents() {
4012
4214
  defineSpectreFooter();
4013
4215
  defineSpectreModal();
4014
4216
  defineSpectreNav();
4217
+ defineSpectreNavItem();
4015
4218
  defineSpectreSidebar();
4016
4219
  defineSpectreToast();
4017
4220
  defineSpectreTooltip();
@@ -4034,6 +4237,7 @@ exports.SpectreInputElement = SpectreInputElement;
4034
4237
  exports.SpectreLabelElement = SpectreLabelElement;
4035
4238
  exports.SpectreModalElement = SpectreModalElement;
4036
4239
  exports.SpectreNavElement = SpectreNavElement;
4240
+ exports.SpectreNavItemElement = SpectreNavItemElement;
4037
4241
  exports.SpectrePricingCardElement = SpectrePricingCardElement;
4038
4242
  exports.SpectreRadioElement = SpectreRadioElement;
4039
4243
  exports.SpectreRatingElement = SpectreRatingElement;
@@ -4065,6 +4269,7 @@ exports.defineSpectreInput = defineSpectreInput;
4065
4269
  exports.defineSpectreLabel = defineSpectreLabel;
4066
4270
  exports.defineSpectreModal = defineSpectreModal;
4067
4271
  exports.defineSpectreNav = defineSpectreNav;
4272
+ exports.defineSpectreNavItem = defineSpectreNavItem;
4068
4273
  exports.defineSpectrePricingCard = defineSpectrePricingCard;
4069
4274
  exports.defineSpectreRadio = defineSpectreRadio;
4070
4275
  exports.defineSpectreRating = defineSpectreRating;