+ Navigation & Overlays
+
+ Tabs переключают близкие представления, dropdown подходит для коротких action-menu,
+ tooltip дает компактную подсказку, popover раскрывает небольшой контекстный блок.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Short contextual hint for compact controls.
+
+
+
+
+
+
+
Context
+
+ Use popover for short forms, metadata, previews, and low-risk contextual actions.
+
+
+
+
+
+
+
+
+
<div class="tabs" role="tablist" aria-label="View mode">
+ <button class="tab tab-active" type="button" role="tab" aria-selected="true">Overview</button>
+ <button class="tab" type="button" role="tab" aria-selected="false">Activity</button>
+ <button class="tab" type="button" role="tab" aria-selected="false">Settings</button>
+</div>
+
+
+
+
+
<div class="dropdown">
+ <button class="btn btn-secondary with-icon" type="button" data-dropdown-toggle aria-expanded="false">
+ <i class="ph ph-dots-three-outline"></i>
+ Actions
+ </button>
+ <div class="dropdown-menu" role="menu">
+ <button class="dropdown-item" type="button" role="menuitem">Edit</button>
+ <button class="dropdown-item" type="button" role="menuitem">Duplicate</button>
+ <button class="dropdown-item dropdown-item-danger" type="button" role="menuitem">Delete</button>
+ </div>
+</div>
+
+
+
+
+
<span class="tooltip">
+ <button class="btn btn-primary with-icon" type="button" data-tooltip-toggle>
+ <i class="ph ph-question"></i>
+ Help
+ </button>
+ <span class="tooltip-panel" role="tooltip">
+ Short contextual hint for compact controls.
+ </span>
+</span>
+
+
+
+
+
<div class="popover">
+ <button class="btn btn-accent with-icon" type="button" data-popover-toggle aria-expanded="false">
+ <i class="ph ph-info"></i>
+ Details
+ </button>
+ <div class="popover-panel">
+ <h3 class="popover-title">Context</h3>
+ <p class="popover-text">Use popover for short forms, metadata, previews, and low-risk contextual actions.</p>
+ </div>
+</div>
+
+
+
+
+
Overlays.init();
+
+// Available on the global namespace too:
+GNexusUIKit.Overlays.closeAll();
+
+
diff --git a/src/js/components/overlays.js b/src/js/components/overlays.js
new file mode 100644
index 0000000..9f3db8d
--- /dev/null
+++ b/src/js/components/overlays.js
@@ -0,0 +1,140 @@
+let initialized = false;
+
+function closeNode(node) {
+ node.classList.remove("is-open");
+ node.querySelectorAll("[aria-expanded='true']").forEach(trigger => {
+ trigger.setAttribute("aria-expanded", "false");
+ });
+}
+
+function closeAll(except = null) {
+ document.querySelectorAll(".dropdown.is-open, .popover.is-open, .tooltip.is-open").forEach(node => {
+ if(node !== except) {
+ closeNode(node);
+ }
+ });
+}
+
+function initTabs(root = document) {
+ root.addEventListener("click", event => {
+ const tab = event.target.closest(".tab");
+
+ if(!tab) {
+ return;
+ }
+
+ const tabs = tab.closest(".tabs");
+
+ if(!tabs) {
+ return;
+ }
+
+ tabs.querySelectorAll(".tab").forEach(item => {
+ const isActive = item === tab;
+ item.classList.toggle("tab-active", isActive);
+ item.setAttribute("aria-selected", String(isActive));
+ });
+ });
+}
+
+function initDropdowns(root = document) {
+ root.addEventListener("click", event => {
+ const trigger = event.target.closest("[data-dropdown-toggle]");
+
+ if(!trigger) {
+ return;
+ }
+
+ const dropdown = trigger.closest(".dropdown");
+
+ if(!dropdown) {
+ return;
+ }
+
+ event.preventDefault();
+ const isOpen = dropdown.classList.contains("is-open");
+ closeAll(dropdown);
+ dropdown.classList.toggle("is-open", !isOpen);
+ trigger.setAttribute("aria-expanded", String(!isOpen));
+ });
+}
+
+function initPopovers(root = document) {
+ root.addEventListener("click", event => {
+ const trigger = event.target.closest("[data-popover-toggle]");
+
+ if(!trigger) {
+ return;
+ }
+
+ const popover = trigger.closest(".popover");
+
+ if(!popover) {
+ return;
+ }
+
+ event.preventDefault();
+ const isOpen = popover.classList.contains("is-open");
+ closeAll(popover);
+ popover.classList.toggle("is-open", !isOpen);
+ trigger.setAttribute("aria-expanded", String(!isOpen));
+ });
+}
+
+function initTooltips(root = document) {
+ root.addEventListener("click", event => {
+ const trigger = event.target.closest("[data-tooltip-toggle]");
+
+ if(!trigger) {
+ return;
+ }
+
+ const tooltip = trigger.closest(".tooltip");
+
+ if(!tooltip) {
+ return;
+ }
+
+ event.preventDefault();
+ const isOpen = tooltip.classList.contains("is-open");
+ closeAll(tooltip);
+ tooltip.classList.toggle("is-open", !isOpen);
+ });
+}
+
+function initDismiss(root = document) {
+ root.addEventListener("click", event => {
+ if(event.target.closest(".dropdown, .popover, .tooltip")) {
+ return;
+ }
+
+ closeAll();
+ });
+
+ document.addEventListener("keydown", event => {
+ if(event.key === "Escape") {
+ closeAll();
+ }
+ });
+}
+
+function init(root = document) {
+ if(root === document && initialized) {
+ return;
+ }
+
+ initTabs(root);
+ initDropdowns(root);
+ initPopovers(root);
+ initTooltips(root);
+ initDismiss(root);
+
+ if(root === document) {
+ initialized = true;
+ }
+}
+
+export default {
+ init,
+ closeAll
+};
diff --git a/src/js/index.js b/src/js/index.js
index 7ab1903..b4ac6a2 100644
--- a/src/js/index.js
+++ b/src/js/index.js
@@ -4,6 +4,7 @@
import advancedSelect from "./components/advanced-select.js";
import editableString from "./components/editable-string.js";
import confirmPopup from "./components/confirm-popup.js";
+import Overlays from "./components/overlays.js";
import demoNavigation from "./demo-navigation.js";
import codeExamples from "./code-examples.js";
@@ -13,12 +14,14 @@
Modals,
advancedSelect,
editableString,
- confirmPopup
+ confirmPopup,
+ Overlays
};
Object.assign(window, api);
document.addEventListener("DOMContentLoaded", () => {
+ Overlays.init();
demoNavigation();
codeExamples();
});
@@ -29,7 +32,8 @@
Modals,
advancedSelect,
editableString,
- confirmPopup
+ confirmPopup,
+ Overlays
};
export default api;
diff --git a/src/scss/components/_badges.scss b/src/scss/components/_badges.scss
index d886ffc..4eb598e 100644
--- a/src/scss/components/_badges.scss
+++ b/src/scss/components/_badges.scss
@@ -1,6 +1,6 @@
.badge {
background: $color-primary;
- color: $color-dark;
+ color: $color-black;
padding: $space-1 $space-2;
font-size: $font-size-sm;
font-weight: $font-weight-semibold;
@@ -26,12 +26,12 @@
&.badge-info {
background: $color-info;
- color: $color-text-light;
+ color: $color-black;
}
&.badge-secondary {
background: $color-secondary;
- color: $color-text-light;
+ color: $color-black;
}
&.badge-primary-outline {
diff --git a/src/scss/components/_buttons.scss b/src/scss/components/_buttons.scss
index dac7005..6219a82 100644
--- a/src/scss/components/_buttons.scss
+++ b/src/scss/components/_buttons.scss
@@ -37,7 +37,7 @@
.ph,
.ph-bold {
position: absolute;
- color: #0a0a0d;
+ color: $color-black;
left: -$control-icon-rail-md;
top: 1px;
font-size: $icon-size-lg;
@@ -76,12 +76,12 @@
@include hover_touch {
background-color: $color-secondary;
- color: $color-text-light;
+ color: $color-black;
&.with-icon {
.ph,
.ph-bold {
- color: $color-text-light;
+ color: $color-black;
}
}
}
@@ -93,12 +93,12 @@
@include hover_touch {
background-color: $color-accent;
- color: $color-text-light;
+ color: $color-black;
&.with-icon {
.ph,
.ph-bold {
- color: $color-text-light;
+ color: $color-black;
}
}
}
@@ -140,12 +140,12 @@
@include hover_touch {
background-color: $color-info;
- color: $color-text-light;
+ color: $color-black;
&.with-icon {
.ph,
.ph-bold {
- color: $color-text-light;
+ color: $color-black;
}
}
}
diff --git a/src/scss/components/_cards.scss b/src/scss/components/_cards.scss
index dcdaf5e..801f6de 100644
--- a/src/scss/components/_cards.scss
+++ b/src/scss/components/_cards.scss
@@ -105,7 +105,7 @@
}
&.card-info {
- @include state_panel($color-info, $color-text-light);
+ @include state_panel($color-info);
.status-icon {
color: $color-info;
@@ -113,7 +113,7 @@
}
&.card-secondary {
- @include state_panel($color-secondary, $color-text-light);
+ @include state_panel($color-secondary);
.status-icon {
color: $color-secondary;
diff --git a/src/scss/components/_navigation-overlays.scss b/src/scss/components/_navigation-overlays.scss
new file mode 100644
index 0000000..e6739a3
--- /dev/null
+++ b/src/scss/components/_navigation-overlays.scss
@@ -0,0 +1,170 @@
+.tabs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: $space-2;
+ align-items: center;
+ margin-bottom: $space-4;
+
+ .tab {
+ display: inline-flex;
+ align-items: center;
+ min-height: $control-height-sm;
+ padding: $space-2 $space-3;
+ border: $border-width-base solid $border-color-muted;
+ border-left-width: $border-width-accent;
+ color: $color-text-medium;
+ background: $surface-panel-muted;
+ font-family: $font-family-base;
+ font-size: $font-size-sm;
+ font-weight: $font-weight-semibold;
+ line-height: $line-height-base;
+ text-transform: uppercase;
+ cursor: pointer;
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: color, background, border-color;
+
+ &:focus-visible {
+ @include focus_ring;
+ }
+
+ @include hover_touch {
+ color: $color-black;
+ background: $color-secondary;
+ border-color: $color-secondary;
+ }
+
+ &.tab-active,
+ &[aria-selected="true"] {
+ color: $color-black;
+ background: $color-primary;
+ border-color: $color-primary;
+ }
+ }
+}
+
+.dropdown,
+.popover {
+ position: relative;
+ display: inline-flex;
+}
+
+.dropdown-menu,
+.popover-panel,
+.tooltip-panel {
+ z-index: 40;
+ background: $surface-panel;
+ border: $border-width-base solid $border-color-muted;
+ border-left-width: $border-width-accent;
+ box-shadow: 0 14px 36px rgba($color-black, 0.36);
+}
+
+.dropdown-menu,
+.popover-panel {
+ position: absolute;
+ top: calc(100% + #{$space-2});
+ left: 0;
+ min-width: 220px;
+ display: none;
+}
+
+.dropdown.is-open .dropdown-menu,
+.popover.is-open .popover-panel {
+ display: block;
+}
+
+.dropdown-menu {
+ padding: $space-1;
+
+ .dropdown-item {
+ display: flex;
+ align-items: center;
+ gap: $space-2;
+ width: 100%;
+ min-height: $control-height-sm;
+ padding: $space-2 $space-3;
+ border: 0;
+ color: $color-text-light;
+ background: transparent;
+ font-family: $font-family-base;
+ font-size: $font-size-sm;
+ text-align: left;
+ cursor: pointer;
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: color, background;
+
+ .ph,
+ .ph-bold {
+ font-size: $icon-size-sm;
+ }
+
+ &:focus-visible {
+ @include focus_ring;
+ }
+
+ @include hover_touch {
+ color: $color-black;
+ background: $color-secondary;
+ }
+
+ &.dropdown-item-danger {
+ color: $color-error;
+
+ @include hover_touch {
+ color: $color-black;
+ background: $color-error;
+ }
+ }
+ }
+}
+
+.popover-panel {
+ width: min(320px, calc(100vw - #{$space-6}));
+ padding: $space-4;
+
+ .popover-title {
+ margin: 0 0 $space-2;
+ font-size: $font-size-md;
+ font-weight: $font-weight-bold;
+ text-transform: uppercase;
+ }
+
+ .popover-text {
+ margin: 0;
+ color: $color-text-medium;
+ font-size: $font-size-sm;
+ line-height: $line-height-relaxed;
+ }
+}
+
+.tooltip {
+ position: relative;
+ display: inline-flex;
+}
+
+.tooltip-panel {
+ position: absolute;
+ left: 50%;
+ bottom: calc(100% + #{$space-2});
+ width: max-content;
+ max-width: 260px;
+ padding: $space-2 $space-3;
+ color: $color-text-light;
+ font-size: $font-size-xs;
+ line-height: $line-height-normal;
+ transform: translateX(-50%);
+ opacity: 0;
+ visibility: hidden;
+ pointer-events: none;
+ transition-duration: $motion-fast;
+ transition-timing-function: $motion-ease;
+ transition-property: opacity, visibility;
+}
+
+.tooltip:hover .tooltip-panel,
+.tooltip:focus-within .tooltip-panel,
+.tooltip.is-open .tooltip-panel {
+ opacity: 1;
+ visibility: visible;
+}
diff --git a/src/scss/components/_toasts.scss b/src/scss/components/_toasts.scss
index 7cc7642..82e9987 100644
--- a/src/scss/components/_toasts.scss
+++ b/src/scss/components/_toasts.scss
@@ -59,7 +59,7 @@
width: $control-height-md;
&:hover {
- color: $color-dark;
+ color: $color-black;
}
}
@@ -68,7 +68,6 @@
.toast-title {
background: $color-info;
- color: $color-text-light;
}
}
diff --git a/src/scss/kit.scss b/src/scss/kit.scss
index 5db9871..c0eef79 100644
--- a/src/scss/kit.scss
+++ b/src/scss/kit.scss
@@ -17,6 +17,7 @@
@import "components/alerts";
@import "components/advanced-select";
@import "components/editable-string";
+@import "components/navigation-overlays";
@import "utils";
* {