Compare commits

...
14 Commits
Author SHA1 Message Date
Renovate Bot de7d7442ff Update dependency @vitest/eslint-plugin to v1.6.10
CI / backend-test (push) Successful in 1m8s
CI / frontend-test (push) Successful in 24s
CI / frontend-e2e (push) Successful in 1m14s
CI / build-and-publish (push) Has been skipped
2026-03-09 19:03:02 +00:00
nitrixandClaude Opus 4.6 fa34223c10 Add tada emoji as SVG favicon
CI / backend-test (push) Successful in 59s
CI / frontend-test (push) Successful in 24s
CI / frontend-e2e (push) Successful in 1m12s
CI / build-and-publish (push) Successful in 1m2s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 19:26:38 +01:00
nitrix e6ea9405a6 Merge pull request 'Apply glassmorphism design system across all UI surfaces' (#23) from glassmorphism-event-cards into master
CI / backend-test (push) Successful in 58s
CI / frontend-test (push) Successful in 24s
CI / frontend-e2e (push) Successful in 1m10s
CI / build-and-publish (push) Successful in 1m9s
2026-03-09 19:11:52 +01:00
nitrixandClaude Opus 4.6 32f96e4c6f Replace hardcoded color values with glass design tokens
CI / backend-test (push) Successful in 1m0s
CI / frontend-test (push) Successful in 25s
CI / frontend-e2e (push) Successful in 1m13s
CI / build-and-publish (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 19:07:43 +01:00
nitrixandClaude Opus 4.6 e6c4a21f65 Apply glassmorphism to ConfirmDialog overlay and surface
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 19:00:39 +01:00
nitrixandClaude Opus 4.6 831ffc071a Apply glassmorphism to BottomSheet and RSVP bar status
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:57:30 +01:00
nitrixandClaude Opus 4.6 5dd7cb3fb8 Add animated glow border to RSVP CTA button
Wrap the "I'm attending" button with animated glow-border and
glass-inner styling. Update test selectors for new structure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:51:21 +01:00
nitrixandClaude Opus 4.6 64816558c1 Apply glass utility class to form fields and buttons
Use .glass class on form fields and buttons on gradient backgrounds.
Buttons get gradient glow border via background-clip trick. Solid
white fallback preserved for BottomSheet context.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:47:57 +01:00
nitrixandClaude Opus 4.6 019ead7be3 Extract glass system into shared CSS utilities and design tokens
Centralize all hardcoded rgba color values into CSS custom properties
and extract glass/glow styles into reusable utility classes (.glass,
.glass-inner, .glow-border, .glow-border--animated) in main.css.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:35:36 +01:00
nitrixandClaude Opus 4.6 29974704d0 Apply glassmorphism to meta icon boxes on event detail view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:22:39 +01:00
nitrixandClaude Opus 4.6 877c869a22 Restyle FAB with glass effect and static glow border
Replace solid orange FAB with glassmorphism inner and a conic
gradient border (pink-purple-indigo) with subtle glow halo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:20:50 +01:00
nitrixandClaude Opus 4.6 a9743025a7 Fix hero image transition on event detail page
Replace hard-edged color overlay with CSS mask-image fade-out and
increase hero height to 420px for a seamless blend into the aurora
mesh background.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:13:57 +01:00
nitrixandClaude Opus 4.6 9f82275c63 Replace linear gradient background with aurora mesh gradient
Use layered radial gradients on a dark base (#1B1730) with
backdrop blur for an organic, aurora-like background effect
that better complements the glassmorphism event cards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:01:46 +01:00
nitrixandClaude Opus 4.6 e203ecf687 Apply glassmorphism styling to event cards on list view
Replace solid white event cards with glass-effect cards featuring
backdrop blur, semi-transparent gradient backgrounds, and light
borders that blend with the Electric Dusk gradient background.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:50:20 +01:00
18 changed files with 351 additions and 85 deletions
@@ -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
+1
View File
@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<title>fete</title>
</head>
<body>
+3 -3
View File
@@ -2947,9 +2947,9 @@
}
},
"node_modules/@vitest/eslint-plugin": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.6.9.tgz",
"integrity": "sha512-9WfPx1OwJ19QLCSRLkqVO7//1WcWnK3fE/3fJhKMAmDe8+9G4rB47xCNIIeCq3FdEzkIoLTfDlwDlPBaUTMhow==",
"version": "1.6.10",
"resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.6.10.tgz",
"integrity": "sha512-/cOf+mTu4HBJIYHTETo8/OFCSZv3T2p+KfGnouzKfjK063cWLZp0TzvK7EU5B3eFG7ypUNtw6l+jK+SA+p1g8g==",
"dev": true,
"license": "MIT",
"dependencies": {
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<text y="0.9em" font-size="80" x="50%" text-anchor="middle">🎉</text>
</svg>

After

Width:  |  Height:  |  Size: 144 B

+125 -14
View File
@@ -16,6 +16,26 @@
--color-text-on-gradient: #ffffff;
--color-surface: #fff5f8;
--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-primary: linear-gradient(135deg, #f06292 0%, #ab47bc 50%, #5c6bc0 100%);
@@ -33,7 +53,7 @@
--radius-button: 14px;
/* 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);
/* Layout */
@@ -60,7 +80,22 @@ html {
body {
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 {
@@ -82,28 +117,35 @@ body {
/* Card-style form fields */
.form-field {
background: var(--color-card);
border: none;
border: 1px solid #e0e0e0;
border-radius: var(--radius-card);
padding: var(--spacing-md) var(--spacing-md);
box-shadow: var(--shadow-card);
width: 100%;
font-family: inherit;
font-size: 0.95rem;
font-weight: 400;
color: var(--color-text);
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 {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.18);
border-color: var(--color-glass-border-hover);
}
.form-field::placeholder {
color: #999;
color: var(--color-text-muted);
font-weight: 400;
}
.form-field.glass::placeholder {
color: var(--color-text-muted);
}
textarea.form-field {
resize: vertical;
min-height: 5rem;
@@ -128,22 +170,29 @@ textarea.form-field {
display: block;
width: 100%;
padding: var(--spacing-md) var(--spacing-lg);
background: var(--color-accent);
background: var(--color-card);
color: var(--color-text);
border: none;
border: 1px solid #e0e0e0;
border-radius: var(--radius-button);
font-family: inherit;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
box-shadow: var(--shadow-button);
transition: opacity 0.2s ease, transform 0.1s ease;
transition: border-color 0.2s ease, transform 0.1s ease;
text-align: center;
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 {
opacity: 0.92;
border-color: var(--color-glass-border-hover);
}
.btn-primary:active {
@@ -176,6 +225,68 @@ textarea.form-field {
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 */
.text-center {
text-align: center;
@@ -197,7 +308,7 @@ textarea.form-field {
.sheet-title {
font-size: 1.2rem;
font-weight: 700;
color: var(--color-text);
color: var(--color-text-on-gradient);
}
.rsvp-form {
@@ -209,7 +320,7 @@ textarea.form-field {
.rsvp-form__label {
font-size: 0.85rem;
font-weight: 700;
color: var(--color-text);
color: var(--color-text-on-gradient);
padding-left: 0.25rem;
}
+3 -3
View File
@@ -28,7 +28,7 @@ defineProps<{
.attendee-list__heading {
font-size: 0.75rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.5);
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
@@ -44,7 +44,7 @@ defineProps<{
.attendee-list__item {
font-size: 0.95rem;
color: rgba(255, 255, 255, 0.85);
color: var(--color-text-soft);
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
@@ -53,7 +53,7 @@ defineProps<{
.attendee-list__empty {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.5);
color: var(--color-text-muted);
font-style: italic;
}
</style>
+7 -3
View File
@@ -45,7 +45,7 @@ watch(
.sheet-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
background: var(--color-glass-overlay);
display: flex;
align-items: flex-end;
justify-content: center;
@@ -53,7 +53,11 @@ watch(
}
.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;
padding: var(--spacing-lg) var(--spacing-xl) var(--spacing-2xl);
width: 100%;
@@ -67,7 +71,7 @@ watch(
.sheet__handle {
width: 36px;
height: 4px;
background: #ccc;
background: var(--color-glass-border-hover);
border-radius: 2px;
align-self: center;
flex-shrink: 0;
+11 -7
View File
@@ -75,7 +75,7 @@ watch(
.confirm-dialog__overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
background: var(--color-glass-overlay);
display: flex;
align-items: center;
justify-content: center;
@@ -84,9 +84,12 @@ watch(
}
.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);
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);
max-width: 320px;
width: 100%;
@@ -98,13 +101,13 @@ watch(
.confirm-dialog__title {
font-size: 1.05rem;
font-weight: 700;
color: var(--color-text);
color: var(--color-text-on-gradient);
}
.confirm-dialog__message {
font-size: 0.9rem;
font-weight: 400;
color: #666;
color: var(--color-text-soft);
}
.confirm-dialog__actions {
@@ -130,8 +133,9 @@ watch(
}
.confirm-dialog__btn--cancel {
background: #e8e8e8;
color: #555;
background: var(--color-glass);
border: 1px solid var(--color-glass-border);
color: var(--color-text-on-gradient);
}
.confirm-dialog__btn--confirm {
+16 -7
View File
@@ -1,6 +1,8 @@
<template>
<RouterLink to="/create" class="fab" aria-label="Create event">
<span class="fab__icon" aria-hidden="true">+</span>
<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>
</RouterLink>
</template>
@@ -16,20 +18,26 @@ import { RouterLink } from 'vue-router'
width: 56px;
height: 56px;
border-radius: 50%;
background: var(--color-accent);
color: #fff;
color: var(--color-text-on-gradient);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
text-decoration: none;
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 {
transform: scale(1.08);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
}
.fab:active {
@@ -41,6 +49,7 @@ import { RouterLink } from 'vue-router'
outline-offset: 3px;
}
.fab__icon {
font-size: 1.8rem;
font-weight: 300;
+1 -1
View File
@@ -12,7 +12,7 @@ defineProps<{
.date-subheader {
font-size: 0.85rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.85);
color: var(--color-text-soft);
margin: 0;
padding: var(--spacing-xs) 0;
}
+32 -1
View File
@@ -1,7 +1,9 @@
<template>
<div class="empty-state">
<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>
</template>
@@ -27,5 +29,34 @@ import { RouterLink } from 'vue-router'
.empty-state__cta {
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>
+8 -9
View File
@@ -1,6 +1,6 @@
<template>
<div
class="event-card"
class="event-card glass"
:class="{ 'event-card--past': isPast, 'event-card--swiping': isSwiping }"
:style="swipeStyle"
@touchstart="onTouchStart"
@@ -93,11 +93,10 @@ function onTouchEnd() {
.event-card {
display: flex;
align-items: center;
background: var(--color-card);
border-radius: var(--radius-card);
box-shadow: var(--shadow-card);
padding: var(--spacing-md) var(--spacing-lg);
gap: var(--spacing-sm);
transition: background 0.2s ease, border-color 0.2s ease;
}
.event-card--past {
@@ -122,7 +121,7 @@ function onTouchEnd() {
.event-card__title {
font-size: 0.95rem;
font-weight: 600;
color: var(--color-text);
color: var(--color-text-on-gradient);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -131,7 +130,7 @@ function onTouchEnd() {
.event-card__time {
font-size: 0.8rem;
font-weight: 400;
color: #888;
color: var(--color-text-secondary);
}
.event-card__badge {
@@ -145,12 +144,12 @@ function onTouchEnd() {
.event-card__badge--organizer {
background: var(--color-accent);
color: #fff;
color: var(--color-text-on-gradient);
}
.event-card__badge--attendee {
background: #e0e0e0;
color: #555;
background: var(--color-glass-strong);
color: var(--color-text-bright);
}
.event-card__delete {
@@ -163,7 +162,7 @@ function onTouchEnd() {
background: none;
border: none;
font-size: 1.2rem;
color: #bbb;
color: var(--color-text-muted);
cursor: pointer;
border-radius: 50%;
transition: color 0.15s ease, background 0.15s ease;
+34 -5
View File
@@ -8,9 +8,11 @@
</div>
<!-- CTA state: no RSVP yet -->
<button v-else class="btn-primary rsvp-bar__cta" type="button" @click="$emit('open')">
I'm attending
</button>
<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
</button>
</div>
</div>
</div>
</template>
@@ -45,6 +47,30 @@ defineEmits<{
.rsvp-bar__cta {
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 {
@@ -52,13 +78,16 @@ defineEmits<{
align-items: center;
justify-content: center;
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);
padding: var(--spacing-md) var(--spacing-lg);
box-shadow: var(--shadow-card);
font-weight: 600;
font-size: 0.95rem;
color: var(--color-text);
color: var(--color-text-on-gradient);
}
.rsvp-bar__check {
+1 -1
View File
@@ -15,7 +15,7 @@ defineProps<{
.section-header {
font-size: 1rem;
font-weight: 700;
color: var(--color-text);
color: var(--color-text-on-gradient);
margin: 0;
padding: var(--spacing-sm) 0;
}
@@ -6,7 +6,7 @@ describe('RsvpBar', () => {
it('renders CTA button when hasRsvp is false', () => {
const wrapper = mount(RsvpBar)
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)
})
@@ -19,7 +19,7 @@ describe('RsvpBar', () => {
it('emits open when CTA button is clicked', async () => {
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)
})
+6 -6
View File
@@ -12,7 +12,7 @@
id="title"
v-model="form.title"
type="text"
class="form-field"
class="form-field glass"
required
maxlength="200"
placeholder="What's the event?"
@@ -27,7 +27,7 @@
<textarea
id="description"
v-model="form.description"
class="form-field"
class="form-field glass"
maxlength="2000"
placeholder="Tell people more about it…"
:aria-invalid="!!errors.description"
@@ -42,7 +42,7 @@
id="dateTime"
v-model="form.dateTime"
type="datetime-local"
class="form-field"
class="form-field glass"
required
:aria-invalid="!!errors.dateTime"
:aria-describedby="errors.dateTime ? 'dateTime-error' : undefined"
@@ -56,7 +56,7 @@
id="location"
v-model="form.location"
type="text"
class="form-field"
class="form-field glass"
maxlength="500"
placeholder="Where is it?"
:aria-invalid="!!errors.location"
@@ -71,7 +71,7 @@
id="expiryDate"
v-model="form.expiryDate"
type="date"
class="form-field"
class="form-field glass"
required
:min="tomorrow"
:aria-invalid="!!errors.expiryDate"
@@ -80,7 +80,7 @@
<span v-if="errors.expiryDate" id="expiryDate-error" class="field-error" role="alert">{{ errors.expiryDate }}</span>
</div>
<button type="submit" class="btn-primary" :disabled="submitting">
<button type="submit" class="btn-primary glass" :disabled="submitting">
{{ submitting ? 'Creating…' : 'Create Event' }}
</button>
+57 -19
View File
@@ -33,21 +33,21 @@
<dl class="detail__meta">
<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>
</dt>
<dd class="detail__meta-text">{{ formattedDateTime }}</dd>
</div>
<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>
</dt>
<dd class="detail__meta-text">{{ event.location }}</dd>
</div>
<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>
</dt>
<dd class="detail__meta-text">{{ event.attendeeCount }} going</dd>
@@ -70,7 +70,7 @@
<!-- Error state -->
<div v-else-if="state === 'error'" class="detail__content detail__content--center" role="alert">
<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>
@@ -90,7 +90,7 @@
<input
id="rsvp-name"
v-model.trim="nameInput"
class="form-field"
class="form-field glass"
type="text"
placeholder="e.g. Max Mustermann"
maxlength="100"
@@ -99,9 +99,11 @@
/>
<span v-if="nameError" class="rsvp-form__field-error" role="alert">{{ nameError }}</span>
</div>
<button class="btn-primary" type="submit" :disabled="submitting">
{{ submitting ? 'Sending…' : "Count me in" }}
</button>
<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" }}
</button>
</div>
<p v-if="submitError" class="rsvp-form__field-error rsvp-form__error" role="alert">{{ submitError }}</p>
</form>
</BottomSheet>
@@ -268,15 +270,19 @@ onMounted(fetchEvent)
.detail__hero {
position: relative;
width: 100%;
height: 260px;
overflow: hidden;
height: 420px;
overflow: visible;
flex-shrink: 0;
}
.detail__hero-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
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 {
@@ -284,9 +290,8 @@ onMounted(fetchEvent)
inset: 0;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.4) 0%,
transparent 50%,
var(--color-gradient-start) 100%
var(--color-glass-overlay) 0%,
transparent 50%
);
}
@@ -366,7 +371,6 @@ onMounted(fetchEvent)
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.15);
border-radius: 10px;
color: var(--color-text-on-gradient);
}
@@ -387,14 +391,14 @@ onMounted(fetchEvent)
.detail__section-title {
font-size: 0.75rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.5);
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.detail__description {
font-size: 0.95rem;
color: rgba(255, 255, 255, 0.85);
color: var(--color-text-soft);
line-height: 1.6;
word-break: break-word;
}
@@ -409,8 +413,8 @@ onMounted(fetchEvent)
}
.detail__banner--expired {
background: rgba(255, 255, 255, 0.12);
color: rgba(255, 255, 255, 0.8);
background: var(--color-glass);
color: var(--color-text-soft);
backdrop-filter: blur(4px);
}
@@ -423,7 +427,7 @@ onMounted(fetchEvent)
/* Skeleton shimmer on gradient */
.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%;
}
@@ -442,4 +446,38 @@ onMounted(fetchEvent)
.skeleton--short {
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>
@@ -262,7 +262,7 @@ describe('EventDetailView', () => {
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()
expect(document.body.querySelector('[role="dialog"]')).not.toBeNull()
@@ -275,7 +275,7 @@ describe('EventDetailView', () => {
const wrapper = await mountWithToken()
await flushPromises()
await wrapper.find('.rsvp-bar__cta').trigger('click')
await wrapper.find('.rsvp-bar__cta-inner').trigger('click')
await flushPromises()
// Form is inside Teleport — find via document.body
@@ -300,7 +300,7 @@ describe('EventDetailView', () => {
await flushPromises()
// Open sheet
await wrapper.find('.rsvp-bar__cta').trigger('click')
await wrapper.find('.rsvp-bar__cta-inner').trigger('click')
await flushPromises()
// Fill name via Teleported input
@@ -386,7 +386,7 @@ describe('EventDetailView', () => {
const wrapper = await mountWithToken()
await flushPromises()
await wrapper.find('.rsvp-bar__cta').trigger('click')
await wrapper.find('.rsvp-bar__cta-inner').trigger('click')
await flushPromises()
const input = document.body.querySelector('#rsvp-name')! as HTMLInputElement