Accordion
- Accordion и Disclosure используют native <details> / <summary>, поэтому раскрытие работает без JS.
+ Accordion и Disclosure используют native <details> / <summary>, а раскрытие анимируется через Accordion.init().
diff --git a/src/js/components/accordion.js b/src/js/components/accordion.js
new file mode 100644
index 0000000..e2a24b7
--- /dev/null
+++ b/src/js/components/accordion.js
@@ -0,0 +1,119 @@
+const initializedRoots = new WeakSet();
+
+function getPanel(details) {
+ return details.querySelector(".accordion-panel");
+}
+
+function prepareOpenPanel(details) {
+ const panel = getPanel(details);
+
+ if(!panel) {
+ return;
+ }
+
+ panel.style.height = "auto";
+ panel.style.opacity = "1";
+ panel.style.transform = "translateY(0)";
+}
+
+function expand(details) {
+ const panel = getPanel(details);
+
+ if(!panel || details.dataset.animating === "true") {
+ return;
+ }
+
+ details.dataset.animating = "true";
+ details.open = true;
+ panel.style.height = "0px";
+ panel.style.opacity = "0";
+ panel.style.transform = "translateY(-8px)";
+
+ requestAnimationFrame(() => {
+ panel.style.height = `${panel.scrollHeight}px`;
+ panel.style.opacity = "1";
+ panel.style.transform = "translateY(0)";
+ });
+
+ panel.addEventListener("transitionend", event => {
+ if(event.propertyName !== "height") {
+ return;
+ }
+
+ panel.style.height = "auto";
+ delete details.dataset.animating;
+ }, { once: true });
+}
+
+function collapse(details) {
+ const panel = getPanel(details);
+
+ if(!panel || details.dataset.animating === "true") {
+ return;
+ }
+
+ details.dataset.animating = "true";
+ panel.style.height = `${panel.scrollHeight}px`;
+ panel.style.opacity = "1";
+ panel.style.transform = "translateY(0)";
+
+ requestAnimationFrame(() => {
+ panel.style.height = "0px";
+ panel.style.opacity = "0";
+ panel.style.transform = "translateY(-8px)";
+ });
+
+ panel.addEventListener("transitionend", event => {
+ if(event.propertyName !== "height") {
+ return;
+ }
+
+ details.open = false;
+ panel.style.height = "";
+ panel.style.opacity = "";
+ panel.style.transform = "";
+ delete details.dataset.animating;
+ }, { once: true });
+}
+
+function toggle(details) {
+ if(details.open) {
+ collapse(details);
+ } else {
+ expand(details);
+ }
+}
+
+function init(root = document) {
+ if(initializedRoots.has(root)) {
+ return;
+ }
+
+ root.querySelectorAll(".accordion-item[open], .disclosure[open]").forEach(prepareOpenPanel);
+
+ root.addEventListener("click", event => {
+ const summary = event.target.closest(".accordion-summary");
+
+ if(!summary) {
+ return;
+ }
+
+ const details = summary.closest(".accordion-item, .disclosure");
+
+ if(!details) {
+ return;
+ }
+
+ event.preventDefault();
+ toggle(details);
+ });
+
+ initializedRoots.add(root);
+}
+
+export default {
+ init,
+ expand,
+ collapse,
+ toggle
+};
diff --git a/src/js/index.js b/src/js/index.js
index 325c80d..cd39981 100644
--- a/src/js/index.js
+++ b/src/js/index.js
@@ -7,6 +7,7 @@
import Drawer from "./components/drawer.js";
import Overlays from "./components/overlays.js";
import InputPatterns from "./components/input-patterns.js";
+import Accordion from "./components/accordion.js";
import demoNavigation from "./demo-navigation.js";
import codeExamples from "./code-examples.js";
@@ -19,7 +20,8 @@
confirmPopup,
Drawer,
Overlays,
- InputPatterns
+ InputPatterns,
+ Accordion
};
window.GNexusUIKit = api;
@@ -28,6 +30,7 @@
document.addEventListener("DOMContentLoaded", () => {
Overlays.init();
InputPatterns.init();
+ Accordion.init();
demoNavigation();
codeExamples();
});
@@ -41,7 +44,8 @@
confirmPopup,
Drawer,
Overlays,
- InputPatterns
+ InputPatterns,
+ Accordion
};
export default api;
diff --git a/src/scss/_motion.scss b/src/scss/_motion.scss
new file mode 100644
index 0000000..9a32f77
--- /dev/null
+++ b/src/scss/_motion.scss
@@ -0,0 +1,79 @@
+@use "kit-deps" as *;
+
+@keyframes terminal_scan_x {
+ 0% {
+ transform: translateX(-120%);
+ }
+
+ 100% {
+ transform: translateX(220%);
+ }
+}
+
+@keyframes terminal_scan_y {
+ 0% {
+ transform: translateY(-120%);
+ }
+
+ 100% {
+ transform: translateY(220%);
+ }
+}
+
+@keyframes terminal_pulse {
+ 0%,
+ 100% {
+ box-shadow: 0 0 0 0 rgba($color-primary, 0);
+ }
+
+ 50% {
+ box-shadow: 0 0 0 4px rgba($color-primary, 0.18);
+ }
+}
+
+@keyframes panel_boot {
+ 0% {
+ opacity: 0;
+ transform: translateY($space-2);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes overlay_reveal {
+ 0% {
+ opacity: 0;
+ transform: translateY(-$space-2);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes tooltip_reveal {
+ 0% {
+ opacity: 0;
+ transform: translateX(-50%) translateY($space-1);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ scroll-behavior: auto !important;
+ transition-duration: 0.01ms !important;
+ }
+}
diff --git a/src/scss/components/_accordion.scss b/src/scss/components/_accordion.scss
index 7b70061..f094d0e 100644
--- a/src/scss/components/_accordion.scss
+++ b/src/scss/components/_accordion.scss
@@ -12,6 +12,7 @@
.accordion-item {
border-bottom: $border-width-base solid rgba($color-text-light, 0.08);
+ overflow: hidden;
&:last-child {
border-bottom: 0;
@@ -79,10 +80,14 @@
}
.accordion-panel {
+ overflow: hidden;
padding: $space-4;
color: $color-text-medium;
font-size: $font-size-sm;
line-height: $line-height-relaxed;
+ transition-duration: $motion-slow;
+ transition-timing-function: $motion-ease;
+ transition-property: height, opacity, transform;
p {
margin-top: 0;
diff --git a/src/scss/components/_alerts.scss b/src/scss/components/_alerts.scss
index 58c106b..e0b0579 100644
--- a/src/scss/components/_alerts.scss
+++ b/src/scss/components/_alerts.scss
@@ -4,6 +4,8 @@
$alert-bg-alpha: 0.1;
.alert {
+ position: relative;
+ overflow: hidden;
margin-bottom: $space-3;
padding: $space-3 $space-4;
border: $border-width-base solid transparent;
@@ -13,6 +15,27 @@
color: $color-text-light;
font-weight: $font-weight-medium;
line-height: $line-height-normal;
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: background, color, border-color;
+
+ &::after {
+ content: "";
+ position: absolute;
+ inset: 0 auto 0 0;
+ width: 36%;
+ background: linear-gradient(90deg, transparent, rgba($color-text-light, 0.12), transparent);
+ opacity: 0;
+ pointer-events: none;
+ transform: translateX(-120%);
+ }
+
+ @include hover_touch {
+ &::after {
+ opacity: 1;
+ animation: terminal_scan_x 0.8s $motion-ease;
+ }
+ }
&.alert-primary {
border-color: $color-primary;
diff --git a/src/scss/components/_avatar.scss b/src/scss/components/_avatar.scss
index e3c7d98..6eca473 100644
--- a/src/scss/components/_avatar.scss
+++ b/src/scss/components/_avatar.scss
@@ -38,6 +38,9 @@
height: 13px;
border: $border-width-base solid $surface-page;
background: $color-text-dark;
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: background, box-shadow;
}
&.avatar-sm {
@@ -87,6 +90,7 @@
&.is-online .avatar-status {
background: $color-success;
+ animation: terminal_pulse 1.8s $motion-ease infinite;
}
&.is-busy .avatar-status {
diff --git a/src/scss/components/_badges.scss b/src/scss/components/_badges.scss
index 2ecd0b2..b1869a4 100644
--- a/src/scss/components/_badges.scss
+++ b/src/scss/components/_badges.scss
@@ -2,6 +2,8 @@
@use "typography" as *;
.badge {
+ position: relative;
+ overflow: hidden;
background: $color-primary;
color: $color-black;
padding: $space-1 $space-2;
@@ -13,6 +15,30 @@
display: inline-flex;
align-items: center;
min-height: 24px;
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: filter, transform, border-color, color, background;
+
+ &::after {
+ content: "";
+ position: absolute;
+ inset: 0 auto 0 0;
+ width: 40%;
+ background: linear-gradient(90deg, transparent, rgba($color-black, 0.16), transparent);
+ opacity: 0;
+ pointer-events: none;
+ transform: translateX(-120%);
+ }
+
+ @include hover_touch {
+ filter: saturate(1.12);
+ transform: translateY(-1px);
+
+ &::after {
+ opacity: 1;
+ animation: terminal_scan_x 0.7s $motion-ease;
+ }
+ }
&.badge-success {
background: $color-success;
diff --git a/src/scss/components/_cards.scss b/src/scss/components/_cards.scss
index c1fa0f4..bc330ab 100644
--- a/src/scss/components/_cards.scss
+++ b/src/scss/components/_cards.scss
@@ -2,9 +2,25 @@
@use "typography" as *;
.card {
+ position: relative;
max-width: 340px;
width: 100%;
+ overflow: hidden;
@include hard_panel($color-text-light);
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: border-color, background, box-shadow, transform;
+
+ &::after {
+ content: "";
+ position: absolute;
+ inset: 0 auto 0 0;
+ width: 34%;
+ background: linear-gradient(90deg, transparent, rgba($color-text-light, 0.08), transparent);
+ opacity: 0;
+ pointer-events: none;
+ transform: translateX(-120%);
+ }
.card-title {
color: $color-black;
@@ -22,6 +38,9 @@
display: block;
width: min(68%, 190px);
margin: $space-5 auto $space-6;
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: transform, filter;
}
p {
@@ -35,6 +54,23 @@
padding-bottom: $space-4;
}
+ @include hover_touch {
+ border-color: $color-secondary;
+ background: $surface-panel-strong;
+ box-shadow: 0 14px 32px rgba($color-black, 0.34);
+ transform: translateY(-2px);
+
+ &::after {
+ opacity: 1;
+ animation: terminal_scan_x 0.85s $motion-ease;
+ }
+
+ .card-thumb {
+ filter: saturate(1.12);
+ transform: translateY(-2px);
+ }
+ }
+
&.status-card {
max-width: 220px;
overflow: hidden;
@@ -69,7 +105,7 @@
width: 100%;
transition-duration: .2s;
- transition-property: color;
+ transition-property: color, transform;
}
}
@@ -93,6 +129,12 @@
line-height: $line-height-normal;
}
+ @include hover_touch {
+ .status-icon {
+ transform: translateY(-2px) scale(1.03);
+ }
+ }
+
&.card-success {
@include state_panel($color-success);
@@ -168,6 +210,9 @@
color: $color-black;
background: $color-secondary;
font-size: $icon-size-md;
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: background, transform;
}
.metric-card-value {
@@ -195,6 +240,13 @@
color: $color-error;
}
}
+
+ @include hover_touch {
+ .metric-card-icon {
+ background: $color-primary;
+ transform: translateY(-2px);
+ }
+ }
}
&.action-card {
@@ -217,6 +269,9 @@
font-weight: $font-weight-bold;
line-height: $line-height-base;
text-transform: uppercase;
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: background, transform;
}
.action-card-title {
@@ -240,5 +295,12 @@
gap: $space-2;
margin-top: $space-2;
}
+
+ @include hover_touch {
+ .action-card-kicker {
+ background: $color-primary;
+ transform: translateX($space-1);
+ }
+ }
}
}
diff --git a/src/scss/components/_chips.scss b/src/scss/components/_chips.scss
index e13a345..22d10dc 100644
--- a/src/scss/components/_chips.scss
+++ b/src/scss/components/_chips.scss
@@ -38,6 +38,9 @@
height: 7px;
flex: 0 0 auto;
background: $color-text-dark;
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: background, box-shadow, transform;
}
&:has(.ph)::before,
@@ -159,5 +162,11 @@
color: $color-text-light;
background: $surface-panel-strong;
border-color: $color-secondary;
+
+ &::before {
+ background: $color-secondary;
+ animation: terminal_pulse 0.7s $motion-ease;
+ transform: scale(1.12);
+ }
}
}
diff --git a/src/scss/components/_description-list.scss b/src/scss/components/_description-list.scss
index 42f4b80..21c62fc 100644
--- a/src/scss/components/_description-list.scss
+++ b/src/scss/components/_description-list.scss
@@ -16,10 +16,25 @@
gap: $space-4;
padding: $space-3 $space-4;
border-bottom: $border-width-base solid rgba($color-text-light, 0.08);
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: background;
&:last-child {
border-bottom: 0;
}
+
+ @include hover_touch {
+ background: $surface-panel-strong;
+
+ .description-list-term {
+ color: $color-secondary;
+ }
+
+ .description-list-value {
+ transform: translateX($space-1);
+ }
+ }
}
.description-list-term {
@@ -29,6 +44,9 @@
font-weight: $font-weight-semibold;
line-height: $line-height-normal;
text-transform: uppercase;
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: color;
}
.description-list-value {
@@ -41,6 +59,9 @@
color: $color-text-light;
font-size: $font-size-base;
line-height: $line-height-normal;
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: transform;
}
.description-list-value-muted {
diff --git a/src/scss/components/_forms.scss b/src/scss/components/_forms.scss
index 5c30865..a0ecf8b 100644
--- a/src/scss/components/_forms.scss
+++ b/src/scss/components/_forms.scss
@@ -60,6 +60,14 @@
&::placeholder {
color: $color-text-dark;
}
+
+ &::-webkit-search-cancel-button,
+ &::-webkit-search-decoration,
+ &::-webkit-search-results-button,
+ &::-webkit-search-results-decoration {
+ display: none;
+ -webkit-appearance: none;
+ }
}
textarea.input {
diff --git a/src/scss/components/_input-group.scss b/src/scss/components/_input-group.scss
index 0b8d1ab..467c557 100644
--- a/src/scss/components/_input-group.scss
+++ b/src/scss/components/_input-group.scss
@@ -68,6 +68,14 @@
&::placeholder {
color: $color-text-dark;
}
+
+ &::-webkit-search-cancel-button,
+ &::-webkit-search-decoration,
+ &::-webkit-search-results-button,
+ &::-webkit-search-results-decoration {
+ display: none;
+ -webkit-appearance: none;
+ }
}
.ph,
diff --git a/src/scss/components/_lists.scss b/src/scss/components/_lists.scss
index 6b9ec27..903c8b0 100644
--- a/src/scss/components/_lists.scss
+++ b/src/scss/components/_lists.scss
@@ -57,6 +57,9 @@
font-weight: $font-weight-bold;
line-height: $line-height-base;
text-transform: uppercase;
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: background, transform;
}
.list-desc {
@@ -64,6 +67,9 @@
color: $color-text-medium;
font-size: $font-size-sm;
line-height: $line-height-relaxed;
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: color, transform;
}
&:last-child {
@@ -75,6 +81,12 @@
.list-term {
background: $color-secondary;
+ transform: translateX($space-1);
+ }
+
+ .list-desc {
+ color: $color-text-light;
+ transform: translateX($space-1);
}
}
}
@@ -182,6 +194,12 @@
.list-controls {
// controls styles
}
+
+ @include hover_touch {
+ .list-title {
+ color: $color-secondary;
+ }
+ }
}
}
}
diff --git a/src/scss/components/_navigation-overlays.scss b/src/scss/components/_navigation-overlays.scss
index 8c3305e..009cbda 100644
--- a/src/scss/components/_navigation-overlays.scss
+++ b/src/scss/components/_navigation-overlays.scss
@@ -69,11 +69,13 @@
left: 0;
min-width: 220px;
display: none;
+ transform-origin: top left;
}
.dropdown.is-open .dropdown-menu,
.popover.is-open .popover-panel {
display: block;
+ animation: overlay_reveal $motion-base $motion-ease both;
}
.dropdown-menu {
@@ -170,4 +172,5 @@
.tooltip.is-open .tooltip-panel {
opacity: 1;
visibility: visible;
+ animation: tooltip_reveal $motion-fast $motion-ease both;
}
diff --git a/src/scss/components/_page-header.scss b/src/scss/components/_page-header.scss
index c8d2f06..16bc44a 100644
--- a/src/scss/components/_page-header.scss
+++ b/src/scss/components/_page-header.scss
@@ -2,6 +2,7 @@
@use "typography" as *;
.page-header {
+ position: relative;
display: flex;
flex-wrap: wrap;
align-items: flex-end;
@@ -12,6 +13,27 @@
border: $border-width-base solid $border-color-muted;
border-left-width: $border-width-accent;
background: $surface-panel-muted;
+ overflow: hidden;
+ animation: panel_boot $motion-slow $motion-ease both;
+
+ &::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 34%;
+ height: $border-width-base;
+ background: linear-gradient(90deg, transparent, $color-secondary, transparent);
+ opacity: 0.72;
+ pointer-events: none;
+ transform: translateX(-120%);
+ }
+
+ @include hover_touch {
+ &::after {
+ animation: terminal_scan_x 0.9s $motion-ease;
+ }
+ }
.page-header-content {
display: flex;
@@ -26,6 +48,9 @@
font-weight: $font-weight-bold;
line-height: $line-height-base;
text-transform: uppercase;
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: color;
}
.page-header-title {
diff --git a/src/scss/components/_tables.scss b/src/scss/components/_tables.scss
index 6591ba6..ddb8fb6 100644
--- a/src/scss/components/_tables.scss
+++ b/src/scss/components/_tables.scss
@@ -56,10 +56,21 @@
.table-body {
.table-row {
transition-duration: $motion-base;
- transition-property: background;
+ transition-timing-function: $motion-ease;
+ transition-property: background, color;
+
+ td {
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: color, background;
+ }
@include hover_touch {
background: rgba($color-secondary, 0.08);
+
+ td:first-child {
+ color: $color-secondary;
+ }
}
}
}
diff --git a/src/scss/components/_timeline.scss b/src/scss/components/_timeline.scss
index 60825b9..f5b211d 100644
--- a/src/scss/components/_timeline.scss
+++ b/src/scss/components/_timeline.scss
@@ -44,6 +44,9 @@
color: $color-text-medium;
background: $surface-page;
font-size: $icon-size-sm;
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: border-color, background, color, box-shadow, transform;
}
.timeline-content {
@@ -56,6 +59,9 @@
border: $border-width-base solid $border-color-muted;
border-left-width: $border-width-accent;
background: $surface-panel-muted;
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: border-color, background, transform;
}
.timeline-header {
@@ -132,6 +138,20 @@
background: $color-error;
}
}
+
+ .timeline-item {
+ @include hover_touch {
+ .timeline-marker {
+ box-shadow: 0 0 0 4px rgba($color-secondary, 0.14);
+ transform: scale(1.04);
+ }
+
+ .timeline-card {
+ background: $surface-panel-strong;
+ transform: translateX($space-1);
+ }
+ }
+ }
}
.activity-log {
@@ -149,10 +169,17 @@
align-items: center;
padding: $space-3 $space-4;
border-bottom: $border-width-base solid rgba($color-text-light, 0.08);
+ transition-duration: $motion-base;
+ transition-timing-function: $motion-ease;
+ transition-property: background;
&:last-child {
border-bottom: 0;
}
+
+ @include hover_touch {
+ background: $surface-panel-strong;
+ }
}
.activity-log-time {
diff --git a/src/scss/kit.scss b/src/scss/kit.scss
index 11bffc2..f09be7e 100644
--- a/src/scss/kit.scss
+++ b/src/scss/kit.scss
@@ -1,5 +1,6 @@
@use "fonts";
@use "kit-deps" as *;
+@use "motion";
@use "components/typography";
@use "components/palette";
@use "components/loader";