From 9483e9b1f7883bf23e2a34a297138b5d0effece5 Mon Sep 17 00:00:00 2001 From: nitrix Date: Fri, 13 Mar 2026 21:40:12 +0100 Subject: [PATCH] Extract shared bar component CSS and add calendar button to RsvpBar Co-Authored-By: Claude Opus 4.6 --- frontend/src/assets/main.css | 66 +++++++++ frontend/src/components/RsvpBar.vue | 138 +++++++++--------- .../src/components/__tests__/RsvpBar.spec.ts | 10 +- 3 files changed, 140 insertions(+), 74 deletions(-) diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index bdb327e..a20082e 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -317,6 +317,72 @@ input[type="datetime-local"].form-field.glass::-webkit-datetime-edit-fields-wrap to { --glow-angle: 360deg; } } +/* ── Fixed Bottom Bar Components ── */ + +/* CTA wrapper (text button, e.g. "I'm attending!", "Post an update") */ +.bar-cta { + flex: 1; + min-width: 0; + border-radius: var(--radius-button); + transition: transform 0.1s ease; +} + +.bar-cta:hover { + transform: scale(1.02); +} + +.bar-cta:active { + transform: scale(0.98); +} + +.bar-cta-btn { + 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; +} + + +/* Icon wrapper (e.g. calendar, bookmark buttons) */ +.bar-icon { + flex-shrink: 0; + border-radius: var(--radius-button); + transition: transform 0.1s ease; +} + +.bar-icon:hover { + transform: scale(1.02); +} + +.bar-icon:active { + transform: scale(0.98); +} + +.bar-icon-btn { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding: var(--spacing-md); + border-radius: calc(var(--radius-button) - 2px); + border: none; + cursor: pointer; + color: var(--color-text-on-gradient); + line-height: 0; +} + +.bar-icon-btn svg { + display: block; +} + /* Utility */ .text-center { text-align: center; diff --git a/frontend/src/components/RsvpBar.vue b/frontend/src/components/RsvpBar.vue index 71e2101..acd3eeb 100644 --- a/frontend/src/components/RsvpBar.vue +++ b/frontend/src/components/RsvpBar.vue @@ -3,20 +3,30 @@
-
- - You're attending! - +
+
+ + You're attending! + +
+
-
-
+
+
+ +
+
+ +
@@ -64,6 +84,7 @@ defineEmits<{ open: [] cancel: [] bookmark: [] + calendar: [] }>() const expanded = ref(false) @@ -111,34 +132,6 @@ watch(expanded, (isExpanded) => { gap: var(--spacing-sm); } -.rsvp-bar__cta { - flex: 1; - min-width: 0; - 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-wrapper { display: flex; @@ -146,7 +139,14 @@ watch(expanded, (isExpanded) => { gap: var(--spacing-xs); } +.rsvp-bar__status-row { + display: flex; + gap: var(--spacing-sm); +} + .rsvp-bar__status { + flex: 1; + min-width: 0; display: flex; align-items: center; justify-content: center; @@ -225,35 +225,35 @@ watch(expanded, (isExpanded) => { transform: translateY(-4px); } -.rsvp-bar__bookmark { + +/* Calendar button — glassmorphic variant (post-RSVP status row) */ +.rsvp-bar__calendar-glass { flex-shrink: 0; - border-radius: var(--radius-button); - transition: transform 0.1s ease; -} - -.rsvp-bar__bookmark:hover { - transform: scale(1.02); -} - -.rsvp-bar__bookmark:active { - transform: scale(0.98); -} - -.rsvp-bar__bookmark-inner { display: flex; align-items: center; justify-content: center; - width: 100%; - height: 100%; padding: var(--spacing-md); - border-radius: calc(var(--radius-button) - 2px); - border: none; - cursor: pointer; + 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); + border-radius: var(--radius-card); + box-shadow: var(--shadow-card); color: var(--color-text-on-gradient); + cursor: pointer; line-height: 0; + transition: transform 0.1s ease, background 0.15s ease; } -.rsvp-bar__bookmark-inner svg { +.rsvp-bar__calendar-glass:hover { + transform: scale(1.02); + background: linear-gradient(135deg, var(--color-glass-hover) 0%, var(--color-glass-strong) 100%); +} + +.rsvp-bar__calendar-glass:active { + transform: scale(0.98); +} + +.rsvp-bar__calendar-glass svg { display: block; } diff --git a/frontend/src/components/__tests__/RsvpBar.spec.ts b/frontend/src/components/__tests__/RsvpBar.spec.ts index 34bb30b..3333ba6 100644 --- a/frontend/src/components/__tests__/RsvpBar.spec.ts +++ b/frontend/src/components/__tests__/RsvpBar.spec.ts @@ -5,8 +5,8 @@ import RsvpBar from '../RsvpBar.vue' 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-inner').text()).toBe("I'm attending!") + expect(wrapper.find('.bar-cta').exists()).toBe(true) + expect(wrapper.find('.bar-cta-btn').text()).toBe("I'm attending!") expect(wrapper.find('.rsvp-bar__status').exists()).toBe(false) }) @@ -14,17 +14,17 @@ describe('RsvpBar', () => { const wrapper = mount(RsvpBar, { props: { hasRsvp: true } }) expect(wrapper.find('.rsvp-bar__status').exists()).toBe(true) expect(wrapper.find('.rsvp-bar__text').text()).toBe("You're attending!") - expect(wrapper.find('.rsvp-bar__cta').exists()).toBe(false) + expect(wrapper.find('.bar-cta').exists()).toBe(false) }) it('emits open when CTA button is clicked', async () => { const wrapper = mount(RsvpBar) - await wrapper.find('.rsvp-bar__cta-inner').trigger('click') + await wrapper.find('.bar-cta-btn').trigger('click') expect(wrapper.emitted('open')).toHaveLength(1) }) it('does not render CTA button when hasRsvp is true', () => { const wrapper = mount(RsvpBar, { props: { hasRsvp: true } }) - expect(wrapper.find('button').exists()).toBe(false) + expect(wrapper.find('.bar-cta-btn').exists()).toBe(false) }) })