diff --git a/CHANGELOG.md b/CHANGELOG.md index e95b5509b..878d90326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Added - Introduced data source transformation support in the import functionality for self-hosted environments +- Added an optional 3D hover effect to the membership card component #### Changed diff --git a/libs/ui/src/lib/membership-card/membership-card.component.html b/libs/ui/src/lib/membership-card/membership-card.component.html index 1c68f5e3f..9faac0d3d 100644 --- a/libs/ui/src/lib/membership-card/membership-card.component.html +++ b/libs/ui/src/lib/membership-card/membership-card.component.html @@ -1,50 +1,54 @@ -
- -
- -
- @if (hasPermissionToCreateApiKey) { -
-
API Key
-
-
* * * * * * * * *
-
- -
-
-
- } -
-
-
Membership
-
{{ name }}
+
+ diff --git a/libs/ui/src/lib/membership-card/membership-card.component.scss b/libs/ui/src/lib/membership-card/membership-card.component.scss index 270adc0f1..fcd923f12 100644 --- a/libs/ui/src/lib/membership-card/membership-card.component.scss +++ b/libs/ui/src/lib/membership-card/membership-card.component.scss @@ -1,71 +1,231 @@ :host { --borderRadius: 1rem; --borderWidth: 2px; + --hover3dSpotlightOpacity: 0.2; display: block; max-width: 25rem; padding-top: calc(1 * var(--borderWidth)); width: 100%; - .card-container { - border-radius: var(--borderRadius); - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15); + .card-wrapper { + &.hover-3d { + perspective: 1000px; + } - &:after { - animation: animatedborder 7s ease alternate infinite; - background: linear-gradient(60deg, #5073b8, #1098ad, #07b39b, #6fba82); - background-size: 300% 300%; + .card-container { border-radius: var(--borderRadius); - content: ''; - height: calc(100% + var(--borderWidth) * 2); - left: calc(-1 * var(--borderWidth)); - top: calc(-1 * var(--borderWidth)); - position: absolute; - width: calc(100% + var(--borderWidth) * 2); - z-index: -1; - - @keyframes animatedborder { - 0% { - background-position: 0% 50%; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15); + + .card-item { + aspect-ratio: 1.586; + background-color: #1d2124; + border-radius: calc(var(--borderRadius) - var(--borderWidth)); + color: rgba(var(--light-primary-text)); + line-height: 1.2; + + button { + color: rgba(var(--light-primary-text)); + height: 1.5rem; + z-index: 3; } - 50% { - background-position: 100% 50%; + + .heading { + font-size: 13px; } - 100% { - background-position: 0% 50%; + + .value { + font-size: 18px; + } + } + + &:not(.premium) { + &::after { + opacity: 0; + } + + .card-item { + background-color: #ffffff; + color: rgba(var(--dark-primary-text)); } } } - .card-item { - aspect-ratio: 1.586; - background-color: #1d2124; - border-radius: calc(var(--borderRadius) - var(--borderWidth)); - color: rgba(var(--light-primary-text)); - line-height: 1.2; + &.hover-3d { + --hover3d-rotate-x: 0; + --hover3d-rotate-y: 0; + --hover3d-shine: 100% 100%; - button { - color: rgba(var(--light-primary-text)); - height: 1.5rem; + .card-container { + overflow: hidden; + position: relative; + scale: 1; + transform: rotate3d( + var(--hover3d-rotate-x), + var(--hover3d-rotate-y), + 0, + 10deg + ); + transform-style: preserve-3d; + transition: + box-shadow 400ms ease-out, + scale 500ms ease-out, + transform 500ms ease-out; + will-change: transform, scale; + + &::before { + background-image: radial-gradient( + circle at 50%, + rgba(255, 255, 255, var(--hover3dSpotlightOpacity)) 10%, + transparent 50% + ); + content: ''; + filter: blur(0.75rem); + height: 33.333%; + opacity: 0; + pointer-events: none; + position: absolute; + scale: 500%; + translate: var(--hover3d-shine); + transition: + opacity 400ms ease-out, + translate 400ms ease-out; + width: 33.333%; + z-index: 1; + } + + .card-item { + position: relative; + + .hover-zone { + height: 33.333%; + width: 33.333%; + z-index: 2; + + &:nth-child(1) { + left: 0; + top: 0; + } + + &:nth-child(2) { + left: 33.333%; + top: 0; + } + + &:nth-child(3) { + right: 0; + top: 0; + } + + &:nth-child(4) { + left: 0; + top: 33.333%; + } + + &:nth-child(5) { + left: 33.333%; + top: 33.333%; + } + + &:nth-child(6) { + right: 0; + top: 33.333%; + } + + &:nth-child(7) { + bottom: 0; + left: 0; + } + + &:nth-child(8) { + bottom: 0; + left: 33.333%; + } + + &:nth-child(9) { + bottom: 0; + right: 0; + } + } + } } - .heading { - font-size: 13px; + &:has(.hover-zone:hover) .card-container { + box-shadow: 0 18px 40px rgba(15, 23, 42, 0.3); + scale: 1.05; + + &::before { + opacity: 1; + } } - .value { - font-size: 18px; + &:has(.hover-zone:nth-child(1):hover) { + --hover3d-rotate-x: 1; + --hover3d-rotate-y: -1; + --hover3d-shine: 0% 0%; } - } - &:not(.premium) { - &:after { - opacity: 0; + &:has(.hover-zone:nth-child(2):hover) { + --hover3d-rotate-x: 1; + --hover3d-rotate-y: 0; + --hover3d-shine: 100% 0%; } - .card-item { - background-color: #ffffff; - color: rgba(var(--dark-primary-text)); + &:has(.hover-zone:nth-child(3):hover) { + --hover3d-rotate-x: 1; + --hover3d-rotate-y: 1; + --hover3d-shine: 200% 0%; + } + + &:has(.hover-zone:nth-child(4):hover) { + --hover3d-rotate-x: 0; + --hover3d-rotate-y: -1; + --hover3d-shine: 0% 100%; + } + + &:has(.hover-zone:nth-child(5):hover) { + --hover3d-rotate-x: 0; + --hover3d-rotate-y: 0; + --hover3d-shine: 100% 100%; + } + + &:has(.hover-zone:nth-child(6):hover) { + --hover3d-rotate-x: 0; + --hover3d-rotate-y: 1; + --hover3d-shine: 200% 100%; + } + + &:has(.hover-zone:nth-child(7):hover) { + --hover3d-rotate-x: -1; + --hover3d-rotate-y: -1; + --hover3d-shine: 0% 200%; + } + + &:has(.hover-zone:nth-child(8):hover) { + --hover3d-rotate-x: -1; + --hover3d-rotate-y: 0; + --hover3d-shine: 100% 200%; + } + + &:has(.hover-zone:nth-child(9):hover) { + --hover3d-rotate-x: -1; + --hover3d-rotate-y: 1; + --hover3d-shine: 200% 200%; + } + } + } + + @media (prefers-reduced-motion: reduce) { + .card-wrapper.hover-3d { + .card-container { + scale: 1 !important; + transform: none !important; + transition: none !important; + + &::before { + opacity: 0 !important; + transition: none !important; + } } } } diff --git a/libs/ui/src/lib/membership-card/membership-card.component.stories.ts b/libs/ui/src/lib/membership-card/membership-card.component.stories.ts index 6b6fbe038..0d475bda7 100644 --- a/libs/ui/src/lib/membership-card/membership-card.component.stories.ts +++ b/libs/ui/src/lib/membership-card/membership-card.component.stories.ts @@ -26,6 +26,9 @@ export default { }) ], argTypes: { + hover3d: { + control: { type: 'boolean' } + }, name: { control: { type: 'select' }, options: ['Basic', 'Premium'] @@ -37,6 +40,7 @@ type Story = StoryObj; export const Basic: Story = { args: { + hover3d: false, name: 'Basic' } }; @@ -45,6 +49,7 @@ export const Premium: Story = { args: { expiresAt: addYears(new Date(), 1).toLocaleDateString(), hasPermissionToCreateApiKey: true, + hover3d: false, name: 'Premium' } }; diff --git a/libs/ui/src/lib/membership-card/membership-card.component.ts b/libs/ui/src/lib/membership-card/membership-card.component.ts index 175a94f42..be223758d 100644 --- a/libs/ui/src/lib/membership-card/membership-card.component.ts +++ b/libs/ui/src/lib/membership-card/membership-card.component.ts @@ -34,6 +34,7 @@ import { GfLogoComponent } from '../logo'; export class GfMembershipCardComponent { @Input() public expiresAt: string; @Input() public hasPermissionToCreateApiKey: boolean; + @Input() public hover3d = false; @Input() public name: string; @Output() generateApiKeyClicked = new EventEmitter();