Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6ea9405a6 | |||
| 32f96e4c6f | |||
| e6c4a21f65 | |||
| 831ffc071a | |||
| 5dd7cb3fb8 | |||
| 64816558c1 | |||
| 019ead7be3 | |||
| 29974704d0 | |||
| 877c869a22 | |||
| a9743025a7 | |||
| 9f82275c63 | |||
| e203ecf687 | |||
| aa3ea04bfc | |||
|
|
27ca8ab4b8 |
37
.specify/memory/research/modern-ui-effects.md
Normal file
37
.specify/memory/research/modern-ui-effects.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Modern UI Effects Research (2025-2026)
|
||||||
|
|
||||||
|
## Liquid Glass (Apple WWDC 2025)
|
||||||
|
Evolved glassmorphism with directional lighting. Three-layer approach: highlight, shadow, illumination.
|
||||||
|
- `backdrop-filter: blur(20px) saturate(1.5)` — higher saturation than basic glass
|
||||||
|
- `inset 0 1px 0 rgba(255,255,255,0.15)` — top highlight (light direction)
|
||||||
|
- `inset 0 -1px 0 rgba(0,0,0,0.1)` — bottom shadow
|
||||||
|
- Outer drop shadow for depth: `0 8px 32px rgba(0,0,0,0.3)`
|
||||||
|
- Advanced: SVG `feTurbulence` + `feSpecularLighting` for refraction (Chromium only)
|
||||||
|
- Browser support: `backdrop-filter` ~88%, Firefox since v103
|
||||||
|
|
||||||
|
## Aurora / Gradient Mesh Backgrounds
|
||||||
|
Stacked animated radial gradients simulating northern lights. Pairs well with glass cards on dark backgrounds.
|
||||||
|
- Multiple `radial-gradient(ellipse ...)` layers with partial opacity
|
||||||
|
- Animated via `background-position` shift (GPU-friendly)
|
||||||
|
- `@property` rule enables direct gradient color animation (broad support since 2024)
|
||||||
|
- Best for ambient background movement, not for content areas
|
||||||
|
|
||||||
|
## Animated Glow Borders
|
||||||
|
Rotating `conic-gradient` borders with blur halo. Striking on dark backgrounds.
|
||||||
|
- Outer wrapper with `conic-gradient(from var(--angle), color1, color2, color3, color1)`
|
||||||
|
- `::before` pseudo with `filter: blur(12px)` and `opacity: 0.5` for glow halo
|
||||||
|
- `@property --angle` trick to animate custom property inside `conic-gradient`
|
||||||
|
- Use sparingly — best for single highlight elements (FAB, CTA), not all cards
|
||||||
|
|
||||||
|
## Modern Neumorphism (2025-2026 revision)
|
||||||
|
Subtler than the original trend. Higher contrast, less extreme extrusion, combined with accent colors.
|
||||||
|
- Light and dark shadow pair: `6px 6px 12px rgba(0,0,0,0.5)` + `-6px -6px 12px rgba(60,50,80,0.15)`
|
||||||
|
- `border: 1px solid rgba(255,255,255,0.05)` for definition
|
||||||
|
- Works on dark backgrounds with slightly lighter "uplift" shadow direction
|
||||||
|
- Better suited for interactive elements (buttons, toggles) than content cards
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
- Apple Liquid Glass CSS: dev.to/gruszdev, dev.to/kevinbism, css-tricks.com, kube.io
|
||||||
|
- Aurora: dev.to/oobleck, daltonwalsh.com, github.com/mattnewdavid
|
||||||
|
- Glow borders: frontendmasters.com (Kevin Powell), docode.co.in
|
||||||
|
- Trends overview: medium.com/design-bootcamp, index.dev, bighuman.com
|
||||||
108
frontend/package-lock.json
generated
108
frontend/package-lock.json
generated
@@ -3204,13 +3204,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-core": {
|
"node_modules/@vue/compiler-core": {
|
||||||
"version": "3.5.29",
|
"version": "3.5.30",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz",
|
||||||
"integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==",
|
"integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.29.0",
|
"@babel/parser": "^7.29.0",
|
||||||
"@vue/shared": "3.5.29",
|
"@vue/shared": "3.5.30",
|
||||||
"entities": "^7.0.1",
|
"entities": "^7.0.1",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
@@ -3229,40 +3229,40 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-dom": {
|
"node_modules/@vue/compiler-dom": {
|
||||||
"version": "3.5.29",
|
"version": "3.5.30",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz",
|
||||||
"integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==",
|
"integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-core": "3.5.29",
|
"@vue/compiler-core": "3.5.30",
|
||||||
"@vue/shared": "3.5.29"
|
"@vue/shared": "3.5.30"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-sfc": {
|
"node_modules/@vue/compiler-sfc": {
|
||||||
"version": "3.5.29",
|
"version": "3.5.30",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz",
|
||||||
"integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==",
|
"integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.29.0",
|
"@babel/parser": "^7.29.0",
|
||||||
"@vue/compiler-core": "3.5.29",
|
"@vue/compiler-core": "3.5.30",
|
||||||
"@vue/compiler-dom": "3.5.29",
|
"@vue/compiler-dom": "3.5.30",
|
||||||
"@vue/compiler-ssr": "3.5.29",
|
"@vue/compiler-ssr": "3.5.30",
|
||||||
"@vue/shared": "3.5.29",
|
"@vue/shared": "3.5.30",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.8",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-ssr": {
|
"node_modules/@vue/compiler-ssr": {
|
||||||
"version": "3.5.29",
|
"version": "3.5.30",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz",
|
||||||
"integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==",
|
"integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.29",
|
"@vue/compiler-dom": "3.5.30",
|
||||||
"@vue/shared": "3.5.29"
|
"@vue/shared": "3.5.30"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/devtools-api": {
|
"node_modules/@vue/devtools-api": {
|
||||||
@@ -3362,53 +3362,53 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
"version": "3.5.29",
|
"version": "3.5.30",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz",
|
||||||
"integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==",
|
"integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/shared": "3.5.29"
|
"@vue/shared": "3.5.30"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/runtime-core": {
|
"node_modules/@vue/runtime-core": {
|
||||||
"version": "3.5.29",
|
"version": "3.5.30",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz",
|
||||||
"integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==",
|
"integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.5.29",
|
"@vue/reactivity": "3.5.30",
|
||||||
"@vue/shared": "3.5.29"
|
"@vue/shared": "3.5.30"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/runtime-dom": {
|
"node_modules/@vue/runtime-dom": {
|
||||||
"version": "3.5.29",
|
"version": "3.5.30",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz",
|
||||||
"integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==",
|
"integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.5.29",
|
"@vue/reactivity": "3.5.30",
|
||||||
"@vue/runtime-core": "3.5.29",
|
"@vue/runtime-core": "3.5.30",
|
||||||
"@vue/shared": "3.5.29",
|
"@vue/shared": "3.5.30",
|
||||||
"csstype": "^3.2.3"
|
"csstype": "^3.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/server-renderer": {
|
"node_modules/@vue/server-renderer": {
|
||||||
"version": "3.5.29",
|
"version": "3.5.30",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz",
|
||||||
"integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==",
|
"integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-ssr": "3.5.29",
|
"@vue/compiler-ssr": "3.5.30",
|
||||||
"@vue/shared": "3.5.29"
|
"@vue/shared": "3.5.30"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "3.5.29"
|
"vue": "3.5.30"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/shared": {
|
"node_modules/@vue/shared": {
|
||||||
"version": "3.5.29",
|
"version": "3.5.30",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz",
|
||||||
"integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==",
|
"integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vue/test-utils": {
|
"node_modules/@vue/test-utils": {
|
||||||
@@ -7319,16 +7319,16 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vue": {
|
"node_modules/vue": {
|
||||||
"version": "3.5.29",
|
"version": "3.5.30",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz",
|
||||||
"integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==",
|
"integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.29",
|
"@vue/compiler-dom": "3.5.30",
|
||||||
"@vue/compiler-sfc": "3.5.29",
|
"@vue/compiler-sfc": "3.5.30",
|
||||||
"@vue/runtime-dom": "3.5.29",
|
"@vue/runtime-dom": "3.5.30",
|
||||||
"@vue/server-renderer": "3.5.29",
|
"@vue/server-renderer": "3.5.30",
|
||||||
"@vue/shared": "3.5.29"
|
"@vue/shared": "3.5.30"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "*"
|
"typescript": "*"
|
||||||
|
|||||||
@@ -16,6 +16,26 @@
|
|||||||
--color-text-on-gradient: #ffffff;
|
--color-text-on-gradient: #ffffff;
|
||||||
--color-surface: #fff5f8;
|
--color-surface: #fff5f8;
|
||||||
--color-card: #ffffff;
|
--color-card: #ffffff;
|
||||||
|
--color-dark-base: #1B1730;
|
||||||
|
|
||||||
|
/* Glass system */
|
||||||
|
--color-glass: rgba(255, 255, 255, 0.1);
|
||||||
|
--color-glass-strong: rgba(255, 255, 255, 0.15);
|
||||||
|
--color-glass-subtle: rgba(255, 255, 255, 0.05);
|
||||||
|
--color-glass-border: rgba(255, 255, 255, 0.18);
|
||||||
|
--color-glass-border-hover: rgba(255, 255, 255, 0.3);
|
||||||
|
--color-glass-hover: rgba(255, 255, 255, 0.18);
|
||||||
|
--color-glass-inner: rgba(27, 23, 48, 0.55);
|
||||||
|
--color-glass-overlay: rgba(27, 23, 48, 0.4);
|
||||||
|
|
||||||
|
/* Text on gradient (opacity variants) */
|
||||||
|
--color-text-muted: rgba(255, 255, 255, 0.5);
|
||||||
|
--color-text-secondary: rgba(255, 255, 255, 0.7);
|
||||||
|
--color-text-soft: rgba(255, 255, 255, 0.85);
|
||||||
|
--color-text-bright: rgba(255, 255, 255, 0.9);
|
||||||
|
|
||||||
|
/* Glow border */
|
||||||
|
--gradient-glow: conic-gradient(from 135deg, var(--color-gradient-start), var(--color-gradient-mid), var(--color-gradient-end), var(--color-gradient-start));
|
||||||
|
|
||||||
/* Gradient */
|
/* Gradient */
|
||||||
--gradient-primary: linear-gradient(135deg, #f06292 0%, #ab47bc 50%, #5c6bc0 100%);
|
--gradient-primary: linear-gradient(135deg, #f06292 0%, #ab47bc 50%, #5c6bc0 100%);
|
||||||
@@ -33,7 +53,7 @@
|
|||||||
--radius-button: 14px;
|
--radius-button: 14px;
|
||||||
|
|
||||||
/* Shadows */
|
/* Shadows */
|
||||||
--shadow-card: 0 2px 8px rgba(0, 0, 0, 0.1);
|
--shadow-card: 0 4px 24px rgba(0, 0, 0, 0.12);
|
||||||
--shadow-button: 0 2px 8px rgba(0, 0, 0, 0.15);
|
--shadow-button: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
/* Layout */
|
/* Layout */
|
||||||
@@ -60,7 +80,22 @@ html {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--gradient-primary);
|
background-color: var(--color-dark-base);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background-color: var(--color-dark-base);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(at 70% 20%, rgba(240, 98, 146, 0.55) 0px, transparent 50%),
|
||||||
|
radial-gradient(at 25% 50%, rgba(171, 71, 188, 0.5) 0px, transparent 55%),
|
||||||
|
radial-gradient(at 80% 70%, rgba(92, 107, 192, 0.55) 0px, transparent 50%),
|
||||||
|
radial-gradient(at 35% 85%, rgba(255, 112, 67, 0.3) 0px, transparent 40%);
|
||||||
|
filter: blur(80px);
|
||||||
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
@@ -82,28 +117,35 @@ body {
|
|||||||
/* Card-style form fields */
|
/* Card-style form fields */
|
||||||
.form-field {
|
.form-field {
|
||||||
background: var(--color-card);
|
background: var(--color-card);
|
||||||
border: none;
|
border: 1px solid #e0e0e0;
|
||||||
border-radius: var(--radius-card);
|
border-radius: var(--radius-card);
|
||||||
padding: var(--spacing-md) var(--spacing-md);
|
padding: var(--spacing-md) var(--spacing-md);
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: box-shadow 0.2s ease;
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field.glass {
|
||||||
|
color: var(--color-text-on-gradient);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field:focus {
|
.form-field:focus {
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.18);
|
border-color: var(--color-glass-border-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field::placeholder {
|
.form-field::placeholder {
|
||||||
color: #999;
|
color: var(--color-text-muted);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-field.glass::placeholder {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
textarea.form-field {
|
textarea.form-field {
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
min-height: 5rem;
|
min-height: 5rem;
|
||||||
@@ -128,22 +170,29 @@ textarea.form-field {
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--spacing-md) var(--spacing-lg);
|
padding: var(--spacing-md) var(--spacing-lg);
|
||||||
background: var(--color-accent);
|
background: var(--color-card);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
border: none;
|
border: 1px solid #e0e0e0;
|
||||||
border-radius: var(--radius-button);
|
border-radius: var(--radius-button);
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: var(--shadow-button);
|
transition: border-color 0.2s ease, transform 0.1s ease;
|
||||||
transition: opacity 0.2s ease, transform 0.1s ease;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-primary.glass {
|
||||||
|
color: var(--color-text-on-gradient);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background:
|
||||||
|
linear-gradient(var(--color-glass-inner), var(--color-glass-inner)) padding-box,
|
||||||
|
var(--gradient-glow) border-box;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
opacity: 0.92;
|
border-color: var(--color-glass-border-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:active {
|
.btn-primary:active {
|
||||||
@@ -176,6 +225,68 @@ textarea.form-field {
|
|||||||
100% { background-position: -200% 0; }
|
100% { background-position: -200% 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Glass System ── */
|
||||||
|
|
||||||
|
/* Glass surface: passive containers on gradient (cards, icon boxes) */
|
||||||
|
.glass {
|
||||||
|
background: linear-gradient(135deg, var(--color-glass-strong) 0%, var(--color-glass-subtle) 100%);
|
||||||
|
border: 1px solid var(--color-glass-border);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass:hover:not(input):not(textarea):not(.btn-primary) {
|
||||||
|
background: var(--color-glass-hover);
|
||||||
|
border-color: var(--color-glass-border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glass interactive inner: dark translucent fill for interactive elements (FAB, CTA) */
|
||||||
|
.glass-inner {
|
||||||
|
background: var(--color-glass-inner);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glow border: conic gradient wrapper with halo (static) */
|
||||||
|
.glow-border {
|
||||||
|
background: var(--gradient-glow);
|
||||||
|
padding: 2px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-border::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -4px;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: var(--gradient-glow);
|
||||||
|
filter: blur(8px);
|
||||||
|
opacity: 0.3;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glow border animated variant */
|
||||||
|
@property --glow-angle {
|
||||||
|
syntax: '<angle>';
|
||||||
|
initial-value: 0deg;
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-border--animated {
|
||||||
|
background: conic-gradient(from var(--glow-angle), var(--color-gradient-start), var(--color-gradient-mid), var(--color-gradient-end), var(--color-gradient-start));
|
||||||
|
animation: glow-rotate 4s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-border--animated::before {
|
||||||
|
background: conic-gradient(from var(--glow-angle), var(--color-gradient-start), var(--color-gradient-mid), var(--color-gradient-end), var(--color-gradient-start));
|
||||||
|
animation: glow-rotate 4s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow-rotate {
|
||||||
|
to { --glow-angle: 360deg; }
|
||||||
|
}
|
||||||
|
|
||||||
/* Utility */
|
/* Utility */
|
||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -197,7 +308,7 @@ textarea.form-field {
|
|||||||
.sheet-title {
|
.sheet-title {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-text);
|
color: var(--color-text-on-gradient);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rsvp-form {
|
.rsvp-form {
|
||||||
@@ -209,7 +320,7 @@ textarea.form-field {
|
|||||||
.rsvp-form__label {
|
.rsvp-form__label {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-text);
|
color: var(--color-text-on-gradient);
|
||||||
padding-left: 0.25rem;
|
padding-left: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ defineProps<{
|
|||||||
.attendee-list__heading {
|
.attendee-list__heading {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: var(--color-text-muted);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ defineProps<{
|
|||||||
|
|
||||||
.attendee-list__item {
|
.attendee-list__item {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: var(--color-text-soft);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -53,7 +53,7 @@ defineProps<{
|
|||||||
|
|
||||||
.attendee-list__empty {
|
.attendee-list__empty {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: var(--color-text-muted);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ watch(
|
|||||||
.sheet-backdrop {
|
.sheet-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0, 0, 0, 0.4);
|
background: var(--color-glass-overlay);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -53,7 +53,11 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sheet {
|
.sheet {
|
||||||
background: var(--color-card);
|
background: linear-gradient(135deg, var(--color-glass-strong) 0%, var(--color-glass-subtle) 100%);
|
||||||
|
border: 1px solid var(--color-glass-border);
|
||||||
|
border-bottom: none;
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
-webkit-backdrop-filter: blur(24px);
|
||||||
border-radius: 20px 20px 0 0;
|
border-radius: 20px 20px 0 0;
|
||||||
padding: var(--spacing-lg) var(--spacing-xl) var(--spacing-2xl);
|
padding: var(--spacing-lg) var(--spacing-xl) var(--spacing-2xl);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -67,7 +71,7 @@ watch(
|
|||||||
.sheet__handle {
|
.sheet__handle {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
background: #ccc;
|
background: var(--color-glass-border-hover);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ watch(
|
|||||||
.confirm-dialog__overlay {
|
.confirm-dialog__overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0, 0, 0, 0.4);
|
background: var(--color-glass-overlay);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -84,9 +84,12 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.confirm-dialog {
|
.confirm-dialog {
|
||||||
background: var(--color-card);
|
background: linear-gradient(135deg, var(--color-glass-strong) 0%, var(--color-glass-subtle) 100%);
|
||||||
|
border: 1px solid var(--color-glass-border);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
-webkit-backdrop-filter: blur(24px);
|
||||||
border-radius: var(--radius-card);
|
border-radius: var(--radius-card);
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl);
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -98,13 +101,13 @@ watch(
|
|||||||
.confirm-dialog__title {
|
.confirm-dialog__title {
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-text);
|
color: var(--color-text-on-gradient);
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirm-dialog__message {
|
.confirm-dialog__message {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #666;
|
color: var(--color-text-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirm-dialog__actions {
|
.confirm-dialog__actions {
|
||||||
@@ -130,8 +133,9 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.confirm-dialog__btn--cancel {
|
.confirm-dialog__btn--cancel {
|
||||||
background: #e8e8e8;
|
background: var(--color-glass);
|
||||||
color: #555;
|
border: 1px solid var(--color-glass-border);
|
||||||
|
color: var(--color-text-on-gradient);
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirm-dialog__btn--confirm {
|
.confirm-dialog__btn--confirm {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<RouterLink to="/create" class="fab" aria-label="Create event">
|
<RouterLink to="/create" class="fab glow-border" aria-label="Create event">
|
||||||
|
<span class="fab__inner glass-inner">
|
||||||
<span class="fab__icon" aria-hidden="true">+</span>
|
<span class="fab__icon" aria-hidden="true">+</span>
|
||||||
|
</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -16,20 +18,26 @@ import { RouterLink } from 'vue-router'
|
|||||||
width: 56px;
|
width: 56px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--color-accent);
|
color: var(--color-text-on-gradient);
|
||||||
color: #fff;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab__inner {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fab:hover {
|
.fab:hover {
|
||||||
transform: scale(1.08);
|
transform: scale(1.08);
|
||||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fab:active {
|
.fab:active {
|
||||||
@@ -41,6 +49,7 @@ import { RouterLink } from 'vue-router'
|
|||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.fab__icon {
|
.fab__icon {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ defineProps<{
|
|||||||
.date-subheader {
|
.date-subheader {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: var(--color-text-soft);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: var(--spacing-xs) 0;
|
padding: var(--spacing-xs) 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<p class="empty-state__message">No events yet.<br />Create your first one!</p>
|
<p class="empty-state__message">No events yet.<br />Create your first one!</p>
|
||||||
<RouterLink to="/create" class="btn-primary empty-state__cta">+ Create Event</RouterLink>
|
<RouterLink to="/create" class="empty-state__cta glow-border glow-border--animated">
|
||||||
|
<span class="empty-state__cta-inner glass-inner">Create Event</span>
|
||||||
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -27,5 +29,34 @@ import { RouterLink } from 'vue-router'
|
|||||||
|
|
||||||
.empty-state__cta {
|
.empty-state__cta {
|
||||||
max-width: 280px;
|
max-width: 280px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: var(--radius-button);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state__cta-inner {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--spacing-md) var(--spacing-lg);
|
||||||
|
border-radius: calc(var(--radius-button) - 2px);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-on-gradient);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state__cta:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state__cta:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state__cta:focus-visible {
|
||||||
|
outline: 2px solid #fff;
|
||||||
|
outline-offset: 3px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="event-card"
|
class="event-card glass"
|
||||||
:class="{ 'event-card--past': isPast, 'event-card--swiping': isSwiping }"
|
:class="{ 'event-card--past': isPast, 'event-card--swiping': isSwiping }"
|
||||||
:style="swipeStyle"
|
:style="swipeStyle"
|
||||||
@touchstart="onTouchStart"
|
@touchstart="onTouchStart"
|
||||||
@@ -93,11 +93,10 @@ function onTouchEnd() {
|
|||||||
.event-card {
|
.event-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--color-card);
|
|
||||||
border-radius: var(--radius-card);
|
border-radius: var(--radius-card);
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
padding: var(--spacing-md) var(--spacing-lg);
|
padding: var(--spacing-md) var(--spacing-lg);
|
||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-sm);
|
||||||
|
transition: background 0.2s ease, border-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-card--past {
|
.event-card--past {
|
||||||
@@ -122,7 +121,7 @@ function onTouchEnd() {
|
|||||||
.event-card__title {
|
.event-card__title {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-text);
|
color: var(--color-text-on-gradient);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -131,7 +130,7 @@ function onTouchEnd() {
|
|||||||
.event-card__time {
|
.event-card__time {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #888;
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-card__badge {
|
.event-card__badge {
|
||||||
@@ -145,12 +144,12 @@ function onTouchEnd() {
|
|||||||
|
|
||||||
.event-card__badge--organizer {
|
.event-card__badge--organizer {
|
||||||
background: var(--color-accent);
|
background: var(--color-accent);
|
||||||
color: #fff;
|
color: var(--color-text-on-gradient);
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-card__badge--attendee {
|
.event-card__badge--attendee {
|
||||||
background: #e0e0e0;
|
background: var(--color-glass-strong);
|
||||||
color: #555;
|
color: var(--color-text-bright);
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-card__delete {
|
.event-card__delete {
|
||||||
@@ -163,7 +162,7 @@ function onTouchEnd() {
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
color: #bbb;
|
color: var(--color-text-muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transition: color 0.15s ease, background 0.15s ease;
|
transition: color 0.15s ease, background 0.15s ease;
|
||||||
|
|||||||
@@ -8,11 +8,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CTA state: no RSVP yet -->
|
<!-- CTA state: no RSVP yet -->
|
||||||
<button v-else class="btn-primary rsvp-bar__cta" type="button" @click="$emit('open')">
|
<div v-else class="rsvp-bar__cta glow-border glow-border--animated">
|
||||||
|
<button class="rsvp-bar__cta-inner glass-inner" type="button" @click="$emit('open')">
|
||||||
I'm attending
|
I'm attending
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -45,6 +47,30 @@ defineEmits<{
|
|||||||
|
|
||||||
.rsvp-bar__cta {
|
.rsvp-bar__cta {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border-radius: var(--radius-button);
|
||||||
|
transition: transform 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rsvp-bar__cta:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rsvp-bar__cta:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rsvp-bar__cta-inner {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--spacing-md) var(--spacing-lg);
|
||||||
|
border-radius: calc(var(--radius-button) - 2px);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-on-gradient);
|
||||||
|
text-align: center;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rsvp-bar__status {
|
.rsvp-bar__status {
|
||||||
@@ -52,13 +78,16 @@ defineEmits<{
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
background: var(--color-card);
|
background: linear-gradient(135deg, var(--color-glass-strong) 0%, var(--color-glass-subtle) 100%);
|
||||||
|
border: 1px solid var(--color-glass-border);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
border-radius: var(--radius-card);
|
border-radius: var(--radius-card);
|
||||||
padding: var(--spacing-md) var(--spacing-lg);
|
padding: var(--spacing-md) var(--spacing-lg);
|
||||||
box-shadow: var(--shadow-card);
|
box-shadow: var(--shadow-card);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
color: var(--color-text);
|
color: var(--color-text-on-gradient);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rsvp-bar__check {
|
.rsvp-bar__check {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ defineProps<{
|
|||||||
.section-header {
|
.section-header {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-text);
|
color: var(--color-text-on-gradient);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: var(--spacing-sm) 0;
|
padding: var(--spacing-sm) 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ describe('RsvpBar', () => {
|
|||||||
it('renders CTA button when hasRsvp is false', () => {
|
it('renders CTA button when hasRsvp is false', () => {
|
||||||
const wrapper = mount(RsvpBar)
|
const wrapper = mount(RsvpBar)
|
||||||
expect(wrapper.find('.rsvp-bar__cta').exists()).toBe(true)
|
expect(wrapper.find('.rsvp-bar__cta').exists()).toBe(true)
|
||||||
expect(wrapper.find('.rsvp-bar__cta').text()).toBe("I'm attending")
|
expect(wrapper.find('.rsvp-bar__cta-inner').text()).toBe("I'm attending")
|
||||||
expect(wrapper.find('.rsvp-bar__status').exists()).toBe(false)
|
expect(wrapper.find('.rsvp-bar__status').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ describe('RsvpBar', () => {
|
|||||||
|
|
||||||
it('emits open when CTA button is clicked', async () => {
|
it('emits open when CTA button is clicked', async () => {
|
||||||
const wrapper = mount(RsvpBar)
|
const wrapper = mount(RsvpBar)
|
||||||
await wrapper.find('.rsvp-bar__cta').trigger('click')
|
await wrapper.find('.rsvp-bar__cta-inner').trigger('click')
|
||||||
expect(wrapper.emitted('open')).toHaveLength(1)
|
expect(wrapper.emitted('open')).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
id="title"
|
id="title"
|
||||||
v-model="form.title"
|
v-model="form.title"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-field"
|
class="form-field glass"
|
||||||
required
|
required
|
||||||
maxlength="200"
|
maxlength="200"
|
||||||
placeholder="What's the event?"
|
placeholder="What's the event?"
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<textarea
|
<textarea
|
||||||
id="description"
|
id="description"
|
||||||
v-model="form.description"
|
v-model="form.description"
|
||||||
class="form-field"
|
class="form-field glass"
|
||||||
maxlength="2000"
|
maxlength="2000"
|
||||||
placeholder="Tell people more about it…"
|
placeholder="Tell people more about it…"
|
||||||
:aria-invalid="!!errors.description"
|
:aria-invalid="!!errors.description"
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
id="dateTime"
|
id="dateTime"
|
||||||
v-model="form.dateTime"
|
v-model="form.dateTime"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
class="form-field"
|
class="form-field glass"
|
||||||
required
|
required
|
||||||
:aria-invalid="!!errors.dateTime"
|
:aria-invalid="!!errors.dateTime"
|
||||||
:aria-describedby="errors.dateTime ? 'dateTime-error' : undefined"
|
:aria-describedby="errors.dateTime ? 'dateTime-error' : undefined"
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
id="location"
|
id="location"
|
||||||
v-model="form.location"
|
v-model="form.location"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-field"
|
class="form-field glass"
|
||||||
maxlength="500"
|
maxlength="500"
|
||||||
placeholder="Where is it?"
|
placeholder="Where is it?"
|
||||||
:aria-invalid="!!errors.location"
|
:aria-invalid="!!errors.location"
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
id="expiryDate"
|
id="expiryDate"
|
||||||
v-model="form.expiryDate"
|
v-model="form.expiryDate"
|
||||||
type="date"
|
type="date"
|
||||||
class="form-field"
|
class="form-field glass"
|
||||||
required
|
required
|
||||||
:min="tomorrow"
|
:min="tomorrow"
|
||||||
:aria-invalid="!!errors.expiryDate"
|
:aria-invalid="!!errors.expiryDate"
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<span v-if="errors.expiryDate" id="expiryDate-error" class="field-error" role="alert">{{ errors.expiryDate }}</span>
|
<span v-if="errors.expiryDate" id="expiryDate-error" class="field-error" role="alert">{{ errors.expiryDate }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn-primary" :disabled="submitting">
|
<button type="submit" class="btn-primary glass" :disabled="submitting">
|
||||||
{{ submitting ? 'Creating…' : 'Create Event' }}
|
{{ submitting ? 'Creating…' : 'Create Event' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -33,21 +33,21 @@
|
|||||||
|
|
||||||
<dl class="detail__meta">
|
<dl class="detail__meta">
|
||||||
<div class="detail__meta-item">
|
<div class="detail__meta-item">
|
||||||
<dt class="detail__meta-icon" aria-label="Date and time">
|
<dt class="detail__meta-icon glass" aria-label="Date and time">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="detail__meta-text">{{ formattedDateTime }}</dd>
|
<dd class="detail__meta-text">{{ formattedDateTime }}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="event.location" class="detail__meta-item">
|
<div v-if="event.location" class="detail__meta-item">
|
||||||
<dt class="detail__meta-icon" aria-label="Location">
|
<dt class="detail__meta-icon glass" aria-label="Location">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="detail__meta-text">{{ event.location }}</dd>
|
<dd class="detail__meta-text">{{ event.location }}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail__meta-item">
|
<div class="detail__meta-item">
|
||||||
<dt class="detail__meta-icon" aria-label="Attendees">
|
<dt class="detail__meta-icon glass" aria-label="Attendees">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="detail__meta-text">{{ event.attendeeCount }} going</dd>
|
<dd class="detail__meta-text">{{ event.attendeeCount }} going</dd>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
<!-- Error state -->
|
<!-- Error state -->
|
||||||
<div v-else-if="state === 'error'" class="detail__content detail__content--center" role="alert">
|
<div v-else-if="state === 'error'" class="detail__content detail__content--center" role="alert">
|
||||||
<p class="detail__message">Something went wrong.</p>
|
<p class="detail__message">Something went wrong.</p>
|
||||||
<button class="btn-primary" type="button" @click="fetchEvent">Retry</button>
|
<button class="btn-primary glass" type="button" @click="fetchEvent">Retry</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
<input
|
<input
|
||||||
id="rsvp-name"
|
id="rsvp-name"
|
||||||
v-model.trim="nameInput"
|
v-model.trim="nameInput"
|
||||||
class="form-field"
|
class="form-field glass"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g. Max Mustermann"
|
placeholder="e.g. Max Mustermann"
|
||||||
maxlength="100"
|
maxlength="100"
|
||||||
@@ -99,9 +99,11 @@
|
|||||||
/>
|
/>
|
||||||
<span v-if="nameError" class="rsvp-form__field-error" role="alert">{{ nameError }}</span>
|
<span v-if="nameError" class="rsvp-form__field-error" role="alert">{{ nameError }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" type="submit" :disabled="submitting">
|
<div class="rsvp-form__submit glow-border glow-border--animated">
|
||||||
|
<button class="rsvp-form__submit-inner glass-inner" type="submit" :disabled="submitting">
|
||||||
{{ submitting ? 'Sending…' : "Count me in" }}
|
{{ submitting ? 'Sending…' : "Count me in" }}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
<p v-if="submitError" class="rsvp-form__field-error rsvp-form__error" role="alert">{{ submitError }}</p>
|
<p v-if="submitError" class="rsvp-form__field-error rsvp-form__error" role="alert">{{ submitError }}</p>
|
||||||
</form>
|
</form>
|
||||||
</BottomSheet>
|
</BottomSheet>
|
||||||
@@ -268,15 +270,19 @@ onMounted(fetchEvent)
|
|||||||
.detail__hero {
|
.detail__hero {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 260px;
|
height: 420px;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail__hero-img {
|
.detail__hero-img {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
mask-image: linear-gradient(to bottom, black 40%, transparent 100%);
|
||||||
|
-webkit-mask-image: linear-gradient(to bottom, black 40%, transparent 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail__hero-overlay {
|
.detail__hero-overlay {
|
||||||
@@ -284,9 +290,8 @@ onMounted(fetchEvent)
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
rgba(0, 0, 0, 0.4) 0%,
|
var(--color-glass-overlay) 0%,
|
||||||
transparent 50%,
|
transparent 50%
|
||||||
var(--color-gradient-start) 100%
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +371,6 @@ onMounted(fetchEvent)
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
color: var(--color-text-on-gradient);
|
color: var(--color-text-on-gradient);
|
||||||
}
|
}
|
||||||
@@ -387,14 +391,14 @@ onMounted(fetchEvent)
|
|||||||
.detail__section-title {
|
.detail__section-title {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: var(--color-text-muted);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail__description {
|
.detail__description {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: var(--color-text-soft);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
@@ -409,8 +413,8 @@ onMounted(fetchEvent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detail__banner--expired {
|
.detail__banner--expired {
|
||||||
background: rgba(255, 255, 255, 0.12);
|
background: var(--color-glass);
|
||||||
color: rgba(255, 255, 255, 0.8);
|
color: var(--color-text-soft);
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,7 +427,7 @@ onMounted(fetchEvent)
|
|||||||
|
|
||||||
/* Skeleton – shimmer on gradient */
|
/* Skeleton – shimmer on gradient */
|
||||||
.skeleton {
|
.skeleton {
|
||||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.1) 25%, rgba(255, 255, 255, 0.25) 50%, rgba(255, 255, 255, 0.1) 75%);
|
background: linear-gradient(90deg, var(--color-glass) 25%, var(--color-glass-hover) 50%, var(--color-glass) 75%);
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,4 +446,38 @@ onMounted(fetchEvent)
|
|||||||
.skeleton--short {
|
.skeleton--short {
|
||||||
width: 45%;
|
width: 45%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* RSVP submit button (glow border wrapper) */
|
||||||
|
.rsvp-form__submit {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: var(--radius-button);
|
||||||
|
transition: transform 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rsvp-form__submit:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rsvp-form__submit:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rsvp-form__submit-inner {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--spacing-md) var(--spacing-lg);
|
||||||
|
border-radius: calc(var(--radius-button) - 2px);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-on-gradient);
|
||||||
|
text-align: center;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rsvp-form__submit-inner:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ describe('EventDetailView', () => {
|
|||||||
|
|
||||||
expect(document.body.querySelector('[role="dialog"]')).toBeNull()
|
expect(document.body.querySelector('[role="dialog"]')).toBeNull()
|
||||||
|
|
||||||
await wrapper.find('.rsvp-bar__cta').trigger('click')
|
await wrapper.find('.rsvp-bar__cta-inner').trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(document.body.querySelector('[role="dialog"]')).not.toBeNull()
|
expect(document.body.querySelector('[role="dialog"]')).not.toBeNull()
|
||||||
@@ -275,7 +275,7 @@ describe('EventDetailView', () => {
|
|||||||
const wrapper = await mountWithToken()
|
const wrapper = await mountWithToken()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
await wrapper.find('.rsvp-bar__cta').trigger('click')
|
await wrapper.find('.rsvp-bar__cta-inner').trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
// Form is inside Teleport — find via document.body
|
// Form is inside Teleport — find via document.body
|
||||||
@@ -300,7 +300,7 @@ describe('EventDetailView', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
// Open sheet
|
// Open sheet
|
||||||
await wrapper.find('.rsvp-bar__cta').trigger('click')
|
await wrapper.find('.rsvp-bar__cta-inner').trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
// Fill name via Teleported input
|
// Fill name via Teleported input
|
||||||
@@ -386,7 +386,7 @@ describe('EventDetailView', () => {
|
|||||||
const wrapper = await mountWithToken()
|
const wrapper = await mountWithToken()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
await wrapper.find('.rsvp-bar__cta').trigger('click')
|
await wrapper.find('.rsvp-bar__cta-inner').trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
const input = document.body.querySelector('#rsvp-name')! as HTMLInputElement
|
const input = document.body.querySelector('#rsvp-name')! as HTMLInputElement
|
||||||
|
|||||||
Reference in New Issue
Block a user