diff --git a/demo/partials/accordion.html b/demo/partials/accordion.html index c922cb2..9ebc149 100644 --- a/demo/partials/accordion.html +++ b/demo/partials/accordion.html @@ -1,7 +1,7 @@

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";