@aquera/nile-elements 1.7.8 → 1.8.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.
Files changed (111) hide show
  1. package/README.md +6 -0
  2. package/dist/index.cjs.js +1 -1
  3. package/dist/index.esm.js +1 -1
  4. package/dist/index.js +862 -520
  5. package/dist/nile-context-menu/index.cjs.js +2 -0
  6. package/dist/nile-context-menu/index.cjs.js.map +1 -0
  7. package/dist/nile-context-menu/index.esm.js +1 -0
  8. package/dist/nile-context-menu/nile-context-menu.cjs.js +2 -0
  9. package/dist/nile-context-menu/nile-context-menu.cjs.js.map +1 -0
  10. package/dist/nile-context-menu/nile-context-menu.css.cjs.js +2 -0
  11. package/dist/nile-context-menu/nile-context-menu.css.cjs.js.map +1 -0
  12. package/dist/nile-context-menu/nile-context-menu.css.esm.js +51 -0
  13. package/dist/nile-context-menu/nile-context-menu.esm.js +25 -0
  14. package/dist/nile-context-menu-group/index.cjs.js +2 -0
  15. package/dist/nile-context-menu-group/index.cjs.js.map +1 -0
  16. package/dist/nile-context-menu-group/index.esm.js +1 -0
  17. package/dist/nile-context-menu-group/nile-context-menu-group.cjs.js +2 -0
  18. package/dist/nile-context-menu-group/nile-context-menu-group.cjs.js.map +1 -0
  19. package/dist/nile-context-menu-group/nile-context-menu-group.css.cjs.js +2 -0
  20. package/dist/nile-context-menu-group/nile-context-menu-group.css.cjs.js.map +1 -0
  21. package/dist/nile-context-menu-group/nile-context-menu-group.css.esm.js +20 -0
  22. package/dist/nile-context-menu-group/nile-context-menu-group.esm.js +11 -0
  23. package/dist/nile-context-menu-item/index.cjs.js +2 -0
  24. package/dist/nile-context-menu-item/index.cjs.js.map +1 -0
  25. package/dist/nile-context-menu-item/index.esm.js +1 -0
  26. package/dist/nile-context-menu-item/nile-context-menu-item.cjs.js +2 -0
  27. package/dist/nile-context-menu-item/nile-context-menu-item.cjs.js.map +1 -0
  28. package/dist/nile-context-menu-item/nile-context-menu-item.css.cjs.js +2 -0
  29. package/dist/nile-context-menu-item/nile-context-menu-item.css.cjs.js.map +1 -0
  30. package/dist/nile-context-menu-item/nile-context-menu-item.css.esm.js +72 -0
  31. package/dist/nile-context-menu-item/nile-context-menu-item.esm.js +20 -0
  32. package/dist/nile-context-submenu/index.cjs.js +2 -0
  33. package/dist/nile-context-submenu/index.cjs.js.map +1 -0
  34. package/dist/nile-context-submenu/index.esm.js +1 -0
  35. package/dist/nile-context-submenu/nile-context-submenu.cjs.js +2 -0
  36. package/dist/nile-context-submenu/nile-context-submenu.cjs.js.map +1 -0
  37. package/dist/nile-context-submenu/nile-context-submenu.esm.js +3 -0
  38. package/dist/nile-status-light/index.cjs.js +2 -0
  39. package/dist/nile-status-light/index.cjs.js.map +1 -0
  40. package/dist/nile-status-light/index.esm.js +1 -0
  41. package/dist/nile-status-light/nile-status-light.cjs.js +2 -0
  42. package/dist/nile-status-light/nile-status-light.cjs.js.map +1 -0
  43. package/dist/nile-status-light/nile-status-light.css.cjs.js +2 -0
  44. package/dist/nile-status-light/nile-status-light.css.cjs.js.map +1 -0
  45. package/dist/nile-status-light/nile-status-light.css.esm.js +136 -0
  46. package/dist/nile-status-light/nile-status-light.esm.js +13 -0
  47. package/dist/src/index.d.ts +4 -0
  48. package/dist/src/index.js +4 -0
  49. package/dist/src/index.js.map +1 -1
  50. package/dist/src/nile-context-menu/index.d.ts +3 -0
  51. package/dist/src/nile-context-menu/index.js +4 -0
  52. package/dist/src/nile-context-menu/index.js.map +1 -0
  53. package/dist/src/nile-context-menu/nile-context-menu.css.d.ts +10 -0
  54. package/dist/src/nile-context-menu/nile-context-menu.css.js +127 -0
  55. package/dist/src/nile-context-menu/nile-context-menu.css.js.map +1 -0
  56. package/dist/src/nile-context-menu/nile-context-menu.d.ts +123 -0
  57. package/dist/src/nile-context-menu/nile-context-menu.js +625 -0
  58. package/dist/src/nile-context-menu/nile-context-menu.js.map +1 -0
  59. package/dist/src/nile-context-menu-group/index.d.ts +1 -0
  60. package/dist/src/nile-context-menu-group/index.js +2 -0
  61. package/dist/src/nile-context-menu-group/index.js.map +1 -0
  62. package/dist/src/nile-context-menu-group/nile-context-menu-group.css.d.ts +9 -0
  63. package/dist/src/nile-context-menu-group/nile-context-menu-group.css.js +29 -0
  64. package/dist/src/nile-context-menu-group/nile-context-menu-group.css.js.map +1 -0
  65. package/dist/src/nile-context-menu-group/nile-context-menu-group.d.ts +28 -0
  66. package/dist/src/nile-context-menu-group/nile-context-menu-group.js +55 -0
  67. package/dist/src/nile-context-menu-group/nile-context-menu-group.js.map +1 -0
  68. package/dist/src/nile-context-menu-item/index.d.ts +1 -0
  69. package/dist/src/nile-context-menu-item/index.js +2 -0
  70. package/dist/src/nile-context-menu-item/index.js.map +1 -0
  71. package/dist/src/nile-context-menu-item/nile-context-menu-item.css.d.ts +9 -0
  72. package/dist/src/nile-context-menu-item/nile-context-menu-item.css.js +81 -0
  73. package/dist/src/nile-context-menu-item/nile-context-menu-item.css.js.map +1 -0
  74. package/dist/src/nile-context-menu-item/nile-context-menu-item.d.ts +45 -0
  75. package/dist/src/nile-context-menu-item/nile-context-menu-item.js +96 -0
  76. package/dist/src/nile-context-menu-item/nile-context-menu-item.js.map +1 -0
  77. package/dist/src/nile-context-submenu/index.d.ts +1 -0
  78. package/dist/src/nile-context-submenu/index.js +2 -0
  79. package/dist/src/nile-context-submenu/index.js.map +1 -0
  80. package/dist/src/nile-context-submenu/nile-context-submenu.d.ts +60 -0
  81. package/dist/src/nile-context-submenu/nile-context-submenu.js +324 -0
  82. package/dist/src/nile-context-submenu/nile-context-submenu.js.map +1 -0
  83. package/dist/src/nile-status-light/index.d.ts +1 -0
  84. package/dist/src/nile-status-light/index.js +2 -0
  85. package/dist/src/nile-status-light/index.js.map +1 -0
  86. package/dist/src/nile-status-light/nile-status-light.css.d.ts +9 -0
  87. package/dist/src/nile-status-light/nile-status-light.css.js +145 -0
  88. package/dist/src/nile-status-light/nile-status-light.css.js.map +1 -0
  89. package/dist/src/nile-status-light/nile-status-light.d.ts +53 -0
  90. package/dist/src/nile-status-light/nile-status-light.js +108 -0
  91. package/dist/src/nile-status-light/nile-status-light.js.map +1 -0
  92. package/dist/src/version.js +1 -1
  93. package/dist/src/version.js.map +1 -1
  94. package/dist/tsconfig.tsbuildinfo +1 -1
  95. package/package.json +2 -1
  96. package/src/index.ts +4 -0
  97. package/src/nile-context-menu/index.ts +12 -0
  98. package/src/nile-context-menu/nile-context-menu.css.ts +130 -0
  99. package/src/nile-context-menu/nile-context-menu.ts +698 -0
  100. package/src/nile-context-menu-group/index.ts +1 -0
  101. package/src/nile-context-menu-group/nile-context-menu-group.css.ts +31 -0
  102. package/src/nile-context-menu-group/nile-context-menu-group.ts +55 -0
  103. package/src/nile-context-menu-item/index.ts +5 -0
  104. package/src/nile-context-menu-item/nile-context-menu-item.css.ts +83 -0
  105. package/src/nile-context-menu-item/nile-context-menu-item.ts +112 -0
  106. package/src/nile-context-submenu/index.ts +1 -0
  107. package/src/nile-context-submenu/nile-context-submenu.ts +322 -0
  108. package/src/nile-status-light/index.ts +1 -0
  109. package/src/nile-status-light/nile-status-light.css.ts +146 -0
  110. package/src/nile-status-light/nile-status-light.ts +111 -0
  111. package/vscode-html-custom-data.json +140 -4
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Copyright Aquera Inc 2026
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import { css } from 'lit';
9
+
10
+ export const styles = css`
11
+ :host {
12
+ box-sizing: border-box;
13
+ display: block;
14
+ }
15
+
16
+ .group {
17
+ display: flex;
18
+ flex-direction: column;
19
+ }
20
+
21
+ .label {
22
+ padding: var(--nile-spacing-xs, var(--ng-spacing-xs)) var(--nile-spacing-xl, var(--ng-spacing-xl));
23
+ font-family: inherit;
24
+ font-size: var(--nile-type-scale-3, var(--ng-font-size-text-xs));
25
+ font-weight: var(--nile-font-weight-medium, var(--ng-font-weight-semibold));
26
+ line-height: var(--nile-line-height-1-8, var(--ng-line-height-text-md));
27
+ color: var(--nile-colors-neutral-500, var(--ng-colors-text-placeholder-subtle));
28
+ }
29
+ `;
30
+
31
+ export default [styles];
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Copyright Aquera Inc 2026
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import { html, CSSResultArray, TemplateResult } from 'lit';
9
+ import { customElement, property } from 'lit/decorators.js';
10
+ import { styles } from './nile-context-menu-group.css';
11
+ import NileElement from '../internal/nile-element';
12
+
13
+ /**
14
+ * Nile context-menu group. Groups related items inside a `nile-context-menu`.
15
+ *
16
+ * @tag nile-context-menu-group
17
+ *
18
+ * @slot - Default slot for `nile-context-menu-item` children.
19
+ */
20
+ @customElement('nile-context-menu-group')
21
+ export class NileContextMenuGroup extends NileElement {
22
+ public static get styles(): CSSResultArray {
23
+ return [styles];
24
+ }
25
+
26
+ @property({ attribute: true, type: String, reflect: true }) label = '';
27
+
28
+ private static _idSeq = 0;
29
+ private readonly _labelId = `nile-context-menu-group-label-${++NileContextMenuGroup._idSeq}`;
30
+
31
+ public render(): TemplateResult {
32
+ const heading = this.label || this.id;
33
+ return html`
34
+ <div
35
+ part="group"
36
+ class="group"
37
+ role="group"
38
+ aria-labelledby=${heading ? this._labelId : undefined}
39
+ >
40
+ ${heading
41
+ ? html`<div part="label" class="label" id=${this._labelId}>${heading}</div>`
42
+ : null}
43
+ <slot></slot>
44
+ </div>
45
+ `;
46
+ }
47
+ }
48
+
49
+ export default NileContextMenuGroup;
50
+
51
+ declare global {
52
+ interface HTMLElementTagNameMap {
53
+ 'nile-context-menu-group': NileContextMenuGroup;
54
+ }
55
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ NileContextMenuItem,
3
+ type NileContextMenuItemSelectDetail,
4
+ type NileContextMenuItemSelectHandler,
5
+ } from './nile-context-menu-item';
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Copyright Aquera Inc 2026
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import { css } from 'lit';
9
+
10
+ export const styles = css`
11
+ :host {
12
+ box-sizing: border-box;
13
+ display: block;
14
+ -webkit-font-smoothing: var(--nile-webkit-font-smoothing, var(--ng-webkit-font-smoothing));
15
+ -moz-osx-font-smoothing: var(--nile-moz-osx-font-smoothing, var(--ng-moz-osx-font-smoothing));
16
+ text-rendering: var(--nile-text-rendering, var(--ng-text-rendering));
17
+ }
18
+
19
+ :host *,
20
+ :host *::before,
21
+ :host *::after {
22
+ box-sizing: inherit;
23
+ }
24
+
25
+ .item {
26
+ position: relative;
27
+ display: flex;
28
+ align-items: center;
29
+ min-height: 40px;
30
+ padding: var(--nile-spacing-none, var(--ng-spacing-none)) var(--nile-spacing-xl, var(--ng-spacing-xl));
31
+ font-family: inherit;
32
+ font-size: var(--nile-type-scale-4, var(--ng-font-size-text-sm));
33
+ font-weight: var(--nile-font-weight-regular, var(--ng-font-weight-semibold));
34
+ line-height: var(--nile-line-height-1-8, var(--ng-line-height-text-md));
35
+ letter-spacing: normal;
36
+ color: var(--nile-colors-text-default, var(--ng-colors-text-secondary-700));
37
+ cursor: pointer;
38
+ white-space: nowrap;
39
+ user-select: none;
40
+ transition: 150ms fill;
41
+ }
42
+
43
+ .item:hover,
44
+ .item:focus,
45
+ .item:focus-visible {
46
+ outline: none;
47
+ background-color: var(--nile-colors-neutral-100, var(--ng-colors-bg-primary-hover));
48
+ color: var(--nile-colors-dark-900, var(--ng-colors-text-secondary-hover));
49
+ }
50
+
51
+ .item[aria-disabled='true'] {
52
+ opacity: 0.5;
53
+ cursor: not-allowed;
54
+ pointer-events: none;
55
+ }
56
+
57
+ .icon {
58
+ flex: 0 0 auto;
59
+ display: inline-flex;
60
+ align-items: center;
61
+ margin-right: var(--nile-spacing-md, var(--ng-spacing-md));
62
+ }
63
+
64
+ .icon[data-empty='true'] {
65
+ display: none;
66
+ }
67
+
68
+ .label {
69
+ flex: 1 1 auto;
70
+ min-width: 0;
71
+ }
72
+
73
+ .chevron {
74
+ flex: 0 0 auto;
75
+ display: inline-flex;
76
+ align-items: center;
77
+ margin-left: var(--nile-spacing-md, var(--ng-spacing-md));
78
+ line-height: 1;
79
+ opacity: 0.6;
80
+ }
81
+ `;
82
+
83
+ export default [styles];
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Copyright Aquera Inc 2026
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import { html, CSSResultArray, TemplateResult, nothing } from 'lit';
9
+ import { customElement, property, state } from 'lit/decorators.js';
10
+ import { styles } from './nile-context-menu-item.css';
11
+ import NileElement from '../internal/nile-element';
12
+
13
+ //Data passed to an item's `onSelect` callback. Matches the `nile-context-menu:select` event detail.
14
+ export interface NileContextMenuItemSelectDetail {
15
+ id: string;
16
+ value: string;
17
+ name: string;
18
+ target: Element | null;
19
+ originalEvent: Event | null;
20
+ }
21
+
22
+ export type NileContextMenuItemSelectHandler = (
23
+ detail: NileContextMenuItemSelectDetail
24
+ ) => void;
25
+
26
+ /**
27
+ * Nile context-menu item. A clickable entry inside `nile-context-menu`.
28
+ *
29
+ * @tag nile-context-menu-item
30
+ *
31
+ * @slot - Default slot for the visible label.
32
+ * @slot icon - Optional leading icon (e.g. a `<nile-glyph slot="icon">`).
33
+ */
34
+ @customElement('nile-context-menu-item')
35
+ export class NileContextMenuItem extends NileElement {
36
+ public static get styles(): CSSResultArray {
37
+ return [styles];
38
+ }
39
+
40
+ @property({ attribute: true, type: String, reflect: true }) value = '';
41
+
42
+ @property({ attribute: true, type: Boolean, reflect: true }) disabled = false;
43
+
44
+ public onSelect?: NileContextMenuItemSelectHandler;
45
+
46
+ @state() private _hasSubmenu = false;
47
+
48
+ @state() private _hasIcon = false;
49
+
50
+ @state() private _submenuExpanded = false;
51
+
52
+ public setSubmenuExpanded(expanded: boolean): void {
53
+ this._submenuExpanded = expanded;
54
+ }
55
+
56
+ public override focus(options?: FocusOptions): void {
57
+ const inner = this.shadowRoot?.querySelector('.item') as HTMLElement | null;
58
+ inner?.focus(options);
59
+ }
60
+
61
+ public get hasSubmenu(): boolean {
62
+ return this._hasSubmenu;
63
+ }
64
+
65
+ private _onSlotChange = (e: Event): void => {
66
+ const slot = e.target as HTMLSlotElement;
67
+ const nodes = slot.assignedElements({ flatten: true });
68
+ this._hasSubmenu = nodes.some(
69
+ n => n.tagName.toLowerCase() === 'nile-context-submenu'
70
+ );
71
+ };
72
+
73
+ private _onIconSlotChange = (e: Event): void => {
74
+ const slot = e.target as HTMLSlotElement;
75
+ this._hasIcon = slot.assignedNodes({ flatten: true }).some(
76
+ n => n.nodeType === Node.ELEMENT_NODE || (n.textContent ?? '').trim() !== ''
77
+ );
78
+ };
79
+
80
+ public render(): TemplateResult {
81
+ return html`
82
+ <div
83
+ part="item"
84
+ class="item"
85
+ role="menuitem"
86
+ tabindex="-1"
87
+ aria-disabled=${this.disabled ? 'true' : 'false'}
88
+ aria-haspopup=${this._hasSubmenu ? 'menu' : nothing}
89
+ aria-expanded=${this._hasSubmenu ? String(this._submenuExpanded) : nothing}
90
+ data-has-submenu=${this._hasSubmenu ? 'true' : 'false'}
91
+ >
92
+ <span part="icon" class="icon" data-empty=${this._hasIcon ? 'false' : 'true'}>
93
+ <slot name="icon" @slotchange=${this._onIconSlotChange}></slot>
94
+ </span>
95
+ <span part="label" class="label"><slot @slotchange=${this._onSlotChange}></slot></span>
96
+ ${this._hasSubmenu
97
+ ? html`<span part="chevron" class="chevron" aria-hidden="true">
98
+ <nile-glyph name="ng-chevron-right" size="16"></nile-glyph>
99
+ </span>`
100
+ : ''}
101
+ </div>
102
+ `;
103
+ }
104
+ }
105
+
106
+ export default NileContextMenuItem;
107
+
108
+ declare global {
109
+ interface HTMLElementTagNameMap {
110
+ 'nile-context-menu-item': NileContextMenuItem;
111
+ }
112
+ }
@@ -0,0 +1 @@
1
+ export { NileContextSubmenu } from './nile-context-submenu';
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Copyright Aquera Inc 2026
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import { html, CSSResultArray, TemplateResult, css } from 'lit';
9
+ import { customElement, property, state } from 'lit/decorators.js';
10
+ import NileElement from '../internal/nile-element';
11
+ import type { NileContextMenuItem } from '../nile-context-menu-item';
12
+ import '../nile-floating-panel';
13
+ import type { NileFloatingPanel } from '../nile-floating-panel';
14
+
15
+ const ITEM_TAG = 'nile-context-menu-item';
16
+ const SUBMENU_TAG = 'nile-context-submenu';
17
+ const HOVER_OPEN_DELAY = 100;
18
+ const HOVER_CLOSE_DELAY = 300;
19
+
20
+ let submenuIdSeq = 0;
21
+
22
+ /**
23
+ * Nested submenu inside a `nile-context-menu-item`.
24
+ * @tag nile-context-submenu
25
+ */
26
+ @customElement('nile-context-submenu')
27
+ export class NileContextSubmenu extends NileElement {
28
+ public static get styles(): CSSResultArray {
29
+ return [
30
+ css`
31
+ :host { display: contents; }
32
+ `,
33
+ ];
34
+ }
35
+
36
+ @property({ attribute: true, type: Number, reflect: true }) zIndex = 9999;
37
+
38
+ @state() private _open = false;
39
+
40
+ public static openStack: NileContextSubmenu[] = [];
41
+
42
+ private _parentItem: NileContextMenuItem | null = null;
43
+ private _proxyId = `nile-context-submenu-anchor-${++submenuIdSeq}`;
44
+ private _proxyEl?: HTMLDivElement;
45
+ private _floatingPanelEl?: NileFloatingPanel;
46
+ private _menuContainerRef: HTMLDivElement | null = null;
47
+ private _openTimer?: number;
48
+ private _closeTimer?: number;
49
+ private _setupDone = false;
50
+
51
+ public override connectedCallback(): void {
52
+ super.connectedCallback();
53
+ let p: Element | null = this.parentElement;
54
+ while (p && p.tagName.toLowerCase() !== ITEM_TAG) p = p.parentElement;
55
+ this._parentItem = p as NileContextMenuItem | null;
56
+ this._ensureProxy();
57
+ if (this._parentItem) {
58
+ this._parentItem.setSubmenuExpanded?.(false);
59
+ this._parentItem.addEventListener('mouseenter', this._onParentEnter);
60
+ this._parentItem.addEventListener('mouseleave', this._onParentLeave);
61
+ this._parentItem.addEventListener('click', this._onParentClick, true);
62
+ }
63
+ this._ensureBodyPanel();
64
+ }
65
+
66
+ private _ensureProxy(): void {
67
+ if (this._proxyEl) return;
68
+ const el = document.createElement('div');
69
+ el.id = this._proxyId;
70
+ el.setAttribute('aria-hidden', 'true');
71
+ el.style.cssText =
72
+ 'position: fixed; top: 0; left: 0; width: 0; height: 0; pointer-events: none;';
73
+ document.body.appendChild(el);
74
+ this._proxyEl = el;
75
+ }
76
+
77
+ private _syncProxyToParent(): void {
78
+ if (!this._proxyEl || !this._parentItem) return;
79
+ const rect = this._parentItem.getBoundingClientRect();
80
+ this._proxyEl.style.left = `${rect.left}px`;
81
+ this._proxyEl.style.top = `${rect.top}px`;
82
+ this._proxyEl.style.width = `${rect.width}px`;
83
+ this._proxyEl.style.height = `${rect.height}px`;
84
+ }
85
+
86
+ public override disconnectedCallback(): void {
87
+ super.disconnectedCallback();
88
+ if (this._parentItem) {
89
+ this._parentItem.removeEventListener('mouseenter', this._onParentEnter);
90
+ this._parentItem.removeEventListener('mouseleave', this._onParentLeave);
91
+ this._parentItem.removeEventListener('click', this._onParentClick, true);
92
+ }
93
+ this._clearTimers();
94
+ if (this._open) this.closeSubmenu();
95
+ this._teardownBodyArtifacts();
96
+ NileContextSubmenu.openStack = NileContextSubmenu.openStack.filter(s => s !== this);
97
+ }
98
+
99
+ private _teardownBodyArtifacts(): void {
100
+ const menu = this._menuContainerRef;
101
+ if (menu) {
102
+ menu.removeEventListener('click', this._onMenuClick);
103
+ menu.removeEventListener('mouseover', this._onMenuMouseOver);
104
+ menu.removeEventListener('mouseenter', this._onPanelEnter);
105
+ menu.removeEventListener('mouseleave', this._onPanelLeave);
106
+ menu.removeEventListener('nile-change', this._onNestedSelect as EventListener);
107
+ while (menu.firstChild) this.appendChild(menu.firstChild);
108
+ }
109
+ this._floatingPanelEl?.remove();
110
+ this._floatingPanelEl = undefined;
111
+ this._menuContainerRef = null;
112
+ this._setupDone = false;
113
+ this._proxyEl?.remove();
114
+ this._proxyEl = undefined;
115
+ }
116
+
117
+ private _ensureBodyPanel(): void {
118
+ if (this._setupDone) return;
119
+ const fp = document.createElement('nile-floating-panel') as NileFloatingPanel;
120
+ fp.setAttribute('for', this._proxyId);
121
+ fp.setAttribute('trigger', 'manual');
122
+ fp.setAttribute('placement', 'right-start');
123
+ fp.panelClass = 'nile-context-menu-panel';
124
+ fp.zIndex = this.zIndex;
125
+ fp.interactive = true;
126
+ fp.hideOnClick = false;
127
+ fp.closeOnEscape = false;
128
+ (fp as unknown as { arrow: 'none' }).arrow = 'none';
129
+ fp.distance = 0;
130
+
131
+ const menu = document.createElement('div');
132
+ menu.className = 'nile-context-menu__menu';
133
+ menu.setAttribute('role', 'menu');
134
+ fp.appendChild(menu);
135
+
136
+ document.body.appendChild(fp);
137
+ this._floatingPanelEl = fp;
138
+ this._menuContainerRef = menu;
139
+
140
+ menu.addEventListener('click', this._onMenuClick);
141
+ menu.addEventListener('mouseover', this._onMenuMouseOver);
142
+ menu.addEventListener('mouseenter', this._onPanelEnter);
143
+ menu.addEventListener('mouseleave', this._onPanelLeave);
144
+ menu.addEventListener('nile-change', this._onNestedSelect as EventListener);
145
+
146
+ this._relocateLightChildren();
147
+ this._setupDone = true;
148
+ }
149
+
150
+ protected override firstUpdated(): void {
151
+ this._relocateLightChildren();
152
+ }
153
+
154
+ private _relocateLightChildren(): void {
155
+ if (!this._menuContainerRef) return;
156
+ for (const kid of Array.from(this.children)) {
157
+ this._menuContainerRef.appendChild(kid);
158
+ }
159
+ }
160
+
161
+ private _clearTimers(): void {
162
+ if (this._openTimer != null) { clearTimeout(this._openTimer); this._openTimer = undefined; }
163
+ if (this._closeTimer != null) { clearTimeout(this._closeTimer); this._closeTimer = undefined; }
164
+ }
165
+
166
+ private _onParentEnter = (): void => {
167
+ if (this._parentItem?.disabled) return;
168
+ this._clearTimers();
169
+ if (this._open) return;
170
+ this._openTimer = window.setTimeout(() => this.openSubmenu(), HOVER_OPEN_DELAY);
171
+ };
172
+
173
+ private _onParentLeave = (): void => {
174
+ if (this._openTimer != null) { clearTimeout(this._openTimer); this._openTimer = undefined; }
175
+ if (this._open) {
176
+ this._closeTimer = window.setTimeout(() => this.closeSubmenu(), HOVER_CLOSE_DELAY);
177
+ }
178
+ };
179
+
180
+ private _onParentClick = (e: MouseEvent): void => {
181
+ e.stopPropagation();
182
+ if (this._parentItem?.disabled) return;
183
+ this._clearTimers();
184
+ if (this._open) this.closeSubmenu();
185
+ else this.openSubmenu();
186
+ };
187
+
188
+ private _onPanelEnter = (): void => {
189
+ this._clearTimers();
190
+ };
191
+
192
+ private _onPanelLeave = (): void => {
193
+ if (this._open) {
194
+ this._closeTimer = window.setTimeout(() => {
195
+ const hasOpenDescendant = NileContextSubmenu.openStack.some(
196
+ sub => sub !== this && this._menuContainerRef?.contains(sub),
197
+ );
198
+ if (hasOpenDescendant) return;
199
+ this.closeSubmenu();
200
+ }, HOVER_CLOSE_DELAY);
201
+ }
202
+ };
203
+
204
+ public openSubmenu(): void {
205
+ if (this._open) return;
206
+ if (this._parentItem?.disabled) return;
207
+ this._closeSiblingSubmenus();
208
+ this._ensureProxy();
209
+ this._ensureBodyPanel();
210
+ this._syncProxyToParent();
211
+ this._open = true;
212
+ if (this._floatingPanelEl) this._floatingPanelEl.open = true;
213
+ NileContextSubmenu.openStack.push(this);
214
+ this._parentItem?.setSubmenuExpanded?.(true);
215
+ }
216
+
217
+ public closeSubmenu(): void {
218
+ if (!this._open) return;
219
+ for (const sub of [...NileContextSubmenu.openStack]) {
220
+ if (sub !== this && this._menuContainerRef?.contains(sub)) {
221
+ sub.closeSubmenu();
222
+ }
223
+ }
224
+ this._open = false;
225
+ if (this._floatingPanelEl) this._floatingPanelEl.open = false;
226
+ NileContextSubmenu.openStack = NileContextSubmenu.openStack.filter(s => s !== this);
227
+ this._clearTimers();
228
+ this._parentItem?.setSubmenuExpanded?.(false);
229
+ }
230
+
231
+ public get isOpen(): boolean {
232
+ return this._open;
233
+ }
234
+
235
+ public static findByContainer(container: Element): NileContextSubmenu | null {
236
+ for (const sub of NileContextSubmenu.openStack) {
237
+ if (sub._menuContainerRef === container) return sub;
238
+ }
239
+ return null;
240
+ }
241
+
242
+ public get parentItem(): NileContextMenuItem | null {
243
+ return this._parentItem;
244
+ }
245
+ public focusFirstItem(): void {
246
+ if (!this._menuContainerRef) return;
247
+ const items = Array.from(
248
+ this._menuContainerRef.querySelectorAll(ITEM_TAG)
249
+ ) as NileContextMenuItem[];
250
+ for (const item of items) {
251
+ let cur: Element | null = item.parentElement;
252
+ let inDeeperSub = false;
253
+ while (cur && cur !== this._menuContainerRef) {
254
+ if (cur.tagName.toLowerCase() === SUBMENU_TAG) { inDeeperSub = true; break; }
255
+ cur = cur.parentElement;
256
+ }
257
+ if (inDeeperSub) continue;
258
+ if (!item.disabled) { item.focus(); return; }
259
+ }
260
+ }
261
+
262
+ private _closeSiblingSubmenus(): void {
263
+ const myLevel = this._parentLevel();
264
+ for (const sub of [...NileContextSubmenu.openStack]) {
265
+ if (sub === this) continue;
266
+ if (sub._parentLevel() === myLevel) sub.closeSubmenu();
267
+ }
268
+ }
269
+
270
+ private _parentLevel(): Element | null {
271
+ const item = this._parentItem;
272
+ if (!item) return null;
273
+ return item.parentElement?.closest('.nile-context-menu__menu') ?? null;
274
+ }
275
+
276
+ private _onMenuMouseOver = (e: MouseEvent): void => {
277
+ const item = e.composedPath().find(
278
+ n => n instanceof HTMLElement && n.tagName.toLowerCase() === ITEM_TAG
279
+ ) as NileContextMenuItem | undefined;
280
+ if (!item || item.disabled) return;
281
+ item.focus();
282
+ };
283
+
284
+ private _onNestedSelect = (e: CustomEvent): void => {
285
+ if (e.target === this) return;
286
+ if (e.detail?.type !== 'click') return;
287
+ this.emit('nile-change', e.detail);
288
+ };
289
+
290
+ private _onMenuClick = (e: MouseEvent): void => {
291
+ const item = e.composedPath().find(
292
+ n => n instanceof HTMLElement && n.tagName.toLowerCase() === ITEM_TAG
293
+ ) as NileContextMenuItem | undefined;
294
+ if (!item || item.disabled) return;
295
+ if (item.querySelector(`:scope > ${SUBMENU_TAG}`)) return;
296
+ const detail = {
297
+ id: item.id,
298
+ value: item.value,
299
+ name: (item.textContent ?? '').trim(),
300
+ target: null,
301
+ originalEvent: e,
302
+ };
303
+ try {
304
+ (item as unknown as { onSelect?: (d: typeof detail) => void }).onSelect?.(detail);
305
+ } catch (err) {
306
+ console.error('[nile-context-submenu] onSelect callback threw:', err);
307
+ }
308
+ this.emit('nile-change', { type: 'click', ...detail });
309
+ };
310
+
311
+ public render(): TemplateResult {
312
+ return html``;
313
+ }
314
+ }
315
+
316
+ export default NileContextSubmenu;
317
+
318
+ declare global {
319
+ interface HTMLElementTagNameMap {
320
+ 'nile-context-submenu': NileContextSubmenu;
321
+ }
322
+ }
@@ -0,0 +1 @@
1
+ export { NileStatusLight } from './nile-status-light';