refactor(webapp): vue3 migration - button - icon + circle + loading (#9208)
@ -2,6 +2,6 @@ import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
||||
|
||||
defineStep('I open the content menu of post {string}', (title) => {
|
||||
cy.contains('.post-teaser', title)
|
||||
.find('.content-menu .base-button')
|
||||
.find('[data-test="content-menu-button"]')
|
||||
.click()
|
||||
})
|
||||
|
||||
@ -2,7 +2,7 @@ import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
||||
|
||||
defineStep('I click on "Report Post" from the content menu of the post', () => {
|
||||
cy.contains('.base-card', 'The Truth about the Holocaust')
|
||||
.find('.content-menu .base-button')
|
||||
.find('[data-test="content-menu-button"]')
|
||||
.click()
|
||||
|
||||
cy.get('.popover .ds-menu-item-link')
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
||||
|
||||
defineStep('I should see the {string} button', button => {
|
||||
cy.get('.base-card .action-buttons .base-button')
|
||||
cy.get('.base-card .action-buttons button')
|
||||
.should('contain', button)
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
||||
|
||||
defineStep('I {string} see {string} from the content menu in the user info box', (condition, link) => {
|
||||
cy.get('.user-content-menu .base-button').click()
|
||||
cy.get('.user-content-menu [data-test="content-menu-button"]').click()
|
||||
cy.get('.popover .ds-menu-item-link')
|
||||
.should(condition === 'should' ? 'contain' : 'not.contain', link)
|
||||
})
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
||||
|
||||
defineStep('I delete a social media link', () => {
|
||||
cy.get(".base-button[title='Delete']")
|
||||
cy.get("button[title='Delete']")
|
||||
.click()
|
||||
})
|
||||
|
||||
@ -2,7 +2,7 @@ import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
||||
|
||||
defineStep('I click on {string} from the content menu in the user info box',
|
||||
button => {
|
||||
cy.get('.user-content-menu .base-button').click()
|
||||
cy.get('.user-content-menu [data-test="content-menu-button"]').click()
|
||||
cy.get('.popover .ds-menu-item-link')
|
||||
.contains(button)
|
||||
.click({
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
@import "../src/styles/animations.css";
|
||||
|
||||
/* Watercolor theme for Storybook - vibrant colors inspired by aquarelle paints */
|
||||
/* All colors meet WCAG AA contrast requirements (4.5:1 for normal text) */
|
||||
|
||||
@ -10,9 +10,9 @@
|
||||
### Übersicht
|
||||
```
|
||||
Phase 0: Analyse ██████████ 100% (8/8 Schritte)
|
||||
Phase 3: Migration ████░░░░░░ 36% (32/90 Buttons)
|
||||
Phase 3: Migration ██████████ 100% (132/132 Buttons) ✅
|
||||
───────────────────────────────────────────
|
||||
Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
||||
Phase 3 ABGESCHLOSSEN: M4a ✅, M4b ✅, M4c ✅ — 0 <base-button> verbleibend
|
||||
```
|
||||
|
||||
### Statistiken
|
||||
@ -25,108 +25,30 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
||||
| Duplikate gefunden | 5 direkte + 3 Familien |
|
||||
| Zur Migration priorisiert | 15 Kern-Komponenten |
|
||||
|
||||
### OsButton Migration (Phase 3)
|
||||
### OsButton Migration (Phase 3) ✅
|
||||
| Status | Anzahl | Details |
|
||||
|--------|--------|---------|
|
||||
| ✅ Migriert | 32 | Erste Welle (16) + Milestone 4a (14) + NotificationMenu (2) |
|
||||
| ⏳ Ausstehend (mit neuen Props) | ~60 | Milestone 4c (benötigen icon/circle/loading) |
|
||||
| **Gesamt** | **~90** | In ~50 Dateien |
|
||||
| ✅ Migriert | 132 | 78 Dateien, alle `<base-button>` ersetzt |
|
||||
| ⬜ Verbleibend | 0 | Nur BaseButton.vue Definition + Test/Snapshot-Dateien |
|
||||
| **Gesamt** | **132** | **100% erledigt** |
|
||||
|
||||
**Migrierte Komponenten (32):**
|
||||
**Alle 132 Buttons migriert in 78 Dateien ✅**
|
||||
|
||||
*Erste Welle (16):*
|
||||
- UserTeaserPopover.vue (1 Button)
|
||||
- GroupForm.vue (1 Button - Cancel)
|
||||
- EmbedComponent.vue (2 Buttons - Cancel, Play Now)
|
||||
- DonationInfo.vue (1 Button)
|
||||
- CommentCard.vue (1 Button - Show More)
|
||||
- MapStylesButtons.vue (1 Button)
|
||||
- GroupMember.vue (1 Button)
|
||||
- embeds.vue (2 Buttons)
|
||||
- notifications.vue (3 Buttons)
|
||||
- privacy.vue (1 Button)
|
||||
- terms-and-conditions-confirm.vue (2 Buttons)
|
||||
Migration vollständig abgeschlossen. 0 `<base-button>` Tags verbleiben in Vue-Templates.
|
||||
|
||||
*Milestone 4a (14) ✅:*
|
||||
- ✅ DisableModal.vue (1 Button - Cancel)
|
||||
- ✅ DeleteUserModal.vue (1 Button - Cancel)
|
||||
- ✅ ReleaseModal.vue (1 Button - Cancel)
|
||||
- ✅ ContributionForm.vue (1 Button - Cancel)
|
||||
- ✅ EnterNonce.vue (1 Button - Submit)
|
||||
- ✅ MySomethingList.vue (1 Button - Cancel)
|
||||
- ✅ ImageUploader.vue (2 Buttons - Crop)
|
||||
- ✅ admin/donations.vue (1 Button - Save)
|
||||
- ✅ profile/_id/_slug.vue (2 Buttons - Unblock, Unmute)
|
||||
- ✅ settings/badges.vue (1 Button - Remove)
|
||||
- ✅ notifications/index.vue (1 Button - Mark All Read)
|
||||
- ✅ ReportRow.vue (1 Button - More Details)
|
||||
**Erkenntnisse aus der Migration:**
|
||||
- `type="submit"` muss explizit gesetzt werden (OsButton Default: `type="button"`)
|
||||
- DsForm `errors` ist ein Objekt → `!!errors` für Boolean-Cast bei `:disabled`
|
||||
- CSS `.base-button` Selektoren → `> button` oder `button`
|
||||
- Position/Dimensions brauchen `!important` für Tailwind-Override
|
||||
- Filter-Buttons nutzen `:appearance="condition ? 'filled' : 'outline'"` Pattern
|
||||
- Circle-Buttons mit Icon: `<template #icon><base-icon :name="..." /></template>`
|
||||
|
||||
*Sonstige (2):*
|
||||
- ✅ NotificationMenu.vue (2 Buttons - Mark All Read, Notification Page)
|
||||
|
||||
**Ausstehend - benötigen neue Props (~60):**
|
||||
|
||||
*Button-Komponenten mit icon/circle/loading:*
|
||||
- ActionButton.vue: 3 Buttons (icon, circle)
|
||||
- LabeledButton.vue: 1 Button (icon)
|
||||
- MenuBarButton.vue: 1 Button (icon)
|
||||
- EmotionButton.vue: 1 Button (icon)
|
||||
- ShoutButton.vue: 1 Button (icon)
|
||||
- FollowButton.vue: 1 Button (icon, loading)
|
||||
- JoinLeaveButton.vue: 1 Button (icon, loading)
|
||||
- ObserveButton.vue: 1 Button (icon, loading)
|
||||
- InviteButton.vue: 1 Button (icon, loading)
|
||||
- MapButton.vue: 1 Button (icon)
|
||||
- PaginationButtons.vue: 2 Buttons (icon, circle)
|
||||
|
||||
*Navigation mit icon:*
|
||||
- LocaleSwitch.vue: 1 Button (icon)
|
||||
- HeaderMenu.vue: 2 Buttons (icon)
|
||||
- AvatarMenu.vue: 1 Button (icon, circle)
|
||||
- NotificationMenu.vue: 1 Button (icon, circle)
|
||||
- ChatNotificationMenu.vue: 1 Button (icon, circle)
|
||||
- FilterMenu.vue: 1 Button (icon)
|
||||
|
||||
*Editor-Buttons:*
|
||||
- Editor.vue: ~10 Toolbar-Buttons (icon)
|
||||
- ContextMenu.vue: 3 Buttons (icon)
|
||||
- LinkInput.vue: 2 Buttons (icon, circle)
|
||||
|
||||
*Filter-Buttons:*
|
||||
- CategoriesFilter.vue: 1 Button (icon)
|
||||
- HashtagsFilter.vue: 1 Button (icon)
|
||||
- DropdownFilter.vue: 1 Button (icon)
|
||||
- FilterMenuSection.vue: 2 Buttons (icon)
|
||||
- DateTimeRange.vue: 2 Buttons (icon)
|
||||
|
||||
*Chat-Buttons:*
|
||||
- Chat.vue: 2 Buttons (icon)
|
||||
- AddChatRoomByUserSearch.vue: 1 Button (icon, circle)
|
||||
|
||||
*Form-Buttons:*
|
||||
- CommentForm.vue: 1 Button (icon, loading)
|
||||
- SearchField.vue: 1 Button (icon, circle)
|
||||
- ShowPassword.vue: 1 Button (icon)
|
||||
|
||||
*Modal-Buttons:*
|
||||
- ConfirmModal.vue: 1 Button (loading)
|
||||
- ReportModal.vue: 1 Button (loading)
|
||||
|
||||
*Feature-Buttons:*
|
||||
- CreateInvitation.vue: 1 Button (icon)
|
||||
- Invitation.vue: 1 Button (icon, circle)
|
||||
- SocialMediaListItem.vue: 1 Button (icon, circle)
|
||||
- Request.vue: 1 Button (icon, loading)
|
||||
- GroupButton.vue: 1 Button (icon)
|
||||
- CtaJoinLeaveGroup.vue: 1 Button (icon)
|
||||
- data-download.vue: 1 Button (icon, loading)
|
||||
- AddGroupMember.vue: 1 Button (icon)
|
||||
- GroupForm.vue Submit: 1 Button (icon)
|
||||
|
||||
*Page-Buttons:*
|
||||
- CommentCard.vue Reply: 1 Button (icon, circle)
|
||||
- EmbedComponent.vue Close: 1 Button (icon, circle)
|
||||
- PostTeaser.vue: 2 Buttons (icon)
|
||||
**Verbleibende Cleanup-Aufgaben:**
|
||||
- [ ] Snapshot-Dateien aktualisieren (enthalten noch `base-button` Referenzen)
|
||||
- [ ] Test-Dateien aktualisieren (Selektoren `.base-button` → `button` oder `os-button-stub`)
|
||||
- [ ] BaseButton.vue Komponente ggf. entfernen (wenn nicht mehr referenziert)
|
||||
- [ ] CSS-Selektor `.base-button` in ImageUploader.vue entfernen
|
||||
|
||||
---
|
||||
|
||||
@ -154,7 +76,7 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
||||
### Data Input
|
||||
| # | Komponente | Status | Webapp-Duplikat | Varianten | Priorität | Notizen |
|
||||
|---|------------|--------|-----------------|-----------|-----------|---------|
|
||||
| 13 | Button | ⏳ Migration | BaseButton, CustomButton, ActionButton, ... | | | → OsButton (16/90 migriert) |
|
||||
| 13 | Button | ⏳ Migration | BaseButton, CustomButton, ActionButton, ... | | | → OsButton (41/90 migriert) |
|
||||
| 14 | CopyField | ⬜ Ausstehend | | | | |
|
||||
| 15 | Form | ⬜ Ausstehend | | | | |
|
||||
| 16 | FormItem | ⬜ Ausstehend | | | | |
|
||||
@ -219,7 +141,7 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
||||
| 7 | BadgeSelection | ⬜ Ausstehend | Input | | |
|
||||
| 8 | Badges | ⬜ Ausstehend | Display | | |
|
||||
| 9 | BadgesSection | ⬜ Ausstehend | Display | | |
|
||||
| 10 | BaseButton | ⏳ Migration | Button | Button | 🔄 → OsButton (16/90 migriert) |
|
||||
| 10 | BaseButton | ⏳ Migration | Button | Button | 🔄 → OsButton (41/90 migriert) |
|
||||
| 11 | BaseCard | ⬜ Ausstehend | Layout | Card | 🔗 DUPLIKAT |
|
||||
| 12 | BaseIcon | ⬜ Ausstehend | Display | Icon | 🔗 DUPLIKAT |
|
||||
|
||||
@ -228,7 +150,7 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
||||
|---|------------|--------|-----------|-------------------|---------|
|
||||
| 13 | CategoriesFilter | ⬜ Ausstehend | Filter | | |
|
||||
| 14 | CategoriesMenu | ⬜ Ausstehend | Navigation | Menu | |
|
||||
| 15 | CategoriesSelect | ⬜ Ausstehend | Input | Select | |
|
||||
| 15 | CategoriesSelect | ⏳ Teilweise | Input | Select | Buttons → OsButton (icon) |
|
||||
| 16 | ChangePassword | ⬜ Ausstehend | Feature | | Auth-spezifisch |
|
||||
| 17 | Change | ⬜ Ausstehend | Feature | | |
|
||||
| 18 | Chat | ⬜ Ausstehend | Feature | | Chat-spezifisch |
|
||||
@ -241,12 +163,12 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
||||
| 25 | ContentMenu | ⬜ Ausstehend | Navigation | Menu | |
|
||||
| 26 | ContentViewer | ⬜ Ausstehend | Display | | |
|
||||
| 27 | ContextMenu | ⬜ Ausstehend | Navigation | Menu | |
|
||||
| 28 | ContributionForm | ⬜ Ausstehend | Feature | Form | Post-spezifisch |
|
||||
| 28 | ContributionForm | ⏳ Teilweise | Feature | Form | Cancel → OsButton |
|
||||
| 29 | CounterIcon | ⬜ Ausstehend | Display | Icon | |
|
||||
| 30 | CountTo | ⬜ Ausstehend | Display | Number | Animation |
|
||||
| 31 | CreateInvitation | ⬜ Ausstehend | Feature | | |
|
||||
| 32 | CtaJoinLeaveGroup | ⬜ Ausstehend | Button | Button | 🔄 Button-Familie |
|
||||
| 33 | CtaUnblockAuthor | ⬜ Ausstehend | Button | Button | 🔄 Button-Familie |
|
||||
| 33 | CtaUnblockAuthor | ✅ Migriert | Button | Button | Button → OsButton (icon) |
|
||||
| 34 | CustomButton | ⬜ Ausstehend | Button | Button | 🔄 Button-Familie |
|
||||
|
||||
### D-E
|
||||
@ -254,8 +176,8 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
||||
|---|------------|--------|-----------|-------------------|---------|
|
||||
| 35 | DateTimeRange | ⬜ Ausstehend | Input | | |
|
||||
| 36 | DeleteData | ⬜ Ausstehend | Feature | | |
|
||||
| 37 | DeleteUserModal | ⬜ Ausstehend | Feedback | Modal | 🔄 Modal-Familie |
|
||||
| 38 | DisableModal | ⬜ Ausstehend | Feedback | Modal | 🔄 Modal-Familie |
|
||||
| 37 | DeleteUserModal | ⏳ Teilweise | Feedback | Modal | 🔄 Modal-Familie, Buttons → OsButton |
|
||||
| 38 | DisableModal | ⏳ Teilweise | Feedback | Modal | 🔄 Modal-Familie, Buttons → OsButton |
|
||||
| 39 | DonationInfo | ✅ Migriert | Display | | Button → OsButton |
|
||||
| 40 | Dropdown | ⬜ Ausstehend | Input | Select | |
|
||||
| 41 | DropdownFilter | ⬜ Ausstehend | Filter | Select | |
|
||||
@ -265,7 +187,7 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
||||
| 45 | EmotionButton | ⬜ Ausstehend | Button | Button | |
|
||||
| 46 | Emotions | ⬜ Ausstehend | Feature | | |
|
||||
| 47 | Empty | ⬜ Ausstehend | Feedback | Placeholder | |
|
||||
| 48 | EnterNonce | ⬜ Ausstehend | Feature | | Auth |
|
||||
| 48 | EnterNonce | ⏳ Teilweise | Feature | | Auth, Submit → OsButton |
|
||||
|
||||
### F-G
|
||||
| # | Komponente | Status | Kategorie | Styleguide-Pendant | Notizen |
|
||||
@ -293,7 +215,7 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
||||
| 65 | HashtagsFilter | ⬜ Ausstehend | Filter | | |
|
||||
| 66 | HeaderButton | ⬜ Ausstehend | Button | Button | 🔄 Button-Familie |
|
||||
| 67 | HeaderMenu | ⬜ Ausstehend | Navigation | Menu | |
|
||||
| 68 | ImageUploader | ⬜ Ausstehend | Input | | |
|
||||
| 68 | ImageUploader | ⏳ Teilweise | Input | | Crop-Buttons → OsButton |
|
||||
| 69 | index | ⬜ Ausstehend | ? | | Prüfen |
|
||||
| 70 | InternalPage | ⬜ Ausstehend | Layout | Page | |
|
||||
| 71 | Invitation | ⬜ Ausstehend | Feature | | |
|
||||
@ -305,7 +227,7 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
||||
| 77 | LoadingSpinner | ⬜ Ausstehend | Feedback | Spinner | 🔗 DUPLIKAT |
|
||||
| 78 | LocaleSwitch | ⬜ Ausstehend | Navigation | | |
|
||||
| 79 | LocationInfo | ⬜ Ausstehend | Display | | |
|
||||
| 80 | LocationSelect | ⬜ Ausstehend | Input | Select | |
|
||||
| 80 | LocationSelect | ⏳ Teilweise | Input | Select | Close-Button → OsButton (icon) |
|
||||
| 81 | LocationTeaser | ⬜ Ausstehend | Display | Card | |
|
||||
| 82 | LoginButton | ⬜ Ausstehend | Button | Button | |
|
||||
| 83 | LoginForm | ⬜ Ausstehend | Feature | Form | Auth |
|
||||
@ -322,8 +244,8 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
||||
| 90 | MenuBarButton | ⬜ Ausstehend | Button | Button | 🔄 Button-Familie |
|
||||
| 91 | MenuLegend | ⬜ Ausstehend | Navigation | | |
|
||||
| 92 | Modal | ⬜ Ausstehend | Feedback | Modal | 🔗 DUPLIKAT |
|
||||
| 93 | MySomethingList | ⬜ Ausstehend | Display | List | |
|
||||
| 94 | NotificationMenu | ⬜ Ausstehend | Navigation | Menu | |
|
||||
| 93 | MySomethingList | ⏳ Teilweise | Display | List | Cancel → OsButton |
|
||||
| 94 | NotificationMenu | ⏳ Teilweise | Navigation | Menu | 2/3 Buttons → OsButton |
|
||||
| 95 | NotificationsTable | ⬜ Ausstehend | Display | Table | |
|
||||
| 96 | ObserveButton | ⬜ Ausstehend | Button | Button | |
|
||||
| 97 | OrderByFilter | ⬜ Ausstehend | Filter | | |
|
||||
@ -333,7 +255,7 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
||||
|---|------------|--------|-----------|-------------------|---------|
|
||||
| 98 | PageFooter | ⬜ Ausstehend | Layout | | |
|
||||
| 99 | PageParamsLink | ⬜ Ausstehend | Navigation | | |
|
||||
| 100 | PaginationButtons | ⬜ Ausstehend | Navigation | | |
|
||||
| 100 | PaginationButtons | ✅ Migriert | Navigation | | 2 Buttons → OsButton (icon, circle) |
|
||||
| 101 | PostTeaser | ⬜ Ausstehend | Display | Card | |
|
||||
| 102 | PostTypeFilter | ⬜ Ausstehend | Filter | | |
|
||||
| 103 | ProfileAvatar | ⬜ Ausstehend | Display | Avatar | |
|
||||
@ -345,10 +267,10 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
||||
| 109 | RegistrationSlideNonce | ⬜ Ausstehend | Feature | | Auth |
|
||||
| 110 | RegistrationSlideNoPublic | ⬜ Ausstehend | Feature | | Auth |
|
||||
| 111 | RegistrationSlider | ⬜ Ausstehend | Feature | | Auth |
|
||||
| 112 | ReleaseModal | ⬜ Ausstehend | Feedback | Modal | 🔄 Modal-Familie |
|
||||
| 112 | ReleaseModal | ⏳ Teilweise | Feedback | Modal | 🔄 Modal-Familie, Cancel → OsButton |
|
||||
| 113 | ReportList | ⬜ Ausstehend | Display | List | |
|
||||
| 114 | ReportModal | ⬜ Ausstehend | Feedback | Modal | 🔄 Modal-Familie |
|
||||
| 115 | ReportRow | ⬜ Ausstehend | Display | | |
|
||||
| 115 | ReportRow | ⏳ Teilweise | Display | | More Details → OsButton |
|
||||
| 116 | ReportsTable | ⬜ Ausstehend | Display | Table | |
|
||||
| 117 | Request | ⬜ Ausstehend | Feature | | |
|
||||
| 118 | ResponsiveImage | ⬜ Ausstehend | Display | | |
|
||||
@ -484,6 +406,9 @@ Diese sollten zuerst migriert werden:
|
||||
| 2026-02-08 | Claude | OsButton erweitert | attrs/listeners Forwarding für Vue 2 ($listeners) |
|
||||
| 2026-02-09 | Claude | Scope erweitert | ~90 Buttons identifiziert (16 migriert, 14 ohne Props, ~60 mit Props) |
|
||||
| 2026-02-09 | Claude | **Milestone 4a: 8 Buttons** | DisableModal, DeleteUserModal, ReleaseModal, ContributionForm, EnterNonce, MySomethingList, ImageUploader (2x) |
|
||||
| 2026-02-09 | Claude | **Milestone 4a abgeschlossen** | 6 weitere: donations, profile (2x), badges, notifications/index, ReportRow |
|
||||
| 2026-02-11 | Claude | **M4b: icon + circle** | icon-Slot implementiert, circle-Prop mit CVA |
|
||||
| 2026-02-11 | Claude | **9 icon-Buttons migriert (M4c)** | DisableModal, DeleteUserModal, CtaUnblockAuthor, LocationSelect, CategoriesSelect, my-email-address, profile Chat, PaginationButtons (2x circle) |
|
||||
|
||||
---
|
||||
|
||||
@ -504,18 +429,18 @@ Diese sollten zuerst migriert werden:
|
||||
10. [x] Webapp-Integration (Jest, Docker, CI)
|
||||
11. [x] 16 Buttons migrieren (validiert ✅)
|
||||
|
||||
**Milestone 4a: 14 Buttons ohne neue Props**
|
||||
12. [ ] Modal Cancel-Buttons (3)
|
||||
13. [ ] Form Cancel/Submit-Buttons (3)
|
||||
14. [ ] ImageUploader Crop-Buttons (2)
|
||||
15. [ ] Page Buttons (6)
|
||||
**Milestone 4a: 14 Buttons ohne neue Props** ✅
|
||||
12. [x] Modal Cancel-Buttons (3)
|
||||
13. [x] Form Cancel/Submit-Buttons (3)
|
||||
14. [x] ImageUploader Crop-Buttons (2)
|
||||
15. [x] Page Buttons (6)
|
||||
|
||||
**Milestone 4b: Props für ~60 Buttons hinzufügen**
|
||||
16. [ ] icon-Prop zu OsButton hinzufügen
|
||||
17. [ ] circle-Variant zu OsButton hinzufügen
|
||||
**Milestone 4b: Props für ~49 Buttons hinzufügen**
|
||||
16. [x] icon-Slot zu OsButton hinzufügen ✅
|
||||
17. [x] circle-Variant zu OsButton hinzufügen ✅
|
||||
18. [ ] loading-Prop zu OsButton hinzufügen
|
||||
|
||||
**Milestone 4c: ~60 Buttons mit neuen Props migrieren**
|
||||
**Milestone 4c: ~49 Buttons mit neuen Props migrieren**
|
||||
19. [ ] Button-Komponenten (~15)
|
||||
20. [ ] Navigation (~8)
|
||||
21. [ ] Editor (~15)
|
||||
@ -1213,7 +1138,7 @@ $box-shadow-small-inset: inset 0 0 0 1px rgba(0,0,0,.05)
|
||||
|
||||
## Phase 3: Webapp-Integration (Tracking)
|
||||
|
||||
### OsButton Migration - Abgeschlossen (16/90)
|
||||
### OsButton Migration - Abgeschlossen (132/132) ✅
|
||||
|
||||
| # | Datei | Button | Status |
|
||||
|---|-------|--------|--------|
|
||||
@ -1234,70 +1159,34 @@ $box-shadow-small-inset: inset 0 0 0 1px rgba(0,0,0,.05)
|
||||
| 15 | terms-and-conditions-confirm.vue | Read T&C | ✅ Migriert |
|
||||
| 16 | terms-and-conditions-confirm.vue | Save | ✅ Migriert |
|
||||
|
||||
### OsButton Migration - Ausstehend ohne neue Props (Milestone 4a: 14/90)
|
||||
### OsButton Migration - Alle Milestones abgeschlossen ✅
|
||||
|
||||
| # | Datei | Button | OsButton Props | Status |
|
||||
|---|-------|--------|----------------|--------|
|
||||
| 17 | Modal/DisableModal.vue | Cancel | `default` | ⬜ Ausstehend |
|
||||
| 18 | Modal/DeleteUserModal.vue | Cancel | `default` | ⬜ Ausstehend |
|
||||
| 19 | Modal/ReleaseModal.vue | Cancel | `default` | ⬜ Ausstehend |
|
||||
| 20 | ContributionForm.vue | Cancel | `:disabled` | ⬜ Ausstehend |
|
||||
| 21 | EnterNonce.vue | Submit | `variant="primary" :disabled` | ⬜ Ausstehend |
|
||||
| 22 | MySomethingList.vue | Cancel | `default` | ⬜ Ausstehend |
|
||||
| 23 | ImageUploader.vue | Crop Confirm 1 | `variant="primary"` | ⬜ Ausstehend |
|
||||
| 24 | ImageUploader.vue | Crop Confirm 2 | `variant="primary"` | ⬜ Ausstehend |
|
||||
| 25 | admin/donations.vue | Save | `variant="primary"` | ⬜ Ausstehend |
|
||||
| 26 | profile/_id/_slug.vue | Unblock | `default` | ⬜ Ausstehend |
|
||||
| 27 | profile/_id/_slug.vue | Unmute | `default` | ⬜ Ausstehend |
|
||||
| 28 | settings/badges.vue | Remove | `default` | ⬜ Ausstehend |
|
||||
| 29 | notifications/index.vue | Mark All Read | `variant="primary" :disabled` | ⬜ Ausstehend |
|
||||
| 30 | ReportRow.vue | More Details | `size="sm"` | ⬜ Ausstehend |
|
||||
| Milestone | Status | Details |
|
||||
|-----------|--------|---------|
|
||||
| M4a: Buttons ohne neue Props | ✅ | 14 Buttons (Modals, Forms, Pages) |
|
||||
| M4b: Props implementieren | ✅ | icon (Slot), circle, loading |
|
||||
| M4c: Buttons mit icon/circle/loading | ✅ | Alle verbleibenden Buttons migriert |
|
||||
|
||||
### OsButton Migration - Ausstehend mit neuen Props (Milestone 4c: ~60/90)
|
||||
### OsButton Features - Alle implementiert ✅
|
||||
|
||||
> Diese Buttons benötigen icon, circle, und/oder loading Props.
|
||||
> Siehe "Ausstehend - benötigen neue Props (~60)" oben für vollständige Liste.
|
||||
|
||||
**Kategorien:**
|
||||
| Kategorie | Anzahl | Props benötigt |
|
||||
|-----------|--------|----------------|
|
||||
| Button-Komponenten | ~15 | icon, circle, loading |
|
||||
| Navigation | ~8 | icon, circle |
|
||||
| Editor | ~15 | icon |
|
||||
| Filter/Chat | ~10 | icon, circle |
|
||||
| Forms/Modals | ~5 | icon, loading |
|
||||
| Features/Pages | ~12 | icon, circle, loading |
|
||||
|
||||
### Fehlende OsButton-Features
|
||||
|
||||
| Feature | Benötigt für | Status |
|
||||
|---------|-------------|--------|
|
||||
| `icon` Prop | ~55 Buttons | ⬜ Fehlt |
|
||||
| `circle` Variant | ~25 Buttons | ⬜ Fehlt |
|
||||
| `loading` Prop | ~10 Buttons | ⬜ Fehlt |
|
||||
| `appearance="outline"` | ✅ Implementiert | ✅ Erledigt |
|
||||
| `appearance="ghost"` | ✅ Implementiert | ✅ Erledigt |
|
||||
| Feature | Status |
|
||||
|---------|--------|
|
||||
| `variant` (7 Varianten) | ✅ |
|
||||
| `appearance` (filled/outline/ghost) | ✅ |
|
||||
| `size` (xs/sm/md/lg/xl) | ✅ |
|
||||
| `icon` Slot | ✅ |
|
||||
| `circle` Prop | ✅ |
|
||||
| `loading` Prop | ✅ |
|
||||
| `disabled` mit hover/active-Override | ✅ |
|
||||
| `fullWidth` Prop | ✅ |
|
||||
| `type` Prop (button/submit/reset) | ✅ |
|
||||
|
||||
### Nächste Schritte
|
||||
|
||||
**Milestone 4a: 14 Buttons ohne neue Props migrieren**
|
||||
1. Modal Cancel-Buttons (3)
|
||||
2. Form Cancel/Submit-Buttons (3)
|
||||
3. ImageUploader Crop-Buttons (2)
|
||||
4. Page Buttons (6)
|
||||
|
||||
**Milestone 4b: Props für ~60 Buttons hinzufügen**
|
||||
1. Icon-Prop zu OsButton hinzufügen
|
||||
2. Circle-Variant zu OsButton hinzufügen
|
||||
3. Loading-Prop zu OsButton hinzufügen
|
||||
|
||||
**Milestone 4c: ~60 Buttons mit neuen Props migrieren**
|
||||
1. Button-Komponenten (~15)
|
||||
2. Navigation (~8)
|
||||
3. Editor (~15)
|
||||
4. Filter/Chat (~10)
|
||||
5. Forms/Modals (~5)
|
||||
6. Features/Pages (~12)
|
||||
1. Snapshot-Dateien aktualisieren
|
||||
2. Test-Selektoren anpassen
|
||||
3. BaseButton.vue ggf. entfernen
|
||||
4. Phase 4: Weitere Komponenten (OsIcon, OsCard, OsModal, ...)
|
||||
|
||||
### Integrations-Protokoll
|
||||
|
||||
|
||||
@ -80,11 +80,11 @@
|
||||
Phase 0: ██████████ 100% (6/6 Aufgaben) ✅
|
||||
Phase 1: ██████████ 100% (6/6 Aufgaben) ✅
|
||||
Phase 2: ██████████ 100% (26/26 Aufgaben) ✅
|
||||
Phase 3: █████████░ 83% (20/24 Aufgaben) - Webapp-Integration (M4a ✅, M5 ✅)
|
||||
Phase 3: ██████████ 100% (24/24 Aufgaben) ✅ - Webapp-Integration komplett
|
||||
Phase 4: █░░░░░░░░░ 6% (1/17 Aufgaben) - OsButton ✅
|
||||
Phase 5: ░░░░░░░░░░ 0% (0/7 Aufgaben)
|
||||
───────────────────────────────────────
|
||||
Gesamt: ███████░░░ 69% (59/86 Aufgaben)
|
||||
Gesamt: ████████░░ 74% (63/86 Aufgaben)
|
||||
```
|
||||
|
||||
### Katalogisierung (Details in KATALOG.md)
|
||||
@ -94,30 +94,31 @@ Styleguide: ██████████ 100% (38 Komponenten erfasst)
|
||||
Analyse: ██████████ 100% (Button, Modal, Menu detailiert)
|
||||
```
|
||||
|
||||
### OsButton Migration (Phase 3)
|
||||
### OsButton Migration (Phase 3) ✅
|
||||
```
|
||||
Scope gesamt: ~90 Buttons in Webapp
|
||||
├─ Migriert: 32 Buttons (36%) ✅
|
||||
├─ Ohne neue Props: 0 Buttons (Milestone 4a ✅)
|
||||
└─ Mit icon/circle/loading: ~60 Buttons (Milestone 4c)
|
||||
Scope gesamt: 133 <os-button> Tags in 79 Webapp-Dateien
|
||||
├─ Migriert: 133 Buttons (100%) ✅
|
||||
├─ <base-button>: 0 verbleibend in Templates
|
||||
├─ <ds-button>: 0 verbleibend in Templates
|
||||
└─ Cleanup: Snapshots/Tests müssen aktualisiert werden
|
||||
|
||||
OsButton Features:
|
||||
├─ variant: ✅ primary, secondary, danger, warning, success, info, default
|
||||
├─ appearance: ✅ filled, outline, ghost
|
||||
├─ size: ✅ xs, sm, md, lg, xl
|
||||
├─ size: ✅ sm, md, lg, xl
|
||||
├─ disabled: ✅ mit hover/active-Override
|
||||
├─ icon: ⬜ TODO (Milestone 4b)
|
||||
├─ circle: ⬜ TODO (Milestone 4b)
|
||||
└─ loading: ⬜ TODO (Milestone 4b)
|
||||
├─ icon: ✅ slot-basiert (icon-system-agnostisch)
|
||||
├─ circle: ✅ rounded-full, größenabhängig (p-1.5 bis p-3)
|
||||
└─ loading: ✅ animated SVG spinner, aria-busy (Milestone 4b)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Aktueller Stand
|
||||
|
||||
**Letzte Aktualisierung:** 2026-02-10 (Session 12)
|
||||
**Letzte Aktualisierung:** 2026-02-13 (Session 19)
|
||||
|
||||
**Aktuelle Phase:** Phase 3 (Webapp-Integration) - Milestone 4a abgeschlossen ✅ (32 Buttons migriert, nächster: Milestone 4b)
|
||||
**Aktuelle Phase:** Phase 3 ✅ ABGESCHLOSSEN + Code-Review-Feedback eingearbeitet
|
||||
|
||||
**Zuletzt abgeschlossen:**
|
||||
- [x] Projektordner erstellt
|
||||
@ -183,11 +184,171 @@ OsButton Features:
|
||||
- Completeness Check (verify Script prüft Story, Visual, checkA11y, Keyboard, Varianten)
|
||||
- ESLint Plugins: vuejs-accessibility, playwright, storybook, jsdoc
|
||||
|
||||
**Aktuell in Arbeit:**
|
||||
- Phase 3, Milestone 4b: icon/circle/loading Props in OsButton implementieren
|
||||
- Phase 3, Milestone 4c: ~60 Buttons mit icon/circle/loading migrieren
|
||||
**Zuvor abgeschlossen (Session 18 - CodeRabbit Review Feedback: data-test Selektoren, Accessibility, Bugfixes):**
|
||||
- [x] Cypress-Selektoren: `.user-content-menu button` → `[data-test="content-menu-button"]` (2 Step-Definitions)
|
||||
- [x] Cypress-Selektoren: `.content-menu button` → `[data-test="content-menu-button"]` (Admin.PinPost + ReportContent)
|
||||
- [x] muted-users.vue: `data-test="unmute-btn"` + `aria-label` auf Unmute-Button
|
||||
- [x] blocked-users.vue: `data-test="unblock-btn"` + `aria-label` auf Unblock-Button
|
||||
- [x] ProfileList.vue: `data-test="load-all-connections-btn"` + FollowList.spec.js Selektoren aktualisiert
|
||||
- [x] FollowButton.vue: `data-test="follow-btn"` + Spec-Selektoren aktualisiert
|
||||
- [x] JoinLeaveButton.vue: `data-test="join-leave-btn"` + `.native` von `@mouseenter`/`@mouseleave` entfernt
|
||||
- [x] LoginButton.vue: `data-test="login-btn"` + `aria-label="$t('login.login')"` + Spec-Selektoren aktualisiert
|
||||
- [x] ReportRow.spec.js: `button[data-variant="danger"]` → `[data-test="confirm"]`
|
||||
- [x] CtaJoinLeaveGroup.spec.js: Selektor auf `[data-test="join-leave-btn"]` aktualisiert
|
||||
- [x] DisableModal.vue: `finally { this.loading = false }` für Loading-State-Reset
|
||||
- [x] ReleaseModal.vue: `:loading="loading"` + `this.loading = true` + `finally { this.loading = false }`
|
||||
- [x] ChangePassword.vue: `:disabled="errors"` → `:disabled="!!errors"` (Boolean-Cast)
|
||||
- [x] Password/Change.vue: Unbenutzte `disabled: true` aus data() entfernt + 2 tote Tests entfernt
|
||||
- [x] MenuBar.vue: Unbenutztes `ref="linkButton"` entfernt
|
||||
- [x] GroupForm.vue: Cancel-Button `variant="default" appearance="filled"` (per User-Anweisung)
|
||||
- [x] `appearance="filled"` ergänzt: donations.vue, LoginForm.vue, EnterNonce.vue
|
||||
- [x] LoginForm.vue: CSS `.login-form button` → `.login-form button[type='submit']`
|
||||
- [x] pages/index.vue: Redundantes `class="my-filter-button"` von `<base-icon>` entfernt
|
||||
- [x] MySomethingList.vue: `:title` + `:aria-label` auf Edit/Delete-Buttons (Tooltip beibehalten)
|
||||
- [x] A11y aria-label auf icon-only Buttons: admin/users (search + edit), AddChatRoomByUserSearch (close), EmbedComponent (close), groups/index (create), profile/_id/_slug (new post), groups/_id/_slug (new post), CustomButton (2x tooltip), HeaderMenu (hamburger), ImageUploader (crop-cancel), ContentMenu (menu), HeaderButton (filter-remove), InviteButton (invite)
|
||||
- [x] post/_id/_slug/index.vue: Zustandsabhängiges `aria-label` (`post.sensitiveContent.show/hide`)
|
||||
- [x] ComponentSlider.vue: `aria-label` mit Interpolation (`component-slider.step`)
|
||||
- [x] i18n: `actions.search`, `actions.close`, `actions.menu` in allen 9 Sprachdateien
|
||||
- [x] i18n: `site.navigation` in allen 9 Sprachdateien
|
||||
- [x] i18n: `post.sensitiveContent.show/hide` in allen 9 Sprachdateien
|
||||
- [x] i18n: `component-slider.step` in allen 9 Sprachdateien
|
||||
|
||||
**Zuletzt abgeschlossen (Session 12 - CSS-Linting, CI-Optimierung, Code-Review Fixes):**
|
||||
**Zuletzt abgeschlossen (Session 19 - CodeRabbit Review Feedback: Cleanup, Accessibility, Bugfixes):**
|
||||
- [x] donations.vue: Redundantes `:checked="showDonations"` entfernt (v-model setzt checked bereits)
|
||||
- [x] MySomethingList.vue: Disabled-Logik vereinfacht `!(!isEditing || (isEditing && !disabled))` → `isEditing && disabled`
|
||||
- [x] button.variants.ts: Hardcoded Fallback `#e5e3e8` entfernt → `var(--color-disabled)` (konsistent mit filled/index.css)
|
||||
- [x] CommentCard.vue: `aria-label` auf icon-only Reply-Button
|
||||
- [x] HashtagsFilter.vue: `aria-label` auf icon-only Clear-Button
|
||||
- [x] ReleaseModal.vue: `$emit('close')` im catch-Block ergänzt (fehlte im Fehlerfall)
|
||||
- [x] Chat.vue: `aria-label` auf Expand- und Close-Buttons
|
||||
- [x] i18n: `chat.expandChat` + `chat.closeChat` in allen 9 Sprachdateien (vollständig übersetzt)
|
||||
- [x] ChatNotificationMenu.vue: `aria-label` auf icon-only Chat-Button
|
||||
- [x] SearchableInput.vue: `aria-label` auf icon-only Close-Button
|
||||
- [x] GroupButton.vue: `aria-label` auf icon-only Groups-Button
|
||||
- [x] MapButton.vue: `aria-label` auf icon-only Map-Button
|
||||
- [x] EmotionButton.vue: `aria-label` auf icon-only Emoji-Button (`<label for>` wirkt nicht auf `<button>`)
|
||||
- [x] ImageUploader.vue: `aria-label` auf icon-only Delete-Button
|
||||
- [x] i18n: `actions.clear` disambiguiert — es: "Borrar" → "Limpiar", it: "Cancella" → "Svuota" (Verwechslung mit `actions.delete`)
|
||||
- [x] OsButton.vue: `as string` Cast bei `attrClass` entfernt (cn/clsx verarbeitet Arrays/Objekte korrekt)
|
||||
- [x] CommentForm.vue: `handleSubmit` auf async/await + try/catch/finally umgestellt (Loading-Bug im Fehlerfall behoben)
|
||||
- [x] MenuBar.vue: `aria-label` auf alle 11 Editor-Toolbar-Buttons (nutzt bestehende `editor.legend.*` Keys)
|
||||
- [x] NotificationMenu.vue: `aria-label` auf alle 3 Bell-Buttons
|
||||
- [x] NotificationMenu.vue: `counter-icon` von Default-Slot in `#icon`-Slot verschoben (2 Stellen, Rendering-Bug)
|
||||
- [x] ChatNotificationMenu.vue: `counter-icon` von Default-Slot in `#icon`-Slot verschoben (Rendering-Bug)
|
||||
- [x] InviteButton.vue: `this.currentUser` → `this.user` (Bug: Getter hieß `user`, `currentUser` war undefined)
|
||||
- [x] pages/index.vue: `beforeDestroy()` aus `methods` in Lifecycle-Hook verschoben (Memory-Leak: Event-Listener wurden nie entfernt)
|
||||
- [x] Editor.vue: Fehlender `else`-Branch in `toggleLinkInput()` — `isLinkInputActive` wird jetzt auch bei no-args-Aufrufen (blur/esc) zurückgesetzt
|
||||
- [x] admin/users/index.vue: Veraltete Slot-Syntax `slot="role" slot-scope="scope"` → `#role="scope"` (Vue 3)
|
||||
- [x] settings/index.vue: Irreführender Komponentenname `NewsFeed` → `Settings`
|
||||
- [x] FilterMenu.spec.js: Typo `dropdwon` → `dropdown` im Testnamen
|
||||
- [x] ImageUploader.vue: `:title` auf Crop-Cancel-Button ergänzt (konsistent mit Delete-Button)
|
||||
- [x] OsButton.spec.ts: `as const` auf `sizes`-Objekt statt Type-Cast bei jedem `mount`-Aufruf
|
||||
- [x] CommentForm.vue: `disabled = false` aus `finally` in `catch` verschoben (verhindert Überschreiben nach `clear()`)
|
||||
- [x] FilterMenu.vue: `aria-label` auf icon-only Filter-Button
|
||||
- [x] ContextMenu.vue: `this.menu.show()` nur bei `type !== 'link'` (Link-Menüs öffneten sich sofort statt auf Klick zu warten)
|
||||
- [x] ContextMenu.vue: `this.menu = null` vor `destroy()` (Race-Condition: ESC + blur feuerten doppelt → removeChild-Error)
|
||||
- [x] CustomButton.vue: `variant="primary"` auf beide `os-button`-Instanzen (Konsistenz mit restlicher Codebase)
|
||||
- [x] Invitation.vue: Ungenutztes Argument `inviteCode.copy` bei `copyInviteCode()` entfernt
|
||||
- [x] CtaUnblockAuthor.vue: `appearance="filled"` explizit gesetzt (fehlte als einziger primärer CTA)
|
||||
- [x] HeaderMenu.vue: `beforeDestroy`-Hook ergänzt — Scroll-Listener wird jetzt entfernt (Memory-Leak)
|
||||
- [x] MenuLegend.vue: `variant="primary"` auf Trigger-Button (konsistent mit Toolbar-Buttons in MenuBar.vue)
|
||||
|
||||
**Zuvor abgeschlossen (Session 18 - Code-Review Feedback, OsButton Refactoring, Accessibility):**
|
||||
- [x] OsButton.vue vereinfacht: `vueAttrs()` Helper, Einmal-Variablen durch `cn()` ersetzt, `children` Array inline (217→227 Zeilen, aber lesbarer)
|
||||
- [x] OsButton: `@import "./animations.css"` vor `@source`-Direktiven verschoben (CSS-Spec-Konformität)
|
||||
- [x] CustomButton.vue: `isEmpty` aus `data()` entfernt → direkter Import im Computed
|
||||
- [x] notifications.spec.js: Doppelten `beforeEach` konsolidiert, `wrapper` in `describe`-Block verschoben
|
||||
- [x] MenuLegend.vue: `<style scoped>` hinzugefügt (verhindert Style-Leaking generischer Klassennamen)
|
||||
- [x] LocationSelect: `data-test="clear-location-button"` + spezifischerer Selektor im Spec
|
||||
- [x] HashtagsFilter: `data-test="clear-search-button"` + spezifischerer Selektor im Spec
|
||||
- [x] FollowButton.vue: `.native` Modifier von `@mouseenter`/`@mouseleave` entfernt (Vue 3 Kompatibilität)
|
||||
- [x] MapButton.vue: Icon in `<template #icon>` verschoben + redundantes Inline-Style entfernt
|
||||
- [x] MySomethingList.vue: Unbenutzte `.icon-button` CSS-Klasse entfernt
|
||||
- [x] PaginationButtons.vue: Hardcoded `aria-label` → `$t('pagination.previous/next')` (i18n)
|
||||
- [x] `pagination.previous/next` in allen 9 Sprachdateien angelegt
|
||||
- [x] GroupContentMenu.vue: `aria-label` via `$t('group.contentMenu.menuButton')` für icon-only Button
|
||||
- [x] `group.contentMenu.menuButton` in allen 9 Sprachdateien angelegt
|
||||
- [x] FilterMenu.vue: Veraltete `slot="default"` + `slot-scope` → `<template #default="{ toggleMenu }">` (Vue 3)
|
||||
- [x] HashtagsFilter.vue: `this.$t()` → `$t()` im Template (Vue 3 Kompatibilität)
|
||||
- [x] DisableModal.vue: `appearance="filled"` + `:loading="loading"` auf Danger-Button
|
||||
- [x] DeleteUserModal.vue: `appearance="filled"` + `:loading="loading"` auf Danger-Button
|
||||
- [x] my-email-address/index.vue: `loadingData` State + `:loading` auf Submit-Button + `finally` Block
|
||||
- [x] ReportModal.vue: `class="report-modal"` + CSS-Selektoren mit Prefix (verhindert globales Style-Leaking)
|
||||
- [x] DeleteUserModal.vue: CSS-Selektoren mit `.delete-user-modal` Prefix (verhindert globales Style-Leaking)
|
||||
- [x] Button-Wrapper-Analyse: GroupButton + MapButton als Kandidaten zum Inlining identifiziert (nur 1 Nutzungsort, keine Logik)
|
||||
|
||||
**Zuvor abgeschlossen (Session 16 - Bugfixes, Code-Review, letzte ds-button Migration):**
|
||||
- [x] Password/Change.vue: `!!errors` Fix für disabled-Prop
|
||||
- [x] CommentForm.vue: `type="submit"` + `!!errors` Fix
|
||||
- [x] GroupForm.vue: Letzter `<ds-button>` → `<os-button>` migriert (save/update mit icon)
|
||||
- [x] OsButton.spec.ts: TypeScript-Fix für size-Prop Union Type
|
||||
- [x] OsButton.vue: v8 ignore Coverage-Fixes (100% Branch Coverage)
|
||||
- [x] 0 `<ds-button>` und 0 `<base-button>` in Webapp-Templates verbleibend
|
||||
- [x] `data-variant` Attribut auf OsButton (konsistent mit `data-appearance`, CSS-Selektor-Support)
|
||||
- [x] notifications.spec.js: `wrapper.find()` → Testing Library `screen.getByText()` (war Vue Test Utils API)
|
||||
- [x] FilterMenu.vue: Dynamische `:appearance="filterActive ? 'filled' : 'ghost'"` (Regressionsbug)
|
||||
- [x] FilterMenu.spec.js: `data-appearance="filled"` statt CSS-Klasse `--filled`
|
||||
- [x] CtaUnblockAuthor.vue: `require` → `required` Typo-Fix
|
||||
- [x] LocationSelect.vue: `clearLocationName()` direkt via `this.currentValue` statt `event.target.value`
|
||||
- [x] LocationSelect.vue: `@click.native` → `@click` (Vue 3 Kompatibilität)
|
||||
- [x] LocationSelect.vue: `aria-label` via `$t('actions.clear')` (i18n)
|
||||
- [x] `actions.clear` in allen 9 Sprachdateien angelegt (en, de, fr, es, it, nl, pl, pt, ru)
|
||||
- [x] OsButton: JSDoc-Dokumentation für Slots (`@slot default`, `@slot icon`)
|
||||
- [x] OsButton: `isSmall` von `['xs', 'sm']` auf `size === 'sm'` vereinfacht (xs existiert nicht)
|
||||
- [x] OsButton: Strikte Typisierung `Record<Size, ...>` statt `Record<string, ...>` für Lookup-Maps
|
||||
- [x] animations.css: Stylelint-konforme Formatierung (eine Deklaration pro Zeile)
|
||||
|
||||
**Zuvor abgeschlossen (Session 15 - Milestone 4c komplett):**
|
||||
- [x] **Alle verbleibenden base-button Instanzen migriert** (132 os-button Tags, 0 base-button verbleibend)
|
||||
- [x] 59 Buttons in dieser Session migriert (Chat, Filter, Modals, Forms, Pages, etc.)
|
||||
- [x] `type="submit"` für alle Form-Buttons (OsButton default ist `type="button"`)
|
||||
- [x] `!!errors` Boolean-Cast für disabled-Props (errors ist Objekt, nicht Boolean)
|
||||
- [x] CSS-Selektoren `.base-button` → `> button` oder `button` angepasst
|
||||
- [x] `!important` für CSS-Positioning (überschreibt Tailwind-Klassen)
|
||||
- [x] Disabled outline border-color Fix (`var(--color-disabled-border,#e5e3e8)`)
|
||||
- [x] ComponentSlider Selection-Dots: dynamic appearance + 18px custom CSS
|
||||
- [x] pages/index.vue FAB: `size="xl"` + position/dimension `!important`
|
||||
- [x] pages/groups FAB: `size="xl"` + box-shadow `!important`
|
||||
- [x] ReportModal Breite auf 700px beibehalten
|
||||
- [x] ContributionForm Submit: `type="submit"` + `!!errors` Fix
|
||||
- [x] my-email-address/index.vue: `!!errors` Fix
|
||||
|
||||
**Zuvor abgeschlossen (Session 14 - Loading Prop, Circle Prop, Code-Optimierung):**
|
||||
- [x] `loading` Prop mit animiertem SVG-Spinner implementiert
|
||||
- [x] Spinner-Architektur: Beide Animationen (rotate + dash) auf `<circle>` Element (Chrome-Compositing-Bug-Workaround)
|
||||
- [x] Spinner zentriert auf Icon (Icon-Buttons) oder Button-Container (Text-Only-Buttons)
|
||||
- [x] Icon bleibt bei loading sichtbar, Spinner überlagert Icon-Bereich
|
||||
- [x] `aria-busy="true"` für Screenreader bei loading
|
||||
- [x] `circle` Prop implementiert (rounded-full, größenabhängige Breiten)
|
||||
- [x] `min-width` pro Größe hinzugefügt (verhindert zu kleine leere Buttons)
|
||||
- [x] Animations-Keyframes in `animations.css` ausgelagert (wiederverwendbar)
|
||||
- [x] Code-Optimierung: OsButton von ~250 auf 207 Zeilen vereinfacht
|
||||
- `buttonData` Objekt für Vue 2/3 geteilt
|
||||
- `SPINNER_PX` vereinfacht (Tuple → einfache Zahlen)
|
||||
- Redundante `cn()` Wrapping entfernt
|
||||
- `getCurrentInstance()` nur bei Vue 2 aufgerufen
|
||||
- [x] 76 Unit-Tests (5 neue: default type, data-appearance, min-w, icon-only loading, circle gap)
|
||||
- [x] Loading-Stories in Storybook (alle Varianten × Appearances)
|
||||
- [x] Visual Tests mit `animationPlayState = 'paused'` für stabile Screenshots
|
||||
- [x] PaginationButtons.vue migriert (2 circle icon-only Buttons)
|
||||
|
||||
**Zuvor abgeschlossen (Session 13 - Icon-Slot, Storybook Playground, Webapp-Migration):**
|
||||
- [x] Icon-Slot für OsButton implementiert (slot-basiert, icon-system-agnostisch)
|
||||
- [x] Render-Funktion: `slots.icon?.()` → `<span class="os-button__icon">` Wrapper
|
||||
- [x] Tailwind-Klassen direkt auf Icon-Wrapper (kein custom CSS in index.css nötig)
|
||||
- [x] VNode-basierte Text-Erkennung: Whitespace-only = icon-only (gap/margin-Logik)
|
||||
- [x] Storybook: 4 neue Stories (Icon, IconOnly, IconSizes, IconAppearances)
|
||||
- [x] Playground: Reaktiver Icon-Selektor (none/check/close/plus) + Label-Text-Control
|
||||
- [x] Visual Tests: 4 neue Tests mit Screenshots + a11y-Checks
|
||||
- [x] Unit Tests: 8 neue Tests (icon slot, keyboard a11y mit aria-label)
|
||||
- [x] Erste Webapp-Migration mit Icon: `my-email-address/index.vue` (Save-Button mit check-Icon)
|
||||
- [x] Code-Optimierung: ICON_CLASS Konstante, iconMargin Variable, vereinfachte hasText-Logik
|
||||
- [x] Größenabhängiger Gap: `gap-1` für xs/sm, `gap-2` für md/lg/xl
|
||||
- [x] Größenabhängiger Icon-Margin: kein negativer Margin bei xs/sm (mehr Abstand zur Button-Grenze)
|
||||
- [x] 6 weitere Buttons mit Icon migriert: DisableModal, DeleteUserModal, CtaUnblockAuthor, LocationSelect, CategoriesSelect, profile Chat
|
||||
- [x] verify.vue hat keinen Button (Eintrag korrigiert)
|
||||
|
||||
**Zuvor abgeschlossen (Session 12 - CSS-Linting, CI-Optimierung, Code-Review Fixes):**
|
||||
- [x] CSS-Linting: `@eslint/css` + `tailwind-csstree` für Tailwind v4 Syntax-Support
|
||||
- [x] `excludeCSS()` Helper: JS-Regeln von CSS-Dateien fernhalten (language-Inkompatibilität)
|
||||
- [x] CSS-Regeln: `no-empty-blocks`, `no-duplicate-imports`, `no-invalid-at-rules`
|
||||
@ -261,14 +422,20 @@ OsButton Features:
|
||||
1. ~~Phase 0: Komponenten-Analyse~~ ✅
|
||||
2. ~~Phase 1: Vue 2.7 Upgrade~~ ✅
|
||||
3. ~~**Phase 2: Projekt-Setup**~~ ✅ ABGESCHLOSSEN
|
||||
4. **Phase 3: Webapp-Integration** - 32/90 Buttons migriert (36%)
|
||||
4. ~~**Phase 3: Webapp-Integration**~~ ✅ ABGESCHLOSSEN — 133 Buttons in 79 Dateien
|
||||
- [x] yarn link / Webpack-Alias in Webapp
|
||||
- [x] CSS-Variablen definieren (ocelot-ui-variables.scss)
|
||||
- [x] 16 Buttons migriert & validiert ✅
|
||||
- [x] Docker Build + CI-Kompatibilität
|
||||
- [x] **Milestone 4a:** 14 weitere Buttons (ohne neue Props) ✅
|
||||
- [ ] **Milestone 4b:** icon/circle/loading Props implementieren
|
||||
- [ ] **Milestone 4c:** ~60 Buttons mit icon/circle/loading migrieren
|
||||
- [x] **Milestone 4b:** icon/circle/loading Props implementieren ✅
|
||||
- [x] **Milestone 4c:** Alle verbleibenden Buttons migriert ✅
|
||||
- [x] **Code-Review Feedback:** Refactoring, A11y, Vue 3 Compat, CSS-Scoping ✅
|
||||
5. **Nächstes:**
|
||||
- [ ] GroupButton + MapButton in HeaderMenu inlinen (keine eigene Komponente nötig)
|
||||
- [ ] `compat/` Verzeichnis in packages/ui anlegen (temporäre Migrations-Wrapper)
|
||||
- [ ] BaseIcon nach `compat/` verschieben (131 Nutzungen, Voraussetzung für weitere Migrationen)
|
||||
- [ ] Snapshots/Tests aktualisieren, BaseButton-Komponente ggf. entfernen
|
||||
|
||||
**Manuelle Setup-Aufgaben (außerhalb Code):**
|
||||
- [ ] `NPM_TOKEN` als GitHub Secret einrichten (für npm publish in ui-release.yml)
|
||||
@ -361,106 +528,120 @@ OsButton Features:
|
||||
- [x] OsButton attrs/listeners Forwarding (Vue 2 $listeners via getCurrentInstance)
|
||||
- [x] 14 weitere Buttons migriert (alle ohne icon/circle/loading)
|
||||
|
||||
**Milestone 4a: Weitere Buttons migrieren (14 ohne neue Props)**
|
||||
- [ ] Modal Cancel-Buttons (DisableModal, DeleteUserModal, ReleaseModal)
|
||||
- [ ] Form Cancel/Submit-Buttons (ContributionForm, EnterNonce, MySomethingList)
|
||||
- [ ] ImageUploader.vue (2× Crop-Buttons)
|
||||
- [ ] Page-Buttons (donations, badges, notifications/index, profile Unblock/Unmute)
|
||||
- [ ] ReportRow.vue More-Details-Button
|
||||
**Milestone 4a: Weitere Buttons migrieren (14 ohne neue Props)** ✅
|
||||
- [x] Modal Cancel-Buttons (DisableModal, DeleteUserModal, ReleaseModal)
|
||||
- [x] Form Cancel/Submit-Buttons (ContributionForm, EnterNonce, MySomethingList)
|
||||
- [x] ImageUploader.vue (2× Crop-Buttons)
|
||||
- [x] Page-Buttons (donations, badges, notifications/index, profile Unblock/Unmute)
|
||||
- [x] ReportRow.vue More-Details-Button
|
||||
|
||||
**Milestone 4b: OsButton Props erweitern**
|
||||
- [ ] `icon` Prop implementieren (slot-basiert oder Icon-Komponente)
|
||||
- [ ] `circle` Variant zu CVA hinzufügen
|
||||
- [ ] `loading` Prop mit Spinner implementieren
|
||||
**Milestone 4b: OsButton Props erweitern** ✅
|
||||
- [x] `icon` Slot implementiert (slot-basiert, icon-system-agnostisch) ✅
|
||||
- [x] `circle` Prop implementiert (rounded-full, größenabhängige Breiten) ✅
|
||||
- [x] `loading` Prop mit animiertem SVG-Spinner implementiert ✅
|
||||
|
||||
**Milestone 4c: Buttons mit icon/circle/loading migrieren (~60 Buttons)**
|
||||
**Milestone 4c: Buttons mit icon/circle/loading migrieren** ✅ ABGESCHLOSSEN
|
||||
|
||||
*Button-Komponenten (Wrapper):*
|
||||
- [ ] Button/JoinLeaveButton.vue (icon, loading)
|
||||
- [ ] Button/FollowButton.vue (icon, loading)
|
||||
- [ ] LoginButton/LoginButton.vue (icon, circle)
|
||||
- [ ] InviteButton/InviteButton.vue (icon, circle)
|
||||
- [ ] EmotionButton/EmotionButton.vue (circle)
|
||||
- [ ] CustomButton/CustomButton.vue (2× circle)
|
||||
- [ ] LabeledButton/LabeledButton.vue (icon, circle)
|
||||
- [x] Button/JoinLeaveButton.vue (icon, loading) ✅
|
||||
- [x] Button/FollowButton.vue (icon, loading) ✅
|
||||
- [x] LoginButton/LoginButton.vue (icon, circle) ✅
|
||||
- [x] InviteButton/InviteButton.vue (icon, circle) ✅
|
||||
- [x] EmotionButton/EmotionButton.vue (circle) ✅
|
||||
- [x] CustomButton/CustomButton.vue (2× circle) ✅
|
||||
- [x] LabeledButton/LabeledButton.vue (icon, circle) ✅
|
||||
|
||||
*Navigation & Menus:*
|
||||
- [ ] ContentMenu/ContentMenu.vue (icon, circle)
|
||||
- [ ] ContentMenu/GroupContentMenu.vue (icon, circle)
|
||||
- [ ] ChatNotificationMenu.vue (circle)
|
||||
- [ ] NotificationMenu.vue (3× icon, circle)
|
||||
- [ ] HeaderMenu/HeaderMenu.vue (icon, circle)
|
||||
- [ ] Map/MapButton.vue (circle)
|
||||
- [x] ContentMenu/ContentMenu.vue (icon, circle) ✅
|
||||
- [x] ContentMenu/GroupContentMenu.vue (icon, circle) ✅
|
||||
- [x] ChatNotificationMenu.vue (circle) ✅
|
||||
- [x] NotificationMenu.vue (3× icon, circle) ✅
|
||||
- [x] HeaderMenu/HeaderMenu.vue (icon, circle) ✅
|
||||
- [x] Map/MapButton.vue (circle) ✅
|
||||
|
||||
*Editor:*
|
||||
- [ ] Editor/MenuBarButton.vue (icon, circle)
|
||||
- [ ] Editor/MenuLegend.vue (~10× icon, circle)
|
||||
- [x] Editor/MenuBar.vue (~11× icon, circle) ✅
|
||||
- [x] Editor/MenuLegend.vue (2× icon) ✅
|
||||
|
||||
*Filter & Input:*
|
||||
- [ ] HashtagsFilter.vue (icon, circle)
|
||||
- [ ] CategoriesSelect.vue (icon)
|
||||
- [ ] SearchableInput.vue (icon, circle)
|
||||
- [ ] Select/LocationSelect.vue (icon)
|
||||
- [ ] PaginationButtons.vue (2× icon, circle)
|
||||
- [x] HashtagsFilter.vue (icon, circle) ✅
|
||||
- [x] CategoriesSelect.vue (icon) ✅
|
||||
- [x] SearchableInput.vue (icon, circle) ✅
|
||||
- [x] Select/LocationSelect.vue (icon) ✅
|
||||
- [x] PaginationButtons.vue (2× icon, circle) ✅
|
||||
|
||||
*Chat:*
|
||||
- [ ] Chat/Chat.vue (2× icon, circle)
|
||||
- [ ] Chat/AddChatRoomByUserSearch.vue (icon, circle)
|
||||
- [x] Chat/Chat.vue (2× icon, circle) ✅
|
||||
- [x] Chat/AddChatRoomByUserSearch.vue (icon, circle) ✅
|
||||
|
||||
*Forms & Auth:*
|
||||
- [ ] LoginForm/LoginForm.vue (icon, loading)
|
||||
- [ ] PasswordReset/Request.vue (loading)
|
||||
- [ ] PasswordReset/ChangePassword.vue (loading)
|
||||
- [ ] Password/Change.vue (loading)
|
||||
- [ ] ContributionForm.vue Submit (icon, loading)
|
||||
- [ ] GroupForm.vue Submit (icon)
|
||||
- [ ] CommentForm/CommentForm.vue (loading)
|
||||
- [x] LoginForm/LoginForm.vue (icon, loading) ✅
|
||||
- [x] PasswordReset/Request.vue (loading) ✅
|
||||
- [x] PasswordReset/ChangePassword.vue (loading) ✅
|
||||
- [x] Password/Change.vue (loading) ✅
|
||||
- [x] ContributionForm.vue Submit (icon, loading) ✅
|
||||
- [x] CommentForm/CommentForm.vue (loading) ✅
|
||||
|
||||
*Modals:*
|
||||
- [ ] Modal/ConfirmModal.vue (2× icon, loading)
|
||||
- [ ] Modal/ReportModal.vue (2× icon, loading)
|
||||
- [ ] Modal/DisableModal.vue Confirm (icon)
|
||||
- [ ] Modal/DeleteUserModal.vue Confirm (icon)
|
||||
- [ ] Modal/ReleaseModal.vue Confirm (icon)
|
||||
- [x] Modal/ConfirmModal.vue (2× icon, loading) ✅
|
||||
- [x] Modal/ReportModal.vue (2× icon, loading) ✅
|
||||
- [x] Modal/DisableModal.vue Confirm (icon) ✅
|
||||
- [x] Modal/DeleteUserModal.vue Confirm (icon) ✅
|
||||
- [x] Modal/ReleaseModal.vue Confirm (icon) ✅
|
||||
|
||||
*Features:*
|
||||
- [ ] ComponentSlider.vue (2× icon, loading)
|
||||
- [ ] MySomethingList.vue (3× icon, circle, loading)
|
||||
- [ ] CreateInvitation.vue (icon, circle)
|
||||
- [ ] Invitation.vue (2× icon, circle)
|
||||
- [ ] ProfileList.vue (loading)
|
||||
- [ ] ReportRow.vue Confirm (icon)
|
||||
- [ ] ImageUploader.vue Delete/Cancel (2× icon, circle)
|
||||
- [ ] CommentCard.vue Reply (icon, circle)
|
||||
- [ ] EmbedComponent.vue Close (icon, circle)
|
||||
- [ ] CtaUnblockAuthor.vue (icon)
|
||||
- [ ] data-download.vue (icon, loading)
|
||||
- [x] ComponentSlider.vue (2× icon) ✅
|
||||
- [x] MySomethingList.vue (3× icon, circle) ✅
|
||||
- [x] CreateInvitation.vue (icon, circle) ✅
|
||||
- [x] Invitation.vue (2× icon, circle) ✅
|
||||
- [x] ProfileList.vue (loading) ✅
|
||||
- [x] ReportRow.vue Confirm (icon) ✅
|
||||
- [x] ImageUploader.vue Delete/Cancel (2× icon, circle) ✅
|
||||
- [x] CommentCard.vue Reply (icon, circle) ✅
|
||||
- [x] EmbedComponent.vue Close (icon, circle) ✅
|
||||
- [x] CtaUnblockAuthor.vue (icon) ✅
|
||||
- [x] data-download.vue (icon, loading) ✅
|
||||
- [x] ActionButton.vue (icon, circle) ✅
|
||||
- [x] DeleteData.vue (icon) ✅
|
||||
- [x] GroupButton.vue (icon, circle) ✅
|
||||
|
||||
*Filter-Menüs:*
|
||||
- [x] FilterMenu/FilterMenu.vue (icon) ✅
|
||||
- [x] FilterMenu/HeaderButton.vue (2× icon) ✅
|
||||
- [x] FilterMenu/CategoriesFilter.vue (2× icon) ✅
|
||||
- [x] FilterMenu/OrderByFilter.vue (2×) ✅
|
||||
- [x] FilterMenu/EventsByFilter.vue (2×) ✅
|
||||
- [x] FilterMenu/FollowingFilter.vue (3×) ✅
|
||||
|
||||
*Pages:*
|
||||
- [ ] pages/groups/_id/_slug.vue (3× icon, circle, loading)
|
||||
- [ ] pages/admin/users/index.vue (2× icon, circle, loading)
|
||||
- [ ] pages/settings/index.vue (icon, loading)
|
||||
- [ ] pages/settings/blocked-users.vue (icon, circle)
|
||||
- [ ] pages/settings/muted-users.vue (icon, circle)
|
||||
- [ ] pages/settings/my-email-address/*.vue (2× icon)
|
||||
- [ ] pages/profile/_id/_slug.vue Chat (icon)
|
||||
- [ ] pages/post/_id/_slug/index.vue (icon, circle)
|
||||
- [x] pages/index.vue (2× icon, circle) ✅
|
||||
- [x] pages/groups/index.vue (icon, circle) ✅
|
||||
- [x] pages/groups/_id/_slug.vue (3× icon, circle) ✅
|
||||
- [x] pages/admin/users/index.vue (2× icon, circle) ✅
|
||||
- [x] pages/settings/index.vue (icon) ✅
|
||||
- [x] pages/settings/blocked-users.vue (icon, circle) ✅
|
||||
- [x] pages/settings/muted-users.vue (icon, circle) ✅
|
||||
- [x] pages/settings/data-download.vue (icon) ✅
|
||||
- [x] pages/settings/my-email-address/index.vue (icon) ✅
|
||||
- [x] pages/settings/my-email-address/enter-nonce.vue (icon) ✅
|
||||
- [x] pages/profile/_id/_slug.vue (icon, circle) ✅
|
||||
- [x] pages/post/_id/_slug/index.vue (icon, circle) ✅
|
||||
|
||||
**Milestone 5: Validierung & Dokumentation** ✅
|
||||
- [x] Keine visuellen Änderungen bestätigt (16/16 Buttons validiert)
|
||||
- [x] Keine funktionalen Änderungen bestätigt
|
||||
- [x] Disabled-Styles korrigiert (hover/active-Override, Border-Fix)
|
||||
- [ ] Webapp-Tests bestehen weiterhin (TODO: Regressionstest)
|
||||
- [ ] Webapp-Tests bestehen weiterhin (TODO: Snapshots aktualisieren)
|
||||
- [ ] Erkenntnisse in KATALOG.md dokumentiert
|
||||
|
||||
**Einsatzstellen-Übersicht:**
|
||||
|
||||
| Kategorie | Buttons | Status |
|
||||
|-----------|---------|--------|
|
||||
| ✅ Migriert & Validiert | 24 | Erledigt |
|
||||
| ⏳ Ohne neue Props (M4a) | 6 | In Arbeit (8 von 14 erledigt) |
|
||||
| ⬜ Mit icon/circle/loading (M4c) | ~60 | Ausstehend |
|
||||
| **Gesamt** | **~90** | **27% erledigt** |
|
||||
| ✅ Migriert (gesamt) | 133 | 79 Dateien |
|
||||
| ⬜ `<base-button>` verbleibend | 0 | Nur BaseButton.vue Definition + Test-Dateien |
|
||||
| ⬜ `<ds-button>` verbleibend | 0 | Alle ersetzt |
|
||||
| **Gesamt** | **133** | **100% erledigt** ✅ |
|
||||
|
||||
**Details siehe KATALOG.md** (vollständige Tracking-Tabellen)
|
||||
|
||||
@ -1486,6 +1667,87 @@ Bei der Migration werden:
|
||||
| 2026-02-10 | **CI-Workflow-Trigger** | 9 UI-Workflows von `on: push` auf `push`+`pull_request` mit Branch-Filter (`master`) und Path-Filter (`packages/ui/**` + Workflow-Datei) umgestellt |
|
||||
| 2026-02-10 | **custom-class entfernt** | `custom-class` Prop (entfernt aus OsButton) → `class` Attribut in notifications.vue, MapStylesButtons.vue, EmbedComponent.vue (4 Stellen); Snapshot aktualisiert |
|
||||
| 2026-02-10 | **Vue 3 Template-Fix** | `this.$t()` → `$t()` in CommentCard.vue (this im Template in Vue 3 nicht verfügbar) |
|
||||
| 2026-02-11 | **Icon-Slot implementiert** | Benannter `#icon` Slot für OsButton, slot-basiert statt Icon-Prop (icon-system-agnostisch) |
|
||||
| 2026-02-11 | **Icon-Wrapper Klassen** | Tailwind-Utility-Klassen direkt auf `<span>`: `inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current` |
|
||||
| 2026-02-11 | **VNode Text-Erkennung** | `hasText` prüft VNode-Children auf sichtbaren Inhalt; whitespace-only → icon-only Verhalten |
|
||||
| 2026-02-11 | **Gap & Margin Logik** | `gap-2` bei Icon+Text, `-ml-1` bei Icon, `-ml-1 -mr-1` bei Icon-Only (optischer Ausgleich) |
|
||||
| 2026-02-11 | **4 neue Stories** | Icon, IconOnly, IconSizes, IconAppearances mit Inline-SVG Komponenten (CheckIcon, CloseIcon, PlusIcon) |
|
||||
| 2026-02-11 | **Playground erweitert** | Reaktiver Icon-Selektor (none/check/close/plus) + Label-Text-Control via `computed()` |
|
||||
| 2026-02-11 | **Storybook: components Option** | Funktionale Komponenten müssen in `components` registriert werden, nicht in `setup()` return |
|
||||
| 2026-02-11 | **Storybook: CSS nicht in index.css** | Storybook lädt eigene `storybook.css`, nicht `src/styles/index.css` → Utility-Klassen direkt verwenden |
|
||||
| 2026-02-11 | **SVG-Targeting** | `[&>svg]` statt `[&>*]` für Icon-Sizing (BaseIcon rendert `<span><svg>`, Wrapper-Span darf nicht beeinflusst werden) |
|
||||
| 2026-02-11 | **my-email-address migriert** | Save-Button: `<os-button variant="primary">` mit `<template #icon><base-icon name="check" /></template>` |
|
||||
| 2026-02-11 | **Code-Optimierung** | `ICON_CLASS` Konstante extrahiert, `iconMargin` Variable, vereinfachte `hasText`-Logik (kein Symbol.for) |
|
||||
| 2026-02-11 | **Größenabhängiger Gap** | `gap-1` (4px) für xs/sm, `gap-2` (8px) für md/lg/xl bei Icon+Text |
|
||||
| 2026-02-11 | **Größenabhängiger Margin** | Kein negativer Icon-Margin bei xs/sm (voller Padding-Abstand zur Button-Grenze) |
|
||||
| 2026-02-11 | **DisableModal.vue** | Confirm-Button migriert: `danger filled icon="exclamation-circle"` → `variant="danger"` + `#icon` Slot |
|
||||
| 2026-02-11 | **DeleteUserModal.vue** | Confirm-Button migriert: identisches Pattern wie DisableModal |
|
||||
| 2026-02-11 | **CtaUnblockAuthor.vue** | Button migriert: `filled icon="arrow-right"` → `variant="primary"` + `#icon` Slot, OsButton importiert |
|
||||
| 2026-02-11 | **LocationSelect.vue** | Icon-only Close-Button migriert: `ghost size="small" icon="close"` → `variant="primary" appearance="ghost" size="sm"` + aria-label |
|
||||
| 2026-02-11 | **CategoriesSelect.vue** | v-for Buttons migriert: dynamisches `:icon` → `#icon` Slot, `:filled` → `:appearance`, CSS `.base-button` → `button` |
|
||||
| 2026-02-11 | **profile/_id/_slug.vue** | Chat-Button migriert: `icon="chat-bubble"` → `variant="primary" appearance="outline" full-width` + `#icon` Slot |
|
||||
| 2026-02-11 | **verify.vue korrigiert** | Kein Button vorhanden (Eintrag aus Milestone-Liste entfernt) |
|
||||
| 2026-02-11 | **PaginationButtons.vue** | 2 circle icon-only Buttons migriert: `outline primary circle` + `#icon` Slot + aria-label |
|
||||
| 2026-02-11 | **OsButton: circle Prop** | `circle` Prop: `rounded-full p-0` + größenabhängige Breiten (CIRCLE_WIDTHS Map) |
|
||||
| 2026-02-11 | **OsButton: loading Prop** | Animierter SVG-Spinner mit `aria-busy="true"`, Button auto-disabled bei loading |
|
||||
| 2026-02-11 | **Spinner-Architektur** | Beide Animationen (rotate + dash) auf `<circle>` Element; SVG ist statischer Container; Chrome-Compositing-Bug-Workaround |
|
||||
| 2026-02-11 | **Spinner-Zentrierung** | Icon-Buttons: Spinner über Icon (translate-basiert, overflow:visible); Text-Buttons: Spinner im Button-Container (inset-0 m-auto) |
|
||||
| 2026-02-11 | **animations.css** | Keyframes `os-spinner-dash` + `os-spinner-rotate` in separate CSS-Datei ausgelagert |
|
||||
| 2026-02-11 | **min-width pro Größe** | `min-w-[26px]`/`min-w-[36px]`/`min-w-12`/`min-w-14` in button.variants.ts (verhindert zu kleine leere Buttons) |
|
||||
| 2026-02-11 | **Code-Optimierung** | OsButton ~250→207 Zeilen: buttonData geteilt, SPINNER_PX vereinfacht, redundante cn() entfernt, getCurrentInstance nur Vue 2 |
|
||||
| 2026-02-11 | **5 neue Unit-Tests** | default type, data-appearance, min-w, icon-only loading, circle gap-1; gesamt: 76 Tests |
|
||||
| 2026-02-11 | **Milestone 4b abgeschlossen** | icon ✅, circle ✅, loading ✅ — alle OsButton-Props implementiert |
|
||||
| 2026-02-11 | **Milestone 4c: 59 Buttons** | Chat (2), AddChatRoomByUserSearch (1), CommentCard (1), CommentForm (2), ComponentSlider (2), ContributionForm (1), DeleteData (1), EmbedComponent (1), FilterMenu (1), HeaderButton (2), CategoriesFilter (2), OrderByFilter (2), EventsByFilter (2), FollowingFilter (3), GroupButton (1), ConfirmModal (2), ReportModal (2), Password/Change (1), PasswordReset/Request (1), PasswordReset/ChangePassword (1), Registration/Signup (1), ReleaseModal (1), ImageUploader (2), CreateInvitation (1), Invitation (2), ProfileList (1), ReportRow (1), MySomethingList (3), ActionButton (1), pages/index (2), profile/add-post (1), post/blur-toggle (1), groups/slug (3), settings/index (1), admin/users (2), blocked-users (1), data-download (1), muted-users (1), groups/index (1), enter-nonce (1) |
|
||||
| 2026-02-11 | **type="submit" Pattern** | OsButton hat `type="button"` als Default; alle Form-Submit-Buttons brauchen explizit `type="submit"` |
|
||||
| 2026-02-11 | **!!errors Pattern** | DsForm `errors` ist ein Objekt, nicht Boolean; OsButton `disabled` Prop erwartet Boolean → `!!errors` nötig |
|
||||
| 2026-02-11 | **CSS-Selector Pattern** | `.base-button` → `> button` oder `button`; Position/Dimensions brauchen `!important` für Tailwind-Override |
|
||||
| 2026-02-11 | **Disabled border-color** | Outline disabled border von `var(--color-disabled)` auf `var(--color-disabled-border,#e5e3e8)` mit Fallback |
|
||||
| 2026-02-11 | **Phase 3 abgeschlossen** | 132 `<os-button>` Tags in 78 Dateien, 0 `<base-button>` in Templates verbleibend |
|
||||
| 2026-02-11 | **Password/Change.vue Fix** | `!!errors` für disabled-Prop (DsForm errors ist Objekt) |
|
||||
| 2026-02-11 | **CommentForm.vue Fix** | `type="submit"` fehlte + `!!errors` für disabled-Prop |
|
||||
| 2026-02-11 | **GroupForm.vue ds-button** | Letzter `<ds-button>` in Webapp → `<os-button>` mit `#icon` Slot migriert |
|
||||
| 2026-02-11 | **OsButton.spec.ts TS-Fix** | `size` aus `Object.entries` als Union Type gecastet (`as 'sm' | 'md' | 'lg' | 'xl'`) |
|
||||
| 2026-02-11 | **Coverage 100%** | `v8 ignore start/stop` für Vue 2 Branch, `v8 ignore next` für defensive `||` Fallback |
|
||||
| 2026-02-11 | **Scope: 133 Buttons** | 133 `<os-button>` Tags in 79 Dateien, 0 `<base-button>` + 0 `<ds-button>` verbleibend |
|
||||
| 2026-02-12 | **data-variant Attribut** | OsButton rendert `data-variant` auf `<button>` (konsistent mit `data-appearance`), ermöglicht CSS-Selektoren wie `button[data-variant="danger"]` |
|
||||
| 2026-02-12 | **notifications.spec.js** | Test-API korrigiert: `wrapper.find()` (Vue Test Utils) → `screen.getByText()` (Testing Library), `button.disabled` statt `button.attributes('disabled')` |
|
||||
| 2026-02-12 | **FilterMenu Regressionsbug** | `appearance="ghost"` war hardcoded statt dynamisch; `filterActive` Computed Property existierte aber war nicht genutzt → `:appearance="filterActive ? 'filled' : 'ghost'"` |
|
||||
| 2026-02-12 | **FilterMenu.spec.js** | Test von CSS-Klasse `--filled` auf `data-appearance="filled"` Attribut-Selektor umgestellt |
|
||||
| 2026-02-12 | **CtaUnblockAuthor.vue** | Typo `require: true` → `required: true` (Vue ignorierte die Prop-Validierung) |
|
||||
| 2026-02-12 | **LocationSelect.vue Fixes** | `event.target.value` → `this.currentValue` (Button hat kein value), `@click.native` → `@click` (Vue 3), `aria-label` via i18n |
|
||||
| 2026-02-12 | **i18n: actions.clear** | Neuer Key in allen 9 Sprachdateien: en=Clear, de=Zurücksetzen, fr=Effacer, es=Borrar, it=Cancella, nl=Wissen, pl=Wyczyść, pt=Limpar, ru=Очистить |
|
||||
| 2026-02-12 | **OsButton JSDoc** | Slot-Dokumentation (`@slot default`, `@slot icon`) für vue-component-meta/Storybook autodocs |
|
||||
| 2026-02-12 | **OsButton xs entfernt** | `isSmall` von `['xs', 'sm'].includes(size)` auf `size === 'sm'` vereinfacht (xs ist kein gültiger Size-Wert) |
|
||||
| 2026-02-12 | **Strikte Typisierung** | `type Size = NonNullable<ButtonVariants['size']>`, `Record<Size, ...>` für CIRCLE_WIDTHS + SPINNER_PX; `props.size!` → `(props.size ?? 'md') as Size` |
|
||||
| 2026-02-12 | **animations.css** | Stylelint-konforme Formatierung: eine Deklaration pro Zeile, Leerzeilen zwischen Keyframe-Stufen |
|
||||
| 2026-02-12 | **OsButton Refactoring** | `vueAttrs()` Helper für Vue 2/3 Attribut-Handling, Einmal-Variablen durch `cn()` ersetzt, `children` inline; 77 Tests, 100% Coverage |
|
||||
| 2026-02-12 | **CSS @import Reihenfolge** | `@import "./animations.css"` vor `@source`-Direktiven verschoben (CSS-Spec: @import vor anderen At-Rules) |
|
||||
| 2026-02-12 | **CustomButton Cleanup** | `isEmpty` aus `data()` entfernt — reine Utility-Funktion braucht keine Vue-Reaktivität |
|
||||
| 2026-02-12 | **notifications.spec.js** | Doppelten `beforeEach` konsolidiert; `wrapper` von Modulebene in `describe`-Block verschoben |
|
||||
| 2026-02-12 | **Style-Scoping** | MenuLegend.vue: `<style scoped>` hinzugefügt; ReportModal + DeleteUserModal: CSS-Selektoren mit Komponenten-Prefix |
|
||||
| 2026-02-12 | **data-test Selektoren** | LocationSelect (`clear-location-button`) + HashtagsFilter (`clear-search-button`): spezifischere Test-Selektoren |
|
||||
| 2026-02-12 | **Vue 3 Compat Fixes** | FollowButton: `.native` entfernt; FilterMenu: `slot`/`slot-scope` → `<template #default>`; HashtagsFilter: `this.$t()` → `$t()` |
|
||||
| 2026-02-12 | **A11y: aria-label** | GroupContentMenu icon-only Button: `$t('group.contentMenu.menuButton')`; PaginationButtons: `$t('pagination.previous/next')` |
|
||||
| 2026-02-12 | **i18n Keys** | `pagination.previous/next` + `group.contentMenu.menuButton` in allen 9 Sprachdateien angelegt |
|
||||
| 2026-02-12 | **Modal Konsistenz** | DisableModal + DeleteUserModal: `appearance="filled"` + `:loading="loading"` auf Danger-Buttons |
|
||||
| 2026-02-12 | **Loading State** | my-email-address/index.vue: `loadingData` hinzugefügt + `finally` Block für Reset |
|
||||
| 2026-02-12 | **MapButton #icon Slot** | Icon von Default-Slot in `<template #icon>` verschoben (konsistent mit allen anderen Buttons) |
|
||||
| 2026-02-12 | **Dead Code entfernt** | MySomethingList.vue: `.icon-button` CSS-Klasse (nach Migration nicht mehr verwendet) |
|
||||
| 2026-02-12 | **Button-Wrapper-Analyse** | 15 OsButton-Wrapper klassifiziert: 4 Smart (Apollo/Vuex), 4 Presentational, 7 Borderline; GroupButton + MapButton als Inline-Kandidaten identifiziert |
|
||||
| 2026-02-12 | **compat/ Konzept** | Separates Verzeichnis für temporäre Migrations-Wrapper (nicht von check-completeness.ts erfasst); BaseIcon als erster Kandidat (131 Nutzungen) |
|
||||
| 2026-02-13 | **data-test Selektoren** | ~10 Komponenten: `data-test` Attribute für robuste Test-Selektoren (unmute-btn, unblock-btn, follow-btn, join-leave-btn, login-btn, load-all-connections-btn, content-menu-button) |
|
||||
| 2026-02-13 | **Cypress Selektoren** | 4 Step-Definitions: `.user-content-menu button` / `.content-menu button` → `[data-test="content-menu-button"]` |
|
||||
| 2026-02-13 | **Spec-Selektoren** | FollowList, FollowButton, LoginButton, CtaJoinLeaveGroup, ReportRow, muted-users, blocked-users: generische `button` → `[data-test="..."]` |
|
||||
| 2026-02-13 | **A11y: aria-label** | ~15 icon-only Buttons: aria-label hinzugefügt (admin/users, AddChatRoom, EmbedComponent, groups, profile, CustomButton, HeaderMenu, ImageUploader, ContentMenu, HeaderButton, InviteButton, LoginButton, blocked/muted-users) |
|
||||
| 2026-02-13 | **Zustandsabhängiges aria-label** | post/_id/_slug: `$t(blurred ? 'post.sensitiveContent.show' : 'post.sensitiveContent.hide')` |
|
||||
| 2026-02-13 | **ComponentSlider aria-label** | Interpoliertes Label: `$t('component-slider.step', { current: index + 1, total: ... })` |
|
||||
| 2026-02-13 | **i18n Keys (6 neue)** | `actions.search`, `actions.close`, `actions.menu`, `site.navigation`, `post.sensitiveContent.show/hide`, `component-slider.step` in allen 9 Sprachdateien |
|
||||
| 2026-02-13 | **Loading-State Fixes** | DisableModal + ReleaseModal: `finally { this.loading = false }` für Reset |
|
||||
| 2026-02-13 | **Bugfixes** | ChangePassword: `!!errors`; Password/Change: `disabled` aus data() entfernt + 2 tote Tests; MenuBar: unbenutztes `ref` entfernt |
|
||||
| 2026-02-13 | **Button-Props** | GroupForm cancel: `variant="default" appearance="filled"`; donations/LoginForm/EnterNonce: `appearance="filled"` ergänzt |
|
||||
| 2026-02-13 | **CSS-Selektoren** | LoginForm: `.login-form button` → `.login-form button[type='submit']`; pages/index: redundante Klasse auf BaseIcon entfernt |
|
||||
| 2026-02-13 | **JoinLeaveButton** | `.native` von `@mouseenter`/`@mouseleave` entfernt (Vue 3 Kompatibilität) |
|
||||
| 2026-02-13 | **MySomethingList** | `:title` + `:aria-label` auf Edit/Delete-Buttons (Tooltip beibehalten neben Accessibility) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -78,6 +78,19 @@ describe('osButton', () => {
|
||||
})
|
||||
expect(wrapper.classes()).toContain('h-12')
|
||||
})
|
||||
|
||||
it('has min-width matching height for each size', () => {
|
||||
const sizes = {
|
||||
sm: 'min-w-[26px]',
|
||||
md: 'min-w-[36px]',
|
||||
lg: 'min-w-12',
|
||||
xl: 'min-w-14',
|
||||
} as const
|
||||
for (const [size, expected] of Object.entries(sizes)) {
|
||||
const wrapper = mount(OsButton, { props: { size: size as keyof typeof sizes } })
|
||||
expect(wrapper.classes()).toContain(expected)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('applies fullWidth class', () => {
|
||||
@ -101,6 +114,11 @@ describe('osButton', () => {
|
||||
expect(wrapper.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('defaults to type="button"', () => {
|
||||
const wrapper = mount(OsButton)
|
||||
expect(wrapper.attributes('type')).toBe('button')
|
||||
})
|
||||
|
||||
it('sets button type', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { type: 'submit' },
|
||||
@ -108,6 +126,20 @@ describe('osButton', () => {
|
||||
expect(wrapper.attributes('type')).toBe('submit')
|
||||
})
|
||||
|
||||
it('sets data-variant attribute', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { variant: 'danger' },
|
||||
})
|
||||
expect(wrapper.attributes('data-variant')).toBe('danger')
|
||||
})
|
||||
|
||||
it('sets data-appearance attribute', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { appearance: 'outline' },
|
||||
})
|
||||
expect(wrapper.attributes('data-appearance')).toBe('outline')
|
||||
})
|
||||
|
||||
it('emits click event', async () => {
|
||||
const wrapper = mount(OsButton)
|
||||
await wrapper.trigger('click')
|
||||
@ -131,6 +163,337 @@ describe('osButton', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('icon slot', () => {
|
||||
it('renders icon slot content in .os-button__icon wrapper', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
slots: { icon: '<svg data-testid="icon"></svg>' },
|
||||
})
|
||||
const iconWrapper = wrapper.find('.os-button__icon')
|
||||
expect(iconWrapper.exists()).toBeTruthy()
|
||||
expect(iconWrapper.find('[data-testid="icon"]').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders both icon and text', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
slots: {
|
||||
icon: '<svg data-testid="icon"></svg>',
|
||||
default: 'Save',
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.os-button__icon').exists()).toBeTruthy()
|
||||
expect(wrapper.text()).toContain('Save')
|
||||
})
|
||||
|
||||
it('adds gap-2 class when icon and text are present', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
slots: {
|
||||
icon: '<svg></svg>',
|
||||
default: 'Save',
|
||||
},
|
||||
})
|
||||
const contentSpan = wrapper.find('button > span')
|
||||
expect(contentSpan.classes()).toContain('gap-2')
|
||||
})
|
||||
|
||||
it('adds gap-1 class for small sizes with icon and text', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { size: 'sm' },
|
||||
slots: {
|
||||
icon: '<svg></svg>',
|
||||
default: 'Save',
|
||||
},
|
||||
})
|
||||
const contentSpan = wrapper.find('button > span')
|
||||
expect(contentSpan.classes()).toContain('gap-1')
|
||||
expect(contentSpan.classes()).not.toContain('gap-2')
|
||||
})
|
||||
|
||||
it('does not add gap-2 for icon-only button', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
slots: { icon: '<svg></svg>' },
|
||||
})
|
||||
const contentSpan = wrapper.find('button > span')
|
||||
expect(contentSpan.classes()).not.toContain('gap-2')
|
||||
})
|
||||
|
||||
it('treats whitespace-only text as icon-only', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
slots: {
|
||||
icon: '<svg></svg>',
|
||||
default: ' ',
|
||||
},
|
||||
})
|
||||
const contentSpan = wrapper.find('button > span')
|
||||
expect(contentSpan.classes()).not.toContain('gap-2')
|
||||
expect(wrapper.find('.os-button__icon').classes()).toContain('-mr-1')
|
||||
})
|
||||
|
||||
it('does not add gap-2 without icon', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
slots: { default: 'Click me' },
|
||||
})
|
||||
const contentSpan = wrapper.find('button > span')
|
||||
expect(contentSpan.classes()).not.toContain('gap-2')
|
||||
})
|
||||
|
||||
it('renders without icon slot (backward compat)', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
slots: { default: 'Click me' },
|
||||
})
|
||||
expect(wrapper.find('.os-button__icon').exists()).toBeFalsy()
|
||||
expect(wrapper.text()).toBe('Click me')
|
||||
})
|
||||
|
||||
it('renders icon-only button without text', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
slots: { icon: '<svg></svg>' },
|
||||
})
|
||||
expect(wrapper.find('.os-button__icon').exists()).toBeTruthy()
|
||||
expect(wrapper.text()).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('circle prop', () => {
|
||||
it('renders as round button with rounded-full and p-0', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { circle: true },
|
||||
slots: { icon: '<svg></svg>' },
|
||||
attrs: { 'aria-label': 'Add' },
|
||||
})
|
||||
expect(wrapper.classes()).toContain('rounded-full')
|
||||
expect(wrapper.classes()).toContain('p-0')
|
||||
})
|
||||
|
||||
it('applies w-[36px] width for md size (default)', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { circle: true },
|
||||
slots: { icon: '<svg></svg>' },
|
||||
attrs: { 'aria-label': 'Add' },
|
||||
})
|
||||
expect(wrapper.classes()).toContain('w-[36px]')
|
||||
})
|
||||
|
||||
it('applies w-[26px] width for sm size', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { circle: true, size: 'sm' },
|
||||
slots: { icon: '<svg></svg>' },
|
||||
attrs: { 'aria-label': 'Add' },
|
||||
})
|
||||
expect(wrapper.classes()).toContain('w-[26px]')
|
||||
})
|
||||
|
||||
it('applies w-12 width for lg size', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { circle: true, size: 'lg' },
|
||||
slots: { icon: '<svg></svg>' },
|
||||
attrs: { 'aria-label': 'Add' },
|
||||
})
|
||||
expect(wrapper.classes()).toContain('w-12')
|
||||
})
|
||||
|
||||
it('applies w-14 width for xl size', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { circle: true, size: 'xl' },
|
||||
slots: { icon: '<svg></svg>' },
|
||||
attrs: { 'aria-label': 'Add' },
|
||||
})
|
||||
expect(wrapper.classes()).toContain('w-14')
|
||||
})
|
||||
|
||||
it('is combinable with primary variant', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { circle: true, variant: 'primary' },
|
||||
slots: { icon: '<svg></svg>' },
|
||||
attrs: { 'aria-label': 'Add' },
|
||||
})
|
||||
expect(wrapper.classes()).toContain('rounded-full')
|
||||
expect(wrapper.classes()).toContain('bg-[var(--color-primary)]')
|
||||
})
|
||||
|
||||
it('is combinable with ghost appearance', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { circle: true, appearance: 'ghost', variant: 'primary' },
|
||||
slots: { icon: '<svg></svg>' },
|
||||
attrs: { 'aria-label': 'Add' },
|
||||
})
|
||||
expect(wrapper.classes()).toContain('rounded-full')
|
||||
expect(wrapper.classes()).toContain('bg-transparent')
|
||||
})
|
||||
|
||||
it('icon has no negative margin in circle mode', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { circle: true },
|
||||
slots: { icon: '<svg></svg>' },
|
||||
attrs: { 'aria-label': 'Add' },
|
||||
})
|
||||
const iconWrapper = wrapper.find('.os-button__icon')
|
||||
expect(iconWrapper.classes()).not.toContain('-ml-1')
|
||||
expect(iconWrapper.classes()).not.toContain('-mr-1')
|
||||
})
|
||||
|
||||
it('uses gap-1 for circle with icon and text', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { circle: true },
|
||||
slots: {
|
||||
icon: '<svg></svg>',
|
||||
default: 'Add',
|
||||
},
|
||||
})
|
||||
const contentSpan = wrapper.find('button > span')
|
||||
expect(contentSpan.classes()).toContain('gap-1')
|
||||
expect(contentSpan.classes()).not.toContain('gap-2')
|
||||
})
|
||||
|
||||
it('does not apply circle classes when circle is false', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { circle: false },
|
||||
slots: { default: 'Click me' },
|
||||
})
|
||||
expect(wrapper.classes()).not.toContain('rounded-full')
|
||||
expect(wrapper.classes()).not.toContain('p-0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('loading prop', () => {
|
||||
it('renders spinner SVG when loading=true', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { loading: true },
|
||||
slots: { default: 'Save' },
|
||||
})
|
||||
expect(wrapper.find('.os-button__spinner').exists()).toBeTruthy()
|
||||
expect(wrapper.find('svg').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('disables button when loading=true', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { loading: true },
|
||||
slots: { default: 'Save' },
|
||||
})
|
||||
expect(wrapper.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('sets aria-busy="true" when loading', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { loading: true },
|
||||
slots: { default: 'Save' },
|
||||
})
|
||||
expect(wrapper.attributes('aria-busy')).toBe('true')
|
||||
})
|
||||
|
||||
it('keeps content visible when loading', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { loading: true },
|
||||
slots: { default: 'Save' },
|
||||
})
|
||||
const contentSpan = wrapper.find('span')
|
||||
expect(contentSpan.classes()).not.toContain('opacity-0')
|
||||
expect(wrapper.text()).toContain('Save')
|
||||
})
|
||||
|
||||
it('does not render spinner when loading=false (default)', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
slots: { default: 'Save' },
|
||||
})
|
||||
expect(wrapper.find('.os-button__spinner').exists()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('does not set aria-busy when not loading', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
slots: { default: 'Save' },
|
||||
})
|
||||
expect(wrapper.attributes('aria-busy')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('loading + disabled: button remains disabled', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { loading: true, disabled: true },
|
||||
slots: { default: 'Save' },
|
||||
})
|
||||
expect(wrapper.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('does not emit click when loading', async () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { loading: true },
|
||||
slots: { default: 'Save' },
|
||||
})
|
||||
await wrapper.trigger('click')
|
||||
expect(wrapper.emitted('click')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('renders spinner inside icon wrapper when icon is present', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { loading: true },
|
||||
slots: {
|
||||
icon: '<svg data-testid="icon"></svg>',
|
||||
default: 'Save',
|
||||
},
|
||||
})
|
||||
const iconWrapper = wrapper.find('.os-button__icon')
|
||||
expect(iconWrapper.exists()).toBeTruthy()
|
||||
expect(iconWrapper.find('.os-button__spinner').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('keeps icon visible when loading with icon', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { loading: true },
|
||||
slots: {
|
||||
icon: '<svg data-testid="icon"></svg>',
|
||||
default: 'Save',
|
||||
},
|
||||
})
|
||||
const iconWrapper = wrapper.find('.os-button__icon')
|
||||
expect(iconWrapper.classes()).not.toContain('[&>*]:invisible')
|
||||
})
|
||||
|
||||
it('renders spinner as direct button child when no icon', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { loading: true },
|
||||
slots: { default: 'Save' },
|
||||
})
|
||||
// Spinner is a direct child of button, not inside content wrapper
|
||||
const spinner = wrapper.find('button > .os-button__spinner')
|
||||
expect(spinner.exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('does not render button-level spinner when icon is present', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { loading: true },
|
||||
slots: {
|
||||
icon: '<svg></svg>',
|
||||
default: 'Save',
|
||||
},
|
||||
})
|
||||
// No spinner as direct child of button — it's inside the icon wrapper
|
||||
const buttonSpinner = wrapper.find('button > .os-button__spinner')
|
||||
expect(buttonSpinner.exists()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('keeps icon visible and shows spinner for icon-only loading', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { loading: true },
|
||||
slots: { icon: '<svg data-testid="icon"></svg>' },
|
||||
})
|
||||
const iconWrapper = wrapper.find('.os-button__icon')
|
||||
expect(iconWrapper.exists()).toBeTruthy()
|
||||
expect(iconWrapper.find('[data-testid="icon"]').exists()).toBeTruthy()
|
||||
expect(iconWrapper.find('.os-button__spinner').exists()).toBeTruthy()
|
||||
expect(iconWrapper.classes()).not.toContain('[&>*]:invisible')
|
||||
})
|
||||
|
||||
it('works with circle prop', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { loading: true, circle: true },
|
||||
slots: { icon: '<svg></svg>' },
|
||||
attrs: { 'aria-label': 'Add' },
|
||||
})
|
||||
expect(wrapper.classes()).toContain('rounded-full')
|
||||
expect(wrapper.find('.os-button__spinner').exists()).toBeTruthy()
|
||||
expect(wrapper.attributes('disabled')).toBeDefined()
|
||||
expect(wrapper.attributes('aria-busy')).toBe('true')
|
||||
})
|
||||
})
|
||||
|
||||
describe('keyboard accessibility', () => {
|
||||
it('renders as native button element for keyboard support', () => {
|
||||
const wrapper = mount(OsButton)
|
||||
@ -152,6 +515,15 @@ describe('osButton', () => {
|
||||
expect(wrapper.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('icon-only button is focusable with aria-label', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
slots: { icon: '<svg></svg>' },
|
||||
attrs: { 'aria-label': 'Close' },
|
||||
})
|
||||
expect(wrapper.attributes('aria-label')).toBe('Close')
|
||||
expect(wrapper.attributes('tabindex')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('can receive focus programmatically', () => {
|
||||
const wrapper = mount(OsButton, { attachTo: document.body })
|
||||
const button = wrapper.element as HTMLButtonElement
|
||||
|
||||
@ -1,7 +1,36 @@
|
||||
import { computed, h } from 'vue'
|
||||
|
||||
import OsButton from './OsButton.vue'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
/**
|
||||
* Inline SVG icons for demo purposes (from Heroicons).
|
||||
* In real usage, the webapp passes its own BaseIcon component.
|
||||
*/
|
||||
const CheckIcon = () =>
|
||||
h('svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 20 20', fill: 'currentColor' }, [
|
||||
h('path', {
|
||||
'fill-rule': 'evenodd',
|
||||
d: 'M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z',
|
||||
'clip-rule': 'evenodd',
|
||||
}),
|
||||
])
|
||||
|
||||
const CloseIcon = () =>
|
||||
h('svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 20 20', fill: 'currentColor' }, [
|
||||
h('path', {
|
||||
d: 'M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z',
|
||||
}),
|
||||
])
|
||||
|
||||
const PlusIcon = () =>
|
||||
h('svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 20 20', fill: 'currentColor' }, [
|
||||
h('path', {
|
||||
d: 'M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z',
|
||||
}),
|
||||
])
|
||||
|
||||
const meta: Meta<typeof OsButton> = {
|
||||
title: 'Components/OsButton',
|
||||
component: OsButton,
|
||||
@ -11,7 +40,27 @@ const meta: Meta<typeof OsButton> = {
|
||||
export default meta
|
||||
type Story = StoryObj<typeof OsButton>
|
||||
|
||||
export const Playground: Story = {
|
||||
/** Custom args for Playground (icon selector + label are not real component props) */
|
||||
interface PlaygroundArgs {
|
||||
variant: string
|
||||
appearance: string
|
||||
size: string
|
||||
fullWidth: boolean
|
||||
circle: boolean
|
||||
disabled: boolean
|
||||
loading: boolean
|
||||
icon: string
|
||||
label: string
|
||||
}
|
||||
|
||||
const iconMap: Record<string, (() => ReturnType<typeof h>) | null> = {
|
||||
none: null,
|
||||
check: CheckIcon,
|
||||
close: CloseIcon,
|
||||
plus: PlusIcon,
|
||||
}
|
||||
|
||||
export const Playground: StoryObj<PlaygroundArgs> = {
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
@ -28,23 +77,51 @@ export const Playground: Story = {
|
||||
fullWidth: {
|
||||
control: 'boolean',
|
||||
},
|
||||
circle: {
|
||||
control: 'boolean',
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
},
|
||||
loading: {
|
||||
control: 'boolean',
|
||||
},
|
||||
icon: {
|
||||
control: 'select',
|
||||
options: Object.keys(iconMap),
|
||||
},
|
||||
label: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
variant: 'primary',
|
||||
appearance: 'filled',
|
||||
size: 'md',
|
||||
fullWidth: false,
|
||||
circle: false,
|
||||
disabled: false,
|
||||
loading: false,
|
||||
icon: 'none',
|
||||
label: 'Button',
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { OsButton },
|
||||
setup() {
|
||||
return { args }
|
||||
const buttonProps = computed(() => {
|
||||
const { icon: _icon, label: _label, ...rest } = args
|
||||
return rest
|
||||
})
|
||||
const IconComponent = computed(() => iconMap[args.icon] ?? null)
|
||||
const label = computed(() => args.label)
|
||||
return { buttonProps, IconComponent, label }
|
||||
},
|
||||
template: '<OsButton v-bind="args">Button</OsButton>',
|
||||
template: `
|
||||
<OsButton v-bind="buttonProps">
|
||||
<template v-if="IconComponent" #icon><component :is="IconComponent" /></template>
|
||||
{{ label }}
|
||||
</OsButton>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
@ -285,3 +362,344 @@ export const FullWidth: Story = {
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Icon: Story = {
|
||||
render: () => ({
|
||||
components: { OsButton, CheckIcon, PlusIcon, CloseIcon },
|
||||
template: `
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<OsButton variant="primary">
|
||||
<template #icon><CheckIcon /></template>
|
||||
Save
|
||||
</OsButton>
|
||||
<OsButton variant="success">
|
||||
<template #icon><CheckIcon /></template>
|
||||
Confirm
|
||||
</OsButton>
|
||||
<OsButton variant="default">
|
||||
<template #icon><PlusIcon /></template>
|
||||
Add
|
||||
</OsButton>
|
||||
<OsButton variant="danger">
|
||||
<template #icon><CloseIcon /></template>
|
||||
Delete
|
||||
</OsButton>
|
||||
<OsButton variant="info">
|
||||
<template #icon><PlusIcon /></template>
|
||||
Create
|
||||
</OsButton>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const IconOnly: Story = {
|
||||
render: () => ({
|
||||
components: { OsButton, CloseIcon, PlusIcon, CheckIcon },
|
||||
template: `
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<OsButton variant="danger" aria-label="Close">
|
||||
<template #icon><CloseIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton variant="primary" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton variant="success" aria-label="Confirm">
|
||||
<template #icon><CheckIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton variant="default" aria-label="Close" appearance="outline">
|
||||
<template #icon><CloseIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton variant="primary" aria-label="Add" appearance="ghost">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const IconSizes: Story = {
|
||||
render: () => ({
|
||||
components: { OsButton, CheckIcon },
|
||||
template: `
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<OsButton size="sm" variant="primary">
|
||||
<template #icon><CheckIcon /></template>
|
||||
Small
|
||||
</OsButton>
|
||||
<OsButton size="md" variant="primary">
|
||||
<template #icon><CheckIcon /></template>
|
||||
Medium
|
||||
</OsButton>
|
||||
<OsButton size="lg" variant="primary">
|
||||
<template #icon><CheckIcon /></template>
|
||||
Large
|
||||
</OsButton>
|
||||
<OsButton size="xl" variant="primary">
|
||||
<template #icon><CheckIcon /></template>
|
||||
Extra Large
|
||||
</OsButton>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const IconAppearances: Story = {
|
||||
render: () => ({
|
||||
components: { OsButton, CheckIcon },
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Filled</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<OsButton appearance="filled" variant="primary">
|
||||
<template #icon><CheckIcon /></template>
|
||||
Primary
|
||||
</OsButton>
|
||||
<OsButton appearance="filled" variant="danger">
|
||||
<template #icon><CheckIcon /></template>
|
||||
Danger
|
||||
</OsButton>
|
||||
<OsButton appearance="filled" variant="success">
|
||||
<template #icon><CheckIcon /></template>
|
||||
Success
|
||||
</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Outline</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<OsButton appearance="outline" variant="primary">
|
||||
<template #icon><CheckIcon /></template>
|
||||
Primary
|
||||
</OsButton>
|
||||
<OsButton appearance="outline" variant="danger">
|
||||
<template #icon><CheckIcon /></template>
|
||||
Danger
|
||||
</OsButton>
|
||||
<OsButton appearance="outline" variant="success">
|
||||
<template #icon><CheckIcon /></template>
|
||||
Success
|
||||
</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Ghost</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<OsButton appearance="ghost" variant="primary">
|
||||
<template #icon><CheckIcon /></template>
|
||||
Primary
|
||||
</OsButton>
|
||||
<OsButton appearance="ghost" variant="danger">
|
||||
<template #icon><CheckIcon /></template>
|
||||
Danger
|
||||
</OsButton>
|
||||
<OsButton appearance="ghost" variant="success">
|
||||
<template #icon><CheckIcon /></template>
|
||||
Success
|
||||
</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Circle: Story = {
|
||||
render: () => ({
|
||||
components: { OsButton, PlusIcon, CloseIcon, CheckIcon },
|
||||
template: `
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<OsButton circle variant="default" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle variant="primary" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle variant="secondary" aria-label="Confirm">
|
||||
<template #icon><CheckIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle variant="danger" aria-label="Close">
|
||||
<template #icon><CloseIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle variant="warning" aria-label="Close">
|
||||
<template #icon><CloseIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle variant="success" aria-label="Confirm">
|
||||
<template #icon><CheckIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle variant="info" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const CircleSizes: Story = {
|
||||
render: () => ({
|
||||
components: { OsButton, PlusIcon },
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Small (26px)</h3>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<OsButton circle size="sm" variant="primary" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle size="sm" variant="danger" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle size="sm" variant="default" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Medium (36px)</h3>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<OsButton circle size="md" variant="primary" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle size="md" variant="danger" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle size="md" variant="default" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Large (48px)</h3>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<OsButton circle size="lg" variant="primary" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle size="lg" variant="danger" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle size="lg" variant="default" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Extra Large (56px)</h3>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<OsButton circle size="xl" variant="primary" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle size="xl" variant="danger" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle size="xl" variant="default" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const CircleAppearances: Story = {
|
||||
render: () => ({
|
||||
components: { OsButton, PlusIcon, CloseIcon, CheckIcon },
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Filled</h3>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<OsButton circle appearance="filled" variant="primary" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle appearance="filled" variant="danger" aria-label="Close">
|
||||
<template #icon><CloseIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle appearance="filled" variant="success" aria-label="Confirm">
|
||||
<template #icon><CheckIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle appearance="filled" variant="default" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Outline</h3>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<OsButton circle appearance="outline" variant="primary" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle appearance="outline" variant="danger" aria-label="Close">
|
||||
<template #icon><CloseIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle appearance="outline" variant="success" aria-label="Confirm">
|
||||
<template #icon><CheckIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle appearance="outline" variant="default" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Ghost</h3>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<OsButton circle appearance="ghost" variant="primary" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle appearance="ghost" variant="danger" aria-label="Close">
|
||||
<template #icon><CloseIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle appearance="ghost" variant="success" aria-label="Confirm">
|
||||
<template #icon><CheckIcon /></template>
|
||||
</OsButton>
|
||||
<OsButton circle appearance="ghost" variant="default" aria-label="Add">
|
||||
<template #icon><PlusIcon /></template>
|
||||
</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Loading: Story = {
|
||||
render: () => ({
|
||||
components: { OsButton, CheckIcon },
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Filled</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<OsButton loading appearance="filled" variant="default">Default</OsButton>
|
||||
<OsButton loading appearance="filled" variant="primary">Primary</OsButton>
|
||||
<OsButton loading appearance="filled" variant="danger">Danger</OsButton>
|
||||
<OsButton loading appearance="filled" variant="success">Success</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Outline</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<OsButton loading appearance="outline" variant="default">Default</OsButton>
|
||||
<OsButton loading appearance="outline" variant="primary">Primary</OsButton>
|
||||
<OsButton loading appearance="outline" variant="danger">Danger</OsButton>
|
||||
<OsButton loading appearance="outline" variant="success">Success</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">With Icon</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<OsButton loading variant="primary">
|
||||
<template #icon><CheckIcon /></template>
|
||||
Save
|
||||
</OsButton>
|
||||
<OsButton loading variant="danger">Delete</OsButton>
|
||||
<OsButton loading circle variant="primary" aria-label="Loading">
|
||||
<template #icon><CheckIcon /></template>
|
||||
</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
@ -122,4 +122,85 @@ test.describe('OsButton visual regression', () => {
|
||||
await expect(root.locator('.flex-col').first()).toHaveScreenshot('full-width.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('icon', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--icon&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
await expect(root.locator('.flex')).toHaveScreenshot('icon.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('icon only', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--icon-only&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
await expect(root.locator('.flex')).toHaveScreenshot('icon-only.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('icon sizes', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--icon-sizes&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
await expect(root.locator('.flex')).toHaveScreenshot('icon-sizes.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('icon appearances', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--icon-appearances&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
await expect(root.locator('.flex-col').first()).toHaveScreenshot('icon-appearances.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('circle', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--circle&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
await expect(root.locator('.flex')).toHaveScreenshot('circle.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('circle sizes', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--circle-sizes&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
await expect(root.locator('.flex-col').first()).toHaveScreenshot('circle-sizes.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('circle appearances', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--circle-appearances&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
await expect(root.locator('.flex-col').first()).toHaveScreenshot('circle-appearances.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('loading', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--loading&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
// Pause animations for deterministic screenshots
|
||||
await page.evaluate(() => {
|
||||
document.querySelectorAll('.os-button__spinner').forEach((el) => {
|
||||
;(el as HTMLElement).style.animationPlayState = 'paused'
|
||||
})
|
||||
document.querySelectorAll('.os-button__spinner circle').forEach((el) => {
|
||||
;(el as HTMLElement).style.animationPlayState = 'paused'
|
||||
})
|
||||
})
|
||||
await expect(root.locator('.flex-col').first()).toHaveScreenshot('loading.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
})
|
||||
|
||||
@ -8,9 +8,64 @@
|
||||
import type { ButtonVariants } from './button.variants'
|
||||
import type { PropType } from 'vue-demi'
|
||||
|
||||
/**
|
||||
* Flexible button component with optional icon slot.
|
||||
* @slot default - Button content (text or HTML)
|
||||
* @slot icon - Optional icon (rendered left of text). Use aria-label for icon-only buttons.
|
||||
*/
|
||||
|
||||
type Size = NonNullable<ButtonVariants['size']>
|
||||
|
||||
const CIRCLE_WIDTHS: Record<Size, string> = {
|
||||
sm: 'w-[26px]',
|
||||
md: 'w-[36px]',
|
||||
lg: 'w-12',
|
||||
xl: 'w-14',
|
||||
}
|
||||
|
||||
const ICON_CLASS =
|
||||
'os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current'
|
||||
|
||||
const SPINNER_PX: Record<Size, number> = { sm: 24, md: 32, lg: 40, xl: 46 }
|
||||
|
||||
const SVG_ATTRS = {
|
||||
viewBox: '0 0 50 50',
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
'aria-hidden': 'true',
|
||||
}
|
||||
|
||||
const CIRCLE_ATTRS = {
|
||||
cx: '25',
|
||||
cy: '25',
|
||||
r: '20',
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
'stroke-width': '4',
|
||||
'stroke-linecap': 'round',
|
||||
}
|
||||
|
||||
const CIRCLE_STYLE =
|
||||
'transform-origin:25px 25px;animation:os-spinner-rotate 16s linear infinite,os-spinner-dash 1.5s ease-in-out infinite'
|
||||
|
||||
function vueAttrs(attrs: Record<string, string>, style?: string) {
|
||||
/* v8 ignore start -- Vue 2 branch tested in webapp Jest tests */
|
||||
return isVue2 ? { attrs, ...(style && { style }) } : { ...attrs, ...(style && { style }) }
|
||||
/* v8 ignore stop */
|
||||
}
|
||||
|
||||
function createSpinner(px: number, center: string) {
|
||||
const svg = h('svg', vueAttrs(SVG_ATTRS, 'width:100%;height:100%;overflow:hidden'), [
|
||||
h('circle', vueAttrs(CIRCLE_ATTRS, CIRCLE_STYLE)),
|
||||
])
|
||||
return h(
|
||||
'span',
|
||||
{ class: `os-button__spinner absolute ${center}`, style: `width:${px}px;height:${px}px` },
|
||||
[svg],
|
||||
)
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OsButton',
|
||||
// In Vue 2, inheritAttrs must be false to manually forward attrs
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
variant: {
|
||||
@ -37,61 +92,131 @@
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
circle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { slots, attrs }) {
|
||||
const classes = computed(() =>
|
||||
cn(
|
||||
buttonVariants({
|
||||
variant: props.variant,
|
||||
appearance: props.appearance,
|
||||
size: props.size,
|
||||
fullWidth: props.fullWidth,
|
||||
}),
|
||||
),
|
||||
const variantClasses = computed(() =>
|
||||
buttonVariants({
|
||||
variant: props.variant,
|
||||
appearance: props.appearance,
|
||||
size: props.size,
|
||||
fullWidth: props.fullWidth,
|
||||
}),
|
||||
)
|
||||
|
||||
// Get component instance for Vue 2 $listeners access
|
||||
const instance = getCurrentInstance()
|
||||
/* v8 ignore start -- Vue 2 only */
|
||||
const instance = isVue2 ? getCurrentInstance() : null
|
||||
/* v8 ignore stop */
|
||||
|
||||
return () => {
|
||||
const children = slots.default?.()
|
||||
const iconContent = slots.icon?.()
|
||||
const defaultContent = slots.default?.()
|
||||
const hasIcon = iconContent && iconContent.length > 0
|
||||
const hasText =
|
||||
defaultContent?.some((node: unknown) => {
|
||||
const children = (node as Record<string, unknown>).children
|
||||
return typeof children !== 'string' || children.trim().length > 0
|
||||
}) ?? false
|
||||
|
||||
const size = props.size as Size
|
||||
const spinnerPx = SPINNER_PX[size] // eslint-disable-line security/detect-object-injection
|
||||
const isSmall = props.circle || size === 'sm'
|
||||
const isLoading = props.loading
|
||||
const isDisabled = props.disabled || isLoading
|
||||
|
||||
// --- Build inner children (icon + text) ---
|
||||
const innerChildren: ReturnType<typeof h>[] = []
|
||||
|
||||
if (hasIcon) {
|
||||
const iconChildren = isLoading
|
||||
? [
|
||||
/* v8 ignore next -- iconContent guaranteed truthy by hasIcon check */
|
||||
...(iconContent || []),
|
||||
createSpinner(spinnerPx, 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'),
|
||||
]
|
||||
: iconContent
|
||||
innerChildren.push(
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
class: cn(
|
||||
ICON_CLASS,
|
||||
!isSmall && (hasText ? '-ml-1' : '-ml-1 -mr-1'),
|
||||
isLoading && 'relative overflow-visible',
|
||||
),
|
||||
},
|
||||
iconChildren,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (defaultContent && hasText) {
|
||||
innerChildren.push(...(defaultContent as ReturnType<typeof h>[]))
|
||||
}
|
||||
|
||||
const contentWrapper = h(
|
||||
'span',
|
||||
{
|
||||
class: cn(
|
||||
'inline-flex items-center',
|
||||
hasIcon && hasText && (isSmall ? 'gap-1' : 'gap-2'),
|
||||
),
|
||||
},
|
||||
innerChildren,
|
||||
)
|
||||
|
||||
const children =
|
||||
isLoading && !hasIcon
|
||||
? [contentWrapper, createSpinner(spinnerPx, 'inset-0 m-auto')]
|
||||
: [contentWrapper]
|
||||
|
||||
const buttonClass = cn(
|
||||
variantClasses.value,
|
||||
props.circle && 'rounded-full p-0',
|
||||
props.circle && CIRCLE_WIDTHS[size], // eslint-disable-line security/detect-object-injection
|
||||
)
|
||||
|
||||
const buttonData = {
|
||||
type: props.type,
|
||||
disabled: isDisabled || undefined,
|
||||
'data-variant': props.variant,
|
||||
'data-appearance': props.appearance,
|
||||
'aria-busy': isLoading || undefined,
|
||||
}
|
||||
|
||||
/* v8 ignore start -- Vue 2 branch tested in webapp Jest tests */
|
||||
if (isVue2) {
|
||||
// Vue 2: separate attrs and on (listeners)
|
||||
// $listeners contains event handlers like @click
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const proxy = instance?.proxy as any
|
||||
const listeners = proxy?.$listeners || {}
|
||||
// In Vue 2, class/style are not in $attrs - access via $vnode
|
||||
const parentClass = proxy?.$vnode?.data?.staticClass || ''
|
||||
const parentDynClass = proxy?.$vnode?.data?.class
|
||||
|
||||
return h(
|
||||
'button',
|
||||
{
|
||||
class: [classes.value, parentClass, parentDynClass].filter(Boolean),
|
||||
attrs: {
|
||||
type: props.type,
|
||||
disabled: props.disabled || undefined,
|
||||
'data-appearance': props.appearance,
|
||||
...attrs,
|
||||
},
|
||||
class: [buttonClass, parentClass, parentDynClass].filter(Boolean),
|
||||
attrs: { ...buttonData, ...attrs },
|
||||
on: listeners,
|
||||
},
|
||||
children,
|
||||
)
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
// Vue 3: flat props, attrs includes listeners
|
||||
// Extract class from attrs to merge instead of overwrite
|
||||
|
||||
const { class: attrClass, ...restAttrs } = attrs as Record<string, unknown>
|
||||
return h(
|
||||
'button',
|
||||
{
|
||||
type: props.type,
|
||||
disabled: props.disabled,
|
||||
'data-appearance': props.appearance,
|
||||
class: cn(classes.value, (attrClass as string) || ''),
|
||||
...buttonData,
|
||||
class: cn(buttonClass, attrClass || ''),
|
||||
...restAttrs,
|
||||
},
|
||||
children,
|
||||
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 18 KiB |
@ -45,7 +45,7 @@ export const buttonVariants = cva(
|
||||
],
|
||||
outline: [
|
||||
'bg-transparent shadow-none',
|
||||
// Disabled: gray border and text
|
||||
// Disabled: light gray border and gray text
|
||||
'disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)]',
|
||||
],
|
||||
ghost: [
|
||||
@ -55,10 +55,10 @@ export const buttonVariants = cva(
|
||||
],
|
||||
},
|
||||
size: {
|
||||
sm: 'h-[26px] px-[8px] py-0 text-[12px] leading-[normal] tracking-[0.6px] rounded-[5px] overflow-hidden whitespace-nowrap align-middle', // base-button --small
|
||||
md: 'h-[36px] px-[16px] py-0 text-[15px] leading-[normal] rounded-[5px] align-middle',
|
||||
lg: 'h-12 px-6 py-3 text-lg',
|
||||
xl: 'h-14 px-8 py-4 text-xl',
|
||||
sm: 'h-[26px] min-w-[26px] px-[8px] py-0 text-[12px] leading-[normal] tracking-[0.6px] rounded-[5px] overflow-hidden whitespace-nowrap align-middle',
|
||||
md: 'h-[36px] min-w-[36px] px-[16px] py-0 text-[15px] leading-[normal] rounded-[5px] align-middle',
|
||||
lg: 'h-12 min-w-12 px-6 py-3 text-lg',
|
||||
xl: 'h-14 min-w-14 px-8 py-4 text-xl',
|
||||
},
|
||||
fullWidth: {
|
||||
true: 'w-full',
|
||||
|
||||
29
packages/ui/src/styles/animations.css
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Spinner animations for loading state
|
||||
*/
|
||||
@keyframes os-spinner-dash {
|
||||
0% {
|
||||
stroke-dasharray: 1, 150;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -35;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -124;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes os-spinner-rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(2160deg);
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,7 @@
|
||||
*/
|
||||
@import "tailwindcss/theme";
|
||||
@import "tailwindcss/utilities";
|
||||
@import "./animations.css";
|
||||
|
||||
/* Scan component files for utility classes */
|
||||
@source "../components/**/*.vue";
|
||||
|
||||
@ -1,20 +1,27 @@
|
||||
<template>
|
||||
<div class="action-button">
|
||||
<base-button
|
||||
<os-button
|
||||
variant="primary"
|
||||
:appearance="filled ? 'filled' : 'outline'"
|
||||
:loading="loading"
|
||||
:disabled="disabled"
|
||||
:icon="icon"
|
||||
:aria-label="text"
|
||||
:filled="filled"
|
||||
circle
|
||||
@click="click"
|
||||
/>
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon :name="icon" />
|
||||
</template>
|
||||
</os-button>
|
||||
<div class="count">{{ count }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
|
||||
export default {
|
||||
components: { OsButton },
|
||||
props: {
|
||||
count: { type: Number, required: true },
|
||||
text: { type: String, required: true },
|
||||
|
||||
@ -29,18 +29,18 @@ describe('FollowButton.vue', () => {
|
||||
|
||||
it('renders button and text', () => {
|
||||
expect(mocks.$t).toHaveBeenCalledWith('followButton.follow')
|
||||
expect(wrapper.findAll('.base-button')).toHaveLength(1)
|
||||
expect(wrapper.findAll('[data-test="follow-btn"]')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('renders button and text when followed', () => {
|
||||
propsData.isFollowed = true
|
||||
wrapper = Wrapper()
|
||||
expect(mocks.$t).toHaveBeenCalledWith('followButton.following')
|
||||
expect(wrapper.findAll('.base-button')).toHaveLength(1)
|
||||
expect(wrapper.findAll('[data-test="follow-btn"]')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it.skip('toggle the button', async () => {
|
||||
wrapper.find('.base-button').trigger('click') // This does not work since @click.prevent is used
|
||||
wrapper.find('[data-test="follow-btn"]').trigger('click') // This does not work since @click.prevent is used
|
||||
expect(wrapper.vm.isFollowed).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,24 +1,29 @@
|
||||
<template>
|
||||
<base-button
|
||||
class="track-button"
|
||||
<os-button
|
||||
data-test="follow-btn"
|
||||
:variant="isFollowed && hovered ? 'danger' : 'primary'"
|
||||
:appearance="isFollowed && !hovered ? 'filled' : 'outline'"
|
||||
:disabled="disabled || !followId"
|
||||
:loading="loading"
|
||||
:icon="icon"
|
||||
:filled="isFollowed && !hovered"
|
||||
:danger="isFollowed && hovered"
|
||||
@mouseenter.native="onHover"
|
||||
@mouseleave.native="hovered = false"
|
||||
full-width
|
||||
@mouseenter="onHover"
|
||||
@mouseleave="hovered = false"
|
||||
@click.prevent="toggle"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon :name="icon" />
|
||||
</template>
|
||||
{{ label }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { followUserMutation, unfollowUserMutation } from '~/graphql/User'
|
||||
|
||||
export default {
|
||||
name: 'HcFollowButton',
|
||||
components: { OsButton },
|
||||
props: {
|
||||
followId: { type: String, default: null },
|
||||
isFollowed: { type: Boolean, default: false },
|
||||
@ -82,10 +87,3 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.track-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,26 +1,31 @@
|
||||
<template>
|
||||
<base-button
|
||||
class="join-leave-button"
|
||||
<os-button
|
||||
data-test="join-leave-btn"
|
||||
:variant="isMember && hovered ? 'danger' : 'primary'"
|
||||
:appearance="filled || (isMember && !hovered) ? 'filled' : 'outline'"
|
||||
:disabled="disabled"
|
||||
:loading="localLoading"
|
||||
:icon="icon"
|
||||
:filled="filled || (isMember && !hovered)"
|
||||
:danger="isMember && hovered"
|
||||
full-width
|
||||
v-tooltip="tooltip"
|
||||
@mouseenter.native="onHover"
|
||||
@mouseleave.native="hovered = false"
|
||||
@mouseenter="onHover"
|
||||
@mouseleave="hovered = false"
|
||||
@click.prevent="toggle"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon :name="icon" />
|
||||
</template>
|
||||
{{ label }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations } from 'vuex'
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { joinGroupMutation, leaveGroupMutation } from '~/graphql/groups'
|
||||
|
||||
export default {
|
||||
name: 'JoinLeaveButton',
|
||||
components: { OsButton },
|
||||
props: {
|
||||
group: { type: Object, required: true },
|
||||
userId: { type: String, required: true },
|
||||
@ -146,10 +151,3 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.join-leave-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`JoinLeaveButton.vue shallowMount renders 1`] = `
|
||||
<base-button-stub icon="plus" size="regular" type="button" class="join-leave-button has-tooltip" data-original-title="null">
|
||||
<os-button-stub variant="primary" appearance="outline" size="md" fullwidth="true" type="button" data-original-title="null" class=" has-tooltip">
|
||||
group.joinLeaveButton.join
|
||||
</base-button-stub>
|
||||
</os-button-stub>
|
||||
`;
|
||||
|
||||
@ -1,25 +1,27 @@
|
||||
<template>
|
||||
<section class="categories-select">
|
||||
<base-button
|
||||
<os-button
|
||||
v-for="category in sortCategories(categories)"
|
||||
:key="category.id"
|
||||
:data-test="categoryButtonsId(category.id)"
|
||||
@click="toggleCategory(category.id)"
|
||||
:filled="isActive(category.id)"
|
||||
variant="primary"
|
||||
:appearance="isActive(category.id) ? 'filled' : 'outline'"
|
||||
:disabled="isDisabled(category.id)"
|
||||
:icon="category.icon"
|
||||
size="small"
|
||||
size="sm"
|
||||
v-tooltip="{
|
||||
content: $t(`contribution.category.description.${category.slug}`),
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
>
|
||||
<template #icon><base-icon :name="category.icon" /></template>
|
||||
{{ $t(`contribution.category.name.${category.slug}`) }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { CATEGORIES_MAX } from '~/constants/categories.js'
|
||||
import xor from 'lodash/xor'
|
||||
import SortCategories from '~/mixins/sortCategoriesMixin.js'
|
||||
@ -31,6 +33,7 @@ export default {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
components: { OsButton },
|
||||
mixins: [SortCategories, GetCategories],
|
||||
props: {
|
||||
existingCategoryIds: { type: Array, default: () => [] },
|
||||
@ -83,7 +86,7 @@ export default {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> .base-button {
|
||||
> button {
|
||||
margin-right: $space-xx-small;
|
||||
margin-bottom: $space-xx-small;
|
||||
}
|
||||
|
||||
@ -2,7 +2,19 @@
|
||||
<div class="add-chat-room-by-user-search">
|
||||
<ds-flex class="headline">
|
||||
<h2 class="title">{{ $t('chat.addRoomHeadline') }}</h2>
|
||||
<base-button class="close-button" icon="close" circle size="small" @click="closeUserSearch" />
|
||||
<os-button
|
||||
class="close-button"
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
size="sm"
|
||||
:aria-label="$t('actions.close')"
|
||||
@click="closeUserSearch"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="close" />
|
||||
</template>
|
||||
</os-button>
|
||||
</ds-flex>
|
||||
<ds-space margin-bottom="small" />
|
||||
<ds-space>
|
||||
@ -12,11 +24,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import SelectUserSearch from '~/components/generic/SelectUserSearch/SelectUserSearch'
|
||||
|
||||
export default {
|
||||
name: 'AddChatRoomByUserSearch',
|
||||
components: {
|
||||
OsButton,
|
||||
SelectUserSearch,
|
||||
},
|
||||
props: {
|
||||
|
||||
@ -35,18 +35,34 @@
|
||||
<ds-flex v-if="singleRoom">
|
||||
<ds-flex-item centered class="single-chat-bubble">
|
||||
<nuxt-link :to="{ name: 'chat' }">
|
||||
<base-button icon="expand" size="small" circle />
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
size="sm"
|
||||
:aria-label="$t('chat.expandChat')"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="expand" />
|
||||
</template>
|
||||
</os-button>
|
||||
</nuxt-link>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item centered>
|
||||
<div class="vac-svg-button vac-room-options">
|
||||
<slot name="menu-icon">
|
||||
<base-button
|
||||
icon="close"
|
||||
size="small"
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
size="sm"
|
||||
:aria-label="$t('chat.closeChat')"
|
||||
@click="$emit('close-single-room', true)"
|
||||
/>
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="close" />
|
||||
</template>
|
||||
</os-button>
|
||||
</slot>
|
||||
</div>
|
||||
</ds-flex-item>
|
||||
@ -89,6 +105,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { roomQuery, createRoom, unreadRoomsQuery } from '~/graphql/Rooms'
|
||||
import {
|
||||
messageQuery,
|
||||
@ -101,6 +118,7 @@ import { mapGetters, mapMutations } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'Chat',
|
||||
components: { OsButton },
|
||||
props: {
|
||||
theme: {
|
||||
type: String,
|
||||
|
||||
@ -1,19 +1,24 @@
|
||||
<template>
|
||||
<nuxt-link class="chat-notification-menu" :to="{ name: 'chat' }">
|
||||
<base-button
|
||||
ghost
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
:aria-label="$t('header.chats.tooltip')"
|
||||
v-tooltip="{
|
||||
content: $t('header.chats.tooltip'),
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
>
|
||||
<counter-icon icon="chat-bubble" :count="unreadRoomCount" danger />
|
||||
</base-button>
|
||||
<template #icon>
|
||||
<counter-icon icon="chat-bubble" :count="unreadRoomCount" danger />
|
||||
</template>
|
||||
</os-button>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
||||
import { unreadRoomsQuery, roomCountUpdated } from '~/graphql/Rooms'
|
||||
@ -21,6 +26,7 @@ import { unreadRoomsQuery, roomCountUpdated } from '~/graphql/Rooms'
|
||||
export default {
|
||||
name: 'ChatNotificationMenu',
|
||||
components: {
|
||||
OsButton,
|
||||
CounterIcon,
|
||||
},
|
||||
computed: {
|
||||
|
||||
@ -54,15 +54,21 @@
|
||||
class="shout-button"
|
||||
node-type="Comment"
|
||||
/>
|
||||
<base-button
|
||||
<os-button
|
||||
:title="$t('post.comment.reply')"
|
||||
icon="level-down"
|
||||
:aria-label="$t('post.comment.reply')"
|
||||
class="reply-button"
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
circle
|
||||
size="small"
|
||||
size="sm"
|
||||
v-scroll-to="'.editor'"
|
||||
@click="reply"
|
||||
/>
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="level-down" />
|
||||
</template>
|
||||
</os-button>
|
||||
</div>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
@ -4,16 +4,24 @@
|
||||
<base-card>
|
||||
<hc-editor ref="editor" :users="users" :value="form.content" @input="updateEditorContent" />
|
||||
<div class="buttons">
|
||||
<base-button
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
:disabled="disabled && !update"
|
||||
@click="handleCancel"
|
||||
data-test="cancel-button"
|
||||
>
|
||||
{{ $t('actions.cancel') }}
|
||||
</base-button>
|
||||
<base-button type="submit" :loading="loading" :disabled="disabled || errors" filled>
|
||||
</os-button>
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="filled"
|
||||
type="submit"
|
||||
:loading="loading"
|
||||
:disabled="disabled || !!errors"
|
||||
>
|
||||
{{ $t('post.comment.submit') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</div>
|
||||
</base-card>
|
||||
</template>
|
||||
@ -21,6 +29,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import HcEditor from '~/components/Editor/Editor'
|
||||
import { COMMENT_MIN_LENGTH } from '~/constants/comment'
|
||||
import { minimisedUserQuery } from '~/graphql/User'
|
||||
@ -28,6 +37,7 @@ import CommentMutations from '~/graphql/CommentMutations'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OsButton,
|
||||
HcEditor,
|
||||
},
|
||||
props: {
|
||||
@ -79,54 +89,49 @@ export default {
|
||||
this.closeEditWindow()
|
||||
}
|
||||
},
|
||||
handleSubmit() {
|
||||
let mutateParams
|
||||
if (!this.update) {
|
||||
mutateParams = {
|
||||
mutation: CommentMutations(this.$i18n).CreateComment,
|
||||
variables: {
|
||||
postId: this.post.id,
|
||||
content: this.form.content,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
mutateParams = {
|
||||
mutation: CommentMutations(this.$i18n).UpdateComment,
|
||||
variables: {
|
||||
id: this.comment.id,
|
||||
content: this.form.content,
|
||||
},
|
||||
}
|
||||
}
|
||||
async handleSubmit() {
|
||||
const mutateParams = !this.update
|
||||
? {
|
||||
mutation: CommentMutations(this.$i18n).CreateComment,
|
||||
variables: {
|
||||
postId: this.post.id,
|
||||
content: this.form.content,
|
||||
},
|
||||
}
|
||||
: {
|
||||
mutation: CommentMutations(this.$i18n).UpdateComment,
|
||||
variables: {
|
||||
id: this.comment.id,
|
||||
content: this.form.content,
|
||||
},
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
this.disabled = true
|
||||
this.$apollo
|
||||
.mutate(mutateParams)
|
||||
.then((res) => {
|
||||
this.loading = false
|
||||
if (!this.update) {
|
||||
const {
|
||||
data: { CreateComment },
|
||||
} = res
|
||||
this.$emit('createComment', CreateComment)
|
||||
this.clear()
|
||||
this.$toast.success(this.$t('post.comment.submitted'))
|
||||
this.disabled = false
|
||||
} else {
|
||||
const {
|
||||
data: { UpdateComment },
|
||||
} = res
|
||||
this.$emit('updateComment', UpdateComment)
|
||||
this.$emit('collapse')
|
||||
this.$toast.success(this.$t('post.comment.updated'))
|
||||
this.disabled = false
|
||||
this.closeEditWindow()
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.$toast.error(err.message)
|
||||
})
|
||||
try {
|
||||
const res = await this.$apollo.mutate(mutateParams)
|
||||
if (!this.update) {
|
||||
const {
|
||||
data: { CreateComment },
|
||||
} = res
|
||||
this.$emit('createComment', CreateComment)
|
||||
this.clear()
|
||||
this.$toast.success(this.$t('post.comment.submitted'))
|
||||
} else {
|
||||
const {
|
||||
data: { UpdateComment },
|
||||
} = res
|
||||
this.$emit('updateComment', UpdateComment)
|
||||
this.$emit('collapse')
|
||||
this.$toast.success(this.$t('post.comment.updated'))
|
||||
this.closeEditWindow()
|
||||
}
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
this.disabled = false
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
@ -152,7 +157,7 @@ export default {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
> .base-button {
|
||||
> button {
|
||||
margin-left: $space-x-small;
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ describe('ComponentSlider.vue', () => {
|
||||
})
|
||||
|
||||
it('click on next Button', async () => {
|
||||
await wrapper.find('.base-button[data-test="next-button"]').trigger('click')
|
||||
await wrapper.find('button[data-test="next-button"]').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(propsData.sliderData.sliderSelectorCallback).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -35,26 +35,26 @@
|
||||
:key="slider.name"
|
||||
:class="['Sliders__slider-selection', index === sliderIndex && '--unconfirmed']"
|
||||
>
|
||||
<base-button
|
||||
<os-button
|
||||
:class="['selection-dot']"
|
||||
style="float: left"
|
||||
:bullet="true"
|
||||
size="tiny"
|
||||
type="submit"
|
||||
filled
|
||||
:loading="false"
|
||||
variant="primary"
|
||||
:appearance="index <= sliderIndex ? 'filled' : 'outline'"
|
||||
circle
|
||||
size="sm"
|
||||
:disabled="index > sliderIndex"
|
||||
:aria-label="
|
||||
$t('component-slider.step', { current: index + 1, total: sliderData.sliders.length })
|
||||
"
|
||||
@click="sliderData.sliderSelectorCallback(index)"
|
||||
/>
|
||||
</div>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item>
|
||||
<base-button
|
||||
<os-button
|
||||
:style="multipleSliders && 'float: right'"
|
||||
:icon="sliderData.sliders[sliderIndex].button.icon"
|
||||
type="submit"
|
||||
filled
|
||||
padding
|
||||
variant="primary"
|
||||
appearance="filled"
|
||||
:loading="
|
||||
sliderData.sliders[sliderIndex].button.loading !== undefined
|
||||
? sliderData.sliders[sliderIndex].button.loading
|
||||
@ -64,8 +64,11 @@
|
||||
@click="onNextClick"
|
||||
data-test="next-button"
|
||||
>
|
||||
<template v-if="sliderData.sliders[sliderIndex].button.icon" #icon>
|
||||
<base-icon :name="sliderData.sliders[sliderIndex].button.icon" />
|
||||
</template>
|
||||
{{ $t(sliderData.sliders[sliderIndex].button.titleIdent) }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
|
||||
@ -74,7 +77,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
|
||||
export default {
|
||||
components: { OsButton },
|
||||
name: 'ComponentSlider',
|
||||
props: {
|
||||
sliderData: { type: Object, required: true },
|
||||
@ -107,6 +113,10 @@ export default {
|
||||
&__slider-selection {
|
||||
.selection-dot {
|
||||
margin-right: 2px;
|
||||
height: 18px !important;
|
||||
width: 18px !important;
|
||||
min-height: 18px !important;
|
||||
min-width: 18px !important;
|
||||
}
|
||||
&.--unconfirmed {
|
||||
opacity: $opacity-disabled;
|
||||
|
||||
@ -2,13 +2,19 @@
|
||||
<dropdown class="content-menu" :placement="placement" offset="5">
|
||||
<template #default="{ toggleMenu }">
|
||||
<slot name="button" :toggleMenu="toggleMenu">
|
||||
<base-button
|
||||
<os-button
|
||||
data-test="content-menu-button"
|
||||
icon="ellipsis-v"
|
||||
size="small"
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
size="sm"
|
||||
circle
|
||||
:aria-label="$t('actions.menu')"
|
||||
@click.prevent="toggleMenu()"
|
||||
/>
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="ellipsis-v" />
|
||||
</template>
|
||||
</os-button>
|
||||
</slot>
|
||||
</template>
|
||||
<template #popover="{ toggleMenu }">
|
||||
@ -31,12 +37,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
import PinnedPostsMixin from '~/mixins/pinnedPosts'
|
||||
|
||||
export default {
|
||||
name: 'ContentMenu',
|
||||
components: {
|
||||
OsButton,
|
||||
Dropdown,
|
||||
},
|
||||
mixins: [PinnedPostsMixin],
|
||||
|
||||
@ -2,13 +2,19 @@
|
||||
<dropdown class="group-content-menu" :placement="placement" offset="5">
|
||||
<template #default="{ toggleMenu }">
|
||||
<slot name="button" :toggleMenu="toggleMenu">
|
||||
<base-button
|
||||
icon="ellipsis-v"
|
||||
size="small"
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
size="sm"
|
||||
circle
|
||||
@click.prevent="toggleMenu()"
|
||||
:aria-label="$t('group.contentMenu.menuButton')"
|
||||
data-test="group-menu-button"
|
||||
/>
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="ellipsis-v" />
|
||||
</template>
|
||||
</os-button>
|
||||
</slot>
|
||||
</template>
|
||||
<template #popover="{ toggleMenu }">
|
||||
@ -32,11 +38,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
|
||||
export default {
|
||||
name: 'GroupContentMenu',
|
||||
components: {
|
||||
OsButton,
|
||||
Dropdown,
|
||||
},
|
||||
props: {
|
||||
|
||||
@ -21,18 +21,26 @@ exports[`GroupContentMenu renders as groupProfile when I am the owner 1`] = `
|
||||
trigger="manual"
|
||||
>
|
||||
<button
|
||||
class="base-button --icon-only --circle --small"
|
||||
aria-label="group.contentMenu.menuButton"
|
||||
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[26px] min-w-[26px] text-[12px] leading-[normal] tracking-[0.6px] overflow-hidden whitespace-nowrap align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[26px]"
|
||||
data-appearance="outline"
|
||||
data-test="group-menu-button"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<div>
|
||||
@ -138,18 +146,26 @@ exports[`GroupContentMenu renders as groupProfile, muted 1`] = `
|
||||
trigger="manual"
|
||||
>
|
||||
<button
|
||||
class="base-button --icon-only --circle --small"
|
||||
aria-label="group.contentMenu.menuButton"
|
||||
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[26px] min-w-[26px] text-[12px] leading-[normal] tracking-[0.6px] overflow-hidden whitespace-nowrap align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[26px]"
|
||||
data-appearance="outline"
|
||||
data-test="group-menu-button"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<div>
|
||||
@ -213,18 +229,26 @@ exports[`GroupContentMenu renders as groupProfile, not muted 1`] = `
|
||||
trigger="manual"
|
||||
>
|
||||
<button
|
||||
class="base-button --icon-only --circle --small"
|
||||
aria-label="group.contentMenu.menuButton"
|
||||
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[26px] min-w-[26px] text-[12px] leading-[normal] tracking-[0.6px] overflow-hidden whitespace-nowrap align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[26px]"
|
||||
data-appearance="outline"
|
||||
data-test="group-menu-button"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<div>
|
||||
@ -288,18 +312,26 @@ exports[`GroupContentMenu renders as groupTeaser 1`] = `
|
||||
trigger="manual"
|
||||
>
|
||||
<button
|
||||
class="base-button --icon-only --circle --small"
|
||||
aria-label="group.contentMenu.menuButton"
|
||||
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[26px] min-w-[26px] text-[12px] leading-[normal] tracking-[0.6px] overflow-hidden whitespace-nowrap align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[26px]"
|
||||
data-appearance="outline"
|
||||
data-test="group-menu-button"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<div>
|
||||
|
||||
@ -182,9 +182,18 @@
|
||||
>
|
||||
{{ $t('actions.cancel') }}
|
||||
</os-button>
|
||||
<base-button type="submit" icon="check" :loading="loading" :disabled="errors" filled>
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="filled"
|
||||
type="submit"
|
||||
:loading="loading"
|
||||
:disabled="!!errors"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="check" />
|
||||
</template>
|
||||
{{ $t('actions.save') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</base-card>
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<a v-if="settings.url" :href="settings.url" :target="settings.target">
|
||||
<base-button
|
||||
<os-button
|
||||
class="custom-button"
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
ghost
|
||||
:aria-label="$t(settings.toolTipIdent)"
|
||||
v-tooltip="{
|
||||
content: $t(settings.toolTipIdent),
|
||||
placement: 'bottom-start',
|
||||
@ -16,13 +18,15 @@
|
||||
:alt="settings.iconAltText"
|
||||
:style="logoWidthStyle"
|
||||
/>
|
||||
</base-button>
|
||||
</os-button>
|
||||
</a>
|
||||
<nuxt-link v-else :to="settings.path">
|
||||
<base-button
|
||||
<os-button
|
||||
class="custom-button"
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
ghost
|
||||
:aria-label="$t(settings.toolTipIdent)"
|
||||
v-tooltip="{
|
||||
content: $t(settings.toolTipIdent),
|
||||
placement: 'bottom-start',
|
||||
@ -34,27 +38,24 @@
|
||||
:alt="settings.iconAltText"
|
||||
:style="logoWidthStyle"
|
||||
/>
|
||||
</base-button>
|
||||
</os-button>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
|
||||
export default {
|
||||
components: { OsButton },
|
||||
name: 'CustomButton',
|
||||
props: {
|
||||
settings: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEmpty,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
logoWidthStyle() {
|
||||
const width = this.isEmpty(this.settings.iconWidth) ? '26px' : this.settings.iconWidth
|
||||
const width = isEmpty(this.settings.iconWidth) ? '26px' : this.settings.iconWidth
|
||||
return `width: ${width};`
|
||||
},
|
||||
},
|
||||
|
||||
@ -42,26 +42,30 @@
|
||||
<section class="warning">
|
||||
<p>{{ $t('settings.deleteUserAccount.accountWarning') }}</p>
|
||||
</section>
|
||||
<base-button
|
||||
icon="trash"
|
||||
danger
|
||||
filled
|
||||
<os-button
|
||||
variant="danger"
|
||||
appearance="filled"
|
||||
:disabled="!deleteEnabled"
|
||||
data-test="delete-button"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="trash" />
|
||||
</template>
|
||||
{{ $t('settings.deleteUserAccount.name') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import gql from 'graphql-tag'
|
||||
import { currentUserCountQuery } from '~/graphql/User'
|
||||
|
||||
export default {
|
||||
name: 'DeleteData',
|
||||
components: { OsButton },
|
||||
data() {
|
||||
return {
|
||||
deleteContributions: false,
|
||||
@ -166,7 +170,7 @@ export default {
|
||||
border-left: 4px solid $color-danger;
|
||||
}
|
||||
|
||||
> .base-button {
|
||||
> button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ export default {
|
||||
displayContextMenu(target, content, type) {
|
||||
const placement = type === 'link' ? 'right' : 'top-start'
|
||||
const trigger = type === 'link' ? 'click' : 'mouseenter'
|
||||
const showOnInit = type !== 'link'
|
||||
|
||||
if (this.menu) {
|
||||
return
|
||||
@ -24,7 +23,6 @@ export default {
|
||||
inertia: true,
|
||||
interactive: true,
|
||||
placement,
|
||||
showOnInit,
|
||||
theme: 'ocelot-social',
|
||||
trigger,
|
||||
onMount(instance) {
|
||||
@ -35,6 +33,7 @@ export default {
|
||||
}
|
||||
},
|
||||
})
|
||||
this.menu.show()
|
||||
|
||||
// we have to update tippy whenever the DOM is updated
|
||||
if (MutationObserver) {
|
||||
@ -50,8 +49,9 @@ export default {
|
||||
},
|
||||
hideContextMenu() {
|
||||
if (this.menu) {
|
||||
this.menu.destroy()
|
||||
const menu = this.menu
|
||||
this.menu = null
|
||||
menu.destroy()
|
||||
}
|
||||
if (this.observer) {
|
||||
this.observer.disconnect()
|
||||
|
||||
@ -248,12 +248,17 @@ export default {
|
||||
this.editor.commands.mention({ id: message.id, label: message.slug })
|
||||
},
|
||||
toggleLinkInput(attrs, element) {
|
||||
if (!this.isLinkInputActive && attrs && element) {
|
||||
if (this.$refs.contextMenu.menu) {
|
||||
this.$refs.contextMenu.hideContextMenu()
|
||||
this.isLinkInputActive = false
|
||||
this.editor.focus()
|
||||
} else if (attrs && element) {
|
||||
this.$refs.linkInput.linkUrl = attrs.href
|
||||
this.isLinkInputActive = true
|
||||
this.$refs.contextMenu.displayContextMenu(element, this.$refs.linkInput.$el, 'link')
|
||||
this.$nextTick(() => {
|
||||
this.$refs.contextMenu.displayContextMenu(element, this.$refs.linkInput.$el, 'link')
|
||||
})
|
||||
} else {
|
||||
this.$refs.contextMenu.hideContextMenu()
|
||||
this.isLinkInputActive = false
|
||||
this.editor.focus()
|
||||
}
|
||||
|
||||
@ -1,64 +1,144 @@
|
||||
<template>
|
||||
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive, getMarkAttrs }">
|
||||
<div>
|
||||
<menu-bar-button :isActive="isActive.bold()" :onClick="commands.bold" icon="bold" />
|
||||
<os-button
|
||||
size="sm"
|
||||
circle
|
||||
variant="primary"
|
||||
:appearance="isActive.bold() ? 'outline' : 'ghost'"
|
||||
:aria-label="$t('editor.legend.bold')"
|
||||
@click="commands.bold"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="bold" />
|
||||
</template>
|
||||
</os-button>
|
||||
|
||||
<menu-bar-button :isActive="isActive.italic()" :onClick="commands.italic" icon="italic" />
|
||||
<os-button
|
||||
size="sm"
|
||||
circle
|
||||
variant="primary"
|
||||
:appearance="isActive.italic() ? 'outline' : 'ghost'"
|
||||
:aria-label="$t('editor.legend.italic')"
|
||||
@click="commands.italic"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="italic" />
|
||||
</template>
|
||||
</os-button>
|
||||
|
||||
<menu-bar-button
|
||||
:isActive="isActive.underline()"
|
||||
:onClick="commands.underline"
|
||||
icon="underline"
|
||||
/>
|
||||
<os-button
|
||||
size="sm"
|
||||
circle
|
||||
variant="primary"
|
||||
:appearance="isActive.underline() ? 'outline' : 'ghost'"
|
||||
:aria-label="$t('editor.legend.underline')"
|
||||
@click="commands.underline"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="underline" />
|
||||
</template>
|
||||
</os-button>
|
||||
|
||||
<menu-bar-button
|
||||
ref="linkButton"
|
||||
:isActive="isActive.link()"
|
||||
:onClick="(event) => toggleLinkInput(getMarkAttrs('link'), event.currentTarget)"
|
||||
icon="link"
|
||||
/>
|
||||
<os-button
|
||||
size="sm"
|
||||
circle
|
||||
variant="primary"
|
||||
:appearance="isActive.link() ? 'outline' : 'ghost'"
|
||||
:aria-label="$t('editor.legend.link')"
|
||||
@click="(event) => toggleLinkInput(getMarkAttrs('link'), event.target.closest('button'))"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="link" />
|
||||
</template>
|
||||
</os-button>
|
||||
|
||||
<menu-bar-button
|
||||
:isActive="isActive.paragraph()"
|
||||
:onClick="commands.paragraph"
|
||||
icon="paragraph"
|
||||
/>
|
||||
<os-button
|
||||
size="sm"
|
||||
circle
|
||||
variant="primary"
|
||||
:appearance="isActive.paragraph() ? 'outline' : 'ghost'"
|
||||
:aria-label="$t('editor.legend.paragraph')"
|
||||
@click="commands.paragraph"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="paragraph" />
|
||||
</template>
|
||||
</os-button>
|
||||
|
||||
<menu-bar-button
|
||||
:isActive="isActive.heading({ level: 3 })"
|
||||
:onClick="() => commands.heading({ level: 3 })"
|
||||
label="H3"
|
||||
/>
|
||||
<os-button
|
||||
size="sm"
|
||||
circle
|
||||
variant="primary"
|
||||
:appearance="isActive.heading({ level: 3 }) ? 'outline' : 'ghost'"
|
||||
:aria-label="$t('editor.legend.heading3')"
|
||||
@click="() => commands.heading({ level: 3 })"
|
||||
>
|
||||
H3
|
||||
</os-button>
|
||||
|
||||
<menu-bar-button
|
||||
:isActive="isActive.heading({ level: 4 })"
|
||||
:onClick="() => commands.heading({ level: 4 })"
|
||||
label="H4"
|
||||
/>
|
||||
<os-button
|
||||
size="sm"
|
||||
circle
|
||||
variant="primary"
|
||||
:appearance="isActive.heading({ level: 4 }) ? 'outline' : 'ghost'"
|
||||
:aria-label="$t('editor.legend.heading4')"
|
||||
@click="() => commands.heading({ level: 4 })"
|
||||
>
|
||||
H4
|
||||
</os-button>
|
||||
|
||||
<menu-bar-button
|
||||
:isActive="isActive.bullet_list()"
|
||||
:onClick="commands.bullet_list"
|
||||
icon="list-ul"
|
||||
/>
|
||||
<os-button
|
||||
size="sm"
|
||||
circle
|
||||
variant="primary"
|
||||
:appearance="isActive.bullet_list() ? 'outline' : 'ghost'"
|
||||
:aria-label="$t('editor.legend.unorderedList')"
|
||||
@click="commands.bullet_list"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="list-ul" />
|
||||
</template>
|
||||
</os-button>
|
||||
|
||||
<menu-bar-button
|
||||
:isActive="isActive.ordered_list()"
|
||||
:onClick="commands.ordered_list"
|
||||
icon="list-ol"
|
||||
/>
|
||||
<os-button
|
||||
size="sm"
|
||||
circle
|
||||
variant="primary"
|
||||
:appearance="isActive.ordered_list() ? 'outline' : 'ghost'"
|
||||
:aria-label="$t('editor.legend.orderedList')"
|
||||
@click="commands.ordered_list"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="list-ol" />
|
||||
</template>
|
||||
</os-button>
|
||||
|
||||
<menu-bar-button
|
||||
:isActive="isActive.blockquote()"
|
||||
:onClick="commands.blockquote"
|
||||
icon="quote-right"
|
||||
/>
|
||||
<os-button
|
||||
size="sm"
|
||||
circle
|
||||
variant="primary"
|
||||
:appearance="isActive.blockquote() ? 'outline' : 'ghost'"
|
||||
:aria-label="$t('editor.legend.quote')"
|
||||
@click="commands.blockquote"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="quote-right" />
|
||||
</template>
|
||||
</os-button>
|
||||
|
||||
<menu-bar-button
|
||||
:isActive="isActive.horizontal_rule()"
|
||||
:onClick="commands.horizontal_rule"
|
||||
icon="minus"
|
||||
/>
|
||||
<os-button
|
||||
size="sm"
|
||||
circle
|
||||
variant="primary"
|
||||
:appearance="isActive.horizontal_rule() ? 'outline' : 'ghost'"
|
||||
:aria-label="$t('editor.legend.ruler')"
|
||||
@click="commands.horizontal_rule"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="minus" />
|
||||
</template>
|
||||
</os-button>
|
||||
|
||||
<menu-legend class="legend-button" />
|
||||
</div>
|
||||
@ -66,14 +146,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { EditorMenuBar } from 'tiptap'
|
||||
import MenuBarButton from './MenuBarButton'
|
||||
import MenuLegend from './MenuLegend.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OsButton,
|
||||
EditorMenuBar,
|
||||
MenuBarButton,
|
||||
MenuLegend,
|
||||
},
|
||||
props: {
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<base-button size="small" circle :ghost="!isActive" @click="onClick" :icon="icon">
|
||||
<span v-if="label">{{ label }}</span>
|
||||
</base-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
isActive: Boolean,
|
||||
icon: String,
|
||||
label: String,
|
||||
onClick: { type: Function, default: () => {} },
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<dropdown :placement="placement" offset="5">
|
||||
<template #default="{ openMenu, closeMenu }">
|
||||
<template #default="{ toggleMenu }">
|
||||
<slot name="button">
|
||||
<menu-bar-button
|
||||
<os-button
|
||||
class="legend-question-button"
|
||||
icon="question-circle"
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
ghost
|
||||
:onClick="
|
||||
() => {
|
||||
isDropdownOpen ? closeMenu() : openMenu()
|
||||
isDropdownOpen = !isDropdownOpen
|
||||
}
|
||||
"
|
||||
/>
|
||||
size="sm"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="question-circle" />
|
||||
</template>
|
||||
</os-button>
|
||||
</slot>
|
||||
</template>
|
||||
<!-- eslint-disable-next-line vue/no-useless-template-attributes -->
|
||||
@ -26,9 +26,12 @@
|
||||
:key="item.name"
|
||||
>
|
||||
<div>
|
||||
<base-button size="small" circle ghost :icon="item.iconName" class="legend-icon">
|
||||
<os-button size="sm" circle variant="primary" appearance="ghost" class="legend-icon">
|
||||
<template v-if="item.iconName" #icon>
|
||||
<base-icon :name="item.iconName" />
|
||||
</template>
|
||||
<span v-if="item.label">{{ item.label }}</span>
|
||||
</base-button>
|
||||
</os-button>
|
||||
<span>{{ $t(item.name) }}</span>
|
||||
</div>
|
||||
<span class="tool-shortcut">{{ item.shortcut }}</span>
|
||||
@ -39,13 +42,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
import MenuBarButton from './MenuBarButton'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Dropdown,
|
||||
MenuBarButton,
|
||||
OsButton,
|
||||
},
|
||||
props: {
|
||||
placement: { type: String, default: 'bottom-start' },
|
||||
@ -65,20 +68,19 @@ export default {
|
||||
{ iconName: 'quote-right', name: `editor.legend.quote`, shortcut: '> + space' },
|
||||
{ iconName: 'minus', name: `editor.legend.ruler`, shortcut: '---' },
|
||||
],
|
||||
isDropdownOpen: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.legend-question-button {
|
||||
color: $color-neutral-40;
|
||||
color: #70677e !important;
|
||||
font-size: 1.2rem !important;
|
||||
}
|
||||
.legend-question-button:hover {
|
||||
background: none !important;
|
||||
color: $color-neutral-40 !important;
|
||||
color: #70677e !important;
|
||||
}
|
||||
.legend-question-button:focus {
|
||||
outline: none !important;
|
||||
|
||||
@ -47,13 +47,19 @@
|
||||
<span>{{ $t('editor.embed.always_allow') }}</span>
|
||||
</label>
|
||||
</aside>
|
||||
<base-button
|
||||
icon="close"
|
||||
size="small"
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
circle
|
||||
size="sm"
|
||||
class="close-button"
|
||||
:aria-label="$t('actions.close')"
|
||||
@click.prevent="removeEmbed()"
|
||||
/>
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="close" />
|
||||
</template>
|
||||
</os-button>
|
||||
</ds-container>
|
||||
</template>
|
||||
|
||||
@ -252,9 +258,9 @@ export default {
|
||||
}
|
||||
|
||||
> .close-button {
|
||||
position: absolute;
|
||||
top: $space-x-small;
|
||||
right: $space-x-small;
|
||||
position: absolute !important;
|
||||
top: $space-x-small !important;
|
||||
right: $space-x-small !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,24 @@
|
||||
<template>
|
||||
<div class="emotion-button">
|
||||
<base-button :id="emotion" circle ghost @click="$emit('toggleEmotion', emotion)">
|
||||
<os-button
|
||||
:id="emotion"
|
||||
appearance="ghost"
|
||||
circle
|
||||
:aria-label="$t(`contribution.emotions-label.${emotion}`)"
|
||||
@click="$emit('toggleEmotion', emotion)"
|
||||
>
|
||||
<img class="image" :src="emojiPath" />
|
||||
</base-button>
|
||||
</os-button>
|
||||
<label class="label" :for="emotion">{{ $t(`contribution.emotions-label.${emotion}`) }}</label>
|
||||
<p v-if="emotionCount !== null" class="count">{{ emotionCount }}x</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
|
||||
export default {
|
||||
components: { OsButton },
|
||||
name: 'EmotionButton',
|
||||
props: {
|
||||
emojiPath: {
|
||||
@ -35,7 +44,7 @@ export default {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
> .base-button {
|
||||
> button {
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
|
||||
@ -63,7 +63,7 @@ describe('CtaJoinLeaveGroup.vue', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
wrapper.find('.base-button').trigger('click')
|
||||
wrapper.find('[data-test="join-leave-btn"]').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
})
|
||||
it('emits update event', async () => {
|
||||
|
||||
@ -8,24 +8,28 @@
|
||||
{{ $t('contribution.comment.commenting-disabled.blocked-author.call-to-action') }}
|
||||
</ds-text>
|
||||
<nuxt-link :to="authorLink">
|
||||
<base-button icon="arrow-right" filled>
|
||||
<os-button variant="primary" appearance="filled">
|
||||
<template #icon><base-icon name="arrow-right" /></template>
|
||||
{{
|
||||
$t('contribution.comment.commenting-disabled.blocked-author.button-label', {
|
||||
name: author.name,
|
||||
})
|
||||
}}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</nuxt-link>
|
||||
</ds-space>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
|
||||
export default {
|
||||
name: 'CtaUnblockAuthor',
|
||||
components: { OsButton },
|
||||
props: {
|
||||
author: {
|
||||
type: Object,
|
||||
require: true,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
||||
@ -11,9 +11,8 @@ exports[`CtaJoinLeaveGroup.vue mount renders 1`] = `
|
||||
</h4>
|
||||
<p class="ds-text">
|
||||
contribution.comment.commenting-disabled.no-group-member.call-to-action
|
||||
</p> <button type="button" class="join-leave-button base-button --filled has-tooltip" data-original-title="null"><span class="base-icon"><!----></span>
|
||||
<!---->
|
||||
</p> <button type="button" data-variant="primary" data-appearance="filled" class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.05)] disabled:bg-[var(--color-disabled)] disabled:text-[var(--color-disabled-contrast)] disabled:border-[var(--color-disabled)] disabled:shadow-[inset_0_0_0_1px_transparent] h-[36px] min-w-[36px] px-[16px] py-0 text-[15px] leading-[normal] rounded-[5px] align-middle w-full bg-[var(--color-primary)] text-[var(--color-primary-contrast)] border-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] hover:border-[var(--color-primary-hover)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] has-tooltip" data-original-title="null"><span class="inline-flex items-center gap-2"><span class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current -ml-1"><span class="base-icon"><!----></span></span>
|
||||
group.joinLeaveButton.join
|
||||
</button>
|
||||
</span></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -10,9 +10,9 @@ exports[`CtaUnblockAuthor.vue shallowMount renders 1`] = `
|
||||
contribution.comment.commenting-disabled.blocked-author.call-to-action
|
||||
</ds-text-stub>
|
||||
<nuxt-link-stub to="[object Object]">
|
||||
<base-button-stub filled="true" icon="arrow-right" size="regular" type="button">
|
||||
<os-button-stub variant="primary" appearance="filled" size="md" type="button">
|
||||
contribution.comment.commenting-disabled.blocked-author.button-label
|
||||
</base-button-stub>
|
||||
</os-button-stub>
|
||||
</nuxt-link-stub>
|
||||
</ds-space-stub>
|
||||
`;
|
||||
|
||||
@ -20,7 +20,13 @@
|
||||
<ds-text>
|
||||
{{ $t('components.registration.email-nonce.form.click-next') }}
|
||||
</ds-text>
|
||||
<os-button variant="primary" :disabled="disabled" name="submit" type="submit">
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="filled"
|
||||
:disabled="disabled"
|
||||
name="submit"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('components.registration.email-nonce.form.next') }}
|
||||
</os-button>
|
||||
<slot></slot>
|
||||
|
||||
@ -60,21 +60,24 @@ describe('CategoriesFilter.vue', () => {
|
||||
|
||||
describe('mount', () => {
|
||||
it('starts with all categories button active', () => {
|
||||
const allCategoriesButton = wrapper.find('.categories-filter .item-all-topics .base-button')
|
||||
expect(allCategoriesButton.attributes().class).toContain('--filled')
|
||||
expect(
|
||||
wrapper
|
||||
.find('.categories-filter .item-all-topics button[data-appearance="filled"]')
|
||||
.exists(),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
// TODO move to FilterMenuComponent.spec.js?
|
||||
// it('sets category button attribute `filled` when corresponding category is filtered', async () => {
|
||||
// getters['posts/filteredCategoryIds'] = jest.fn(() => ['cat9'])
|
||||
// wrapper = await Wrapper()
|
||||
// democracyAndPoliticsButton = wrapper.find('.categories-filter .item-save-topics .base-button')
|
||||
// democracyAndPoliticsButton = wrapper.find('.categories-filter .item-save-topics button')
|
||||
// expect(democracyAndPoliticsButton.attributes().class).toContain('--filled')
|
||||
// })
|
||||
|
||||
describe('click on an "catetories-buttons" button', () => {
|
||||
it('calls TOGGLE_CATEGORY when clicked', () => {
|
||||
environmentAndNatureButton = wrapper.findAll('.category-filter-list .base-button').at(0)
|
||||
environmentAndNatureButton = wrapper.findAll('.category-filter-list button').at(0)
|
||||
environmentAndNatureButton.trigger('click')
|
||||
expect(mutations['posts/TOGGLE_CATEGORY']).toHaveBeenCalledWith({}, 'cat15')
|
||||
})
|
||||
@ -84,7 +87,7 @@ describe('CategoriesFilter.vue', () => {
|
||||
it('when all button is clicked', async () => {
|
||||
getters['posts/filteredCategoryIds'] = jest.fn(() => ['cat9'])
|
||||
wrapper = await Wrapper()
|
||||
const allCategoriesButton = wrapper.find('.categories-filter .item-all-topics .base-button')
|
||||
const allCategoriesButton = wrapper.find('.categories-filter .item-all-topics button')
|
||||
allCategoriesButton.trigger('click')
|
||||
expect(mutations['posts/RESET_CATEGORIES']).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@ -94,7 +97,7 @@ describe('CategoriesFilter.vue', () => {
|
||||
// describe('save categories', () => {
|
||||
// it('calls the API', async () => {
|
||||
// wrapper = await Wrapper()
|
||||
// const saveButton = wrapper.find('.categories-filter .item-save-topics .base-button')
|
||||
// const saveButton = wrapper.find('.categories-filter .item-save-topics button')
|
||||
// saveButton.trigger('click')
|
||||
// expect(apolloMutationMock).toBeCalled()
|
||||
// })
|
||||
|
||||
@ -2,38 +2,44 @@
|
||||
<filter-menu-section :title="$t('filter-menu.categories')" class="categories-filter">
|
||||
<template #filter-list>
|
||||
<div class="item item-all-topics">
|
||||
<base-button
|
||||
:filled="!filteredCategoryIds.length"
|
||||
:label="$t('filter-menu.all')"
|
||||
icon="check"
|
||||
<os-button
|
||||
variant="primary"
|
||||
:appearance="!filteredCategoryIds.length ? 'filled' : 'outline'"
|
||||
size="sm"
|
||||
@click="setResetCategories"
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="check" />
|
||||
</template>
|
||||
{{ $t('filter-menu.all') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</div>
|
||||
<div class="category-filter-list">
|
||||
<!-- <ds-space margin="small" /> -->
|
||||
<base-button
|
||||
<os-button
|
||||
v-for="category in sortCategories(categories)"
|
||||
:key="category.id"
|
||||
variant="primary"
|
||||
:appearance="filteredCategoryIds.includes(category.id) ? 'filled' : 'outline'"
|
||||
size="sm"
|
||||
@click="saveCategories(category.id)"
|
||||
:filled="filteredCategoryIds.includes(category.id)"
|
||||
:icon="category.icon"
|
||||
size="small"
|
||||
v-tooltip="{
|
||||
content: $t(`contribution.category.description.${category.slug}`),
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon :name="category.icon" />
|
||||
</template>
|
||||
{{ $t(`contribution.category.name.${category.slug}`) }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</div>
|
||||
</template>
|
||||
</filter-menu-section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
|
||||
import SortCategories from '~/mixins/sortCategoriesMixin.js'
|
||||
@ -42,6 +48,7 @@ import GetCategories from '~/mixins/getCategoriesMixin.js'
|
||||
export default {
|
||||
components: {
|
||||
FilterMenuSection,
|
||||
OsButton,
|
||||
},
|
||||
mixins: [SortCategories, GetCategories],
|
||||
computed: {
|
||||
@ -69,7 +76,7 @@ export default {
|
||||
.category-filter-list {
|
||||
margin-left: $space-xx-small;
|
||||
|
||||
> .base-button {
|
||||
> button {
|
||||
margin-right: $space-xx-small;
|
||||
margin-bottom: $space-xx-small;
|
||||
}
|
||||
|
||||
@ -45,14 +45,14 @@ describe('mount', () => {
|
||||
|
||||
// describe('mount', () => {
|
||||
// it('starts with all emotions button active', () => {
|
||||
// const allEmotionsButton = wrapper.find('.emotions-filter .sidebar .base-button')
|
||||
// const allEmotionsButton = wrapper.find('.emotions-filter .sidebar button')
|
||||
// expect(allEmotionsButton.attributes().class).toContain('--filled')
|
||||
// })
|
||||
|
||||
// describe('click on an "emotion-button" button', () => {
|
||||
// it('calls TOGGLE_EMOTION when clicked', () => {
|
||||
// const wrapper = Wrapper()
|
||||
// happyEmotionButton = wrapper.findAll('.emotion-button > .base-button').at(1)
|
||||
// happyEmotionButton = wrapper.findAll('.emotion-button > button').at(1)
|
||||
// happyEmotionButton.trigger('click')
|
||||
// expect(mutations['posts/TOGGLE_EMOTION']).toHaveBeenCalledWith({}, 'happy')
|
||||
// })
|
||||
@ -60,7 +60,7 @@ describe('mount', () => {
|
||||
// it('sets the attribute `src` to colorized image', () => {
|
||||
// getters['posts/filteredByEmotions'] = jest.fn(() => ['happy'])
|
||||
// const wrapper = Wrapper()
|
||||
// happyEmotionButton = wrapper.findAll('.emotion-button > .base-button').at(1)
|
||||
// happyEmotionButton = wrapper.findAll('.emotion-button > button').at(1)
|
||||
// const happyEmotionButtonImage = happyEmotionButton.find('img')
|
||||
// expect(happyEmotionButtonImage.attributes().src).toEqual('/img/svg/emoji/happy_color.svg')
|
||||
// })
|
||||
@ -70,7 +70,7 @@ describe('mount', () => {
|
||||
// it('when all button is clicked', async () => {
|
||||
// getters['posts/filteredByEmotions'] = jest.fn(() => ['happy'])
|
||||
// wrapper = await Wrapper()
|
||||
// const allEmotionsButton = wrapper.find('.emotions-filter .sidebar .base-button')
|
||||
// const allEmotionsButton = wrapper.find('.emotions-filter .sidebar button')
|
||||
// allEmotionsButton.trigger('click')
|
||||
// expect(mutations['posts/RESET_EMOTIONS']).toHaveBeenCalledTimes(1)
|
||||
// })
|
||||
|
||||
@ -2,36 +2,41 @@
|
||||
<filter-menu-section class="order-by-filter" :title="sectionTitle" :divider="false">
|
||||
<template #filter-list>
|
||||
<li class="item">
|
||||
<base-button
|
||||
icon="check"
|
||||
:label="$t('filter-menu.ended.all.label')"
|
||||
:filled="!eventsEnded"
|
||||
<os-button
|
||||
variant="primary"
|
||||
:appearance="!eventsEnded ? 'filled' : 'outline'"
|
||||
size="sm"
|
||||
:title="$t('filter-menu.ended.all.hint')"
|
||||
@click="toggleEventsEnded"
|
||||
data-test="all-button"
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="check" />
|
||||
</template>
|
||||
{{ $t('filter-menu.ended.all.label') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</li>
|
||||
<li class="item">
|
||||
<base-button
|
||||
icon="calendar"
|
||||
:label="$t('filter-menu.ended.onlyEnded.label')"
|
||||
:filled="eventsEnded"
|
||||
<os-button
|
||||
variant="primary"
|
||||
:appearance="eventsEnded ? 'filled' : 'outline'"
|
||||
size="sm"
|
||||
:title="$t('filter-menu.ended.onlyEnded.hint')"
|
||||
@click="toggleEventsEnded"
|
||||
data-test="not-ended-button"
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="calendar" />
|
||||
</template>
|
||||
{{ $t('filter-menu.ended.onlyEnded.label') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</li>
|
||||
</template>
|
||||
</filter-menu-section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
|
||||
|
||||
@ -39,6 +44,7 @@ export default {
|
||||
name: 'EventsByFilter',
|
||||
components: {
|
||||
FilterMenuSection,
|
||||
OsButton,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
|
||||
@ -39,15 +39,13 @@ describe('FilterMenu.vue', () => {
|
||||
|
||||
describe('mount', () => {
|
||||
it('starts with dropdown button inactive', () => {
|
||||
const dropdownButton = wrapper.find('.filter-menu .base-button')
|
||||
expect(dropdownButton.attributes().class).toContain('--ghost')
|
||||
expect(wrapper.find('.filter-menu button[data-appearance="ghost"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('sets dropdwon button attribute `filled` when a filter is applied', () => {
|
||||
it('sets dropdown button attribute `filled` when a filter is applied', () => {
|
||||
getters['posts/isActive'] = jest.fn(() => true)
|
||||
wrapper = Wrapper()
|
||||
const dropdownButton = wrapper.find('.filter-menu .base-button')
|
||||
expect(dropdownButton.attributes().class).toContain('--filled')
|
||||
expect(wrapper.find('.filter-menu button[data-appearance="filled"]').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
<template>
|
||||
<dropdown ref="menu" placement="top-start" :offset="8" class="filter-menu">
|
||||
<base-button
|
||||
slot="default"
|
||||
icon="filter"
|
||||
:filled="filterActive"
|
||||
:ghost="!filterActive"
|
||||
slot-scope="{ toggleMenu }"
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||
</base-button>
|
||||
<template #default="{ toggleMenu }">
|
||||
<os-button
|
||||
variant="primary"
|
||||
:appearance="filterActive ? 'filled' : 'ghost'"
|
||||
:aria-label="$t('common.filter')"
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="filter" />
|
||||
</template>
|
||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||
</os-button>
|
||||
</template>
|
||||
<template #popover>
|
||||
<filter-menu-component />
|
||||
</template>
|
||||
@ -17,6 +20,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
import { mapGetters } from 'vuex'
|
||||
import FilterMenuComponent from './FilterMenuComponent'
|
||||
@ -25,6 +29,7 @@ export default {
|
||||
components: {
|
||||
Dropdown,
|
||||
FilterMenuComponent,
|
||||
OsButton,
|
||||
},
|
||||
props: {
|
||||
placement: { type: String },
|
||||
|
||||
@ -44,19 +44,21 @@ describe('FollowingFilter', () => {
|
||||
const wrapper = Wrapper()
|
||||
expect(
|
||||
wrapper
|
||||
.find('.following-filter .filter-list .follower-item .base-button')
|
||||
.classes('--filled'),
|
||||
.find('.following-filter .filter-list .follower-item button[data-appearance="filled"]')
|
||||
.exists(),
|
||||
).toBe(true)
|
||||
expect(
|
||||
wrapper
|
||||
.find('.following-filter .filter-list .posts-in-my-groups-item .base-button')
|
||||
.classes('--filled'),
|
||||
.find(
|
||||
'.following-filter .filter-list .posts-in-my-groups-item button[data-appearance="filled"]',
|
||||
)
|
||||
.exists(),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
describe('click "filter-by-followed" button', () => {
|
||||
it('calls TOGGLE_FILTER_BY_FOLLOWED', () => {
|
||||
wrapper.find('.following-filter .filter-list .follower-item .base-button').trigger('click')
|
||||
wrapper.find('.following-filter .filter-list .follower-item button').trigger('click')
|
||||
expect(mutations['posts/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34')
|
||||
})
|
||||
})
|
||||
@ -64,7 +66,7 @@ describe('FollowingFilter', () => {
|
||||
describe('click "filter-by-my-groups" button', () => {
|
||||
it('calls TOGGLE_FILTER_BY_MY_GROUPS', () => {
|
||||
wrapper
|
||||
.find('.following-filter .filter-list .posts-in-my-groups-item .base-button')
|
||||
.find('.following-filter .filter-list .posts-in-my-groups-item button')
|
||||
.trigger('click')
|
||||
expect(mutations['posts/TOGGLE_FILTER_BY_MY_GROUPS']).toHaveBeenCalled()
|
||||
})
|
||||
@ -72,9 +74,7 @@ describe('FollowingFilter', () => {
|
||||
describe('clears follower filter', () => {
|
||||
it('when all button is clicked', async () => {
|
||||
wrapper = await Wrapper()
|
||||
const clearFollowerButton = wrapper.find(
|
||||
'.following-filter .item-all-follower .base-button',
|
||||
)
|
||||
const clearFollowerButton = wrapper.find('.following-filter .item-all-follower button')
|
||||
clearFollowerButton.trigger('click')
|
||||
expect(mutations['posts/RESET_FOLLOWERS_FILTER']).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@ -6,40 +6,48 @@
|
||||
>
|
||||
<template #filter-follower>
|
||||
<div class="item item-all-follower">
|
||||
<base-button
|
||||
:filled="!filteredByUsersFollowed && !filteredByPostsInMyGroups"
|
||||
:label="$t('filter-menu.all')"
|
||||
icon="check"
|
||||
<os-button
|
||||
variant="primary"
|
||||
:appearance="
|
||||
!filteredByUsersFollowed && !filteredByPostsInMyGroups ? 'filled' : 'outline'
|
||||
"
|
||||
size="sm"
|
||||
@click="setResetFollowers"
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="check" />
|
||||
</template>
|
||||
{{ $t('filter-menu.all') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</div>
|
||||
<div class="follower-filter-list">
|
||||
<li class="item follower-item">
|
||||
<base-button
|
||||
icon="user-plus"
|
||||
:label="$t('filter-menu.following')"
|
||||
:filled="filteredByUsersFollowed"
|
||||
<os-button
|
||||
variant="primary"
|
||||
:appearance="filteredByUsersFollowed ? 'filled' : 'outline'"
|
||||
size="sm"
|
||||
:title="$t('filter-menu.following')"
|
||||
@click="toggleFilteredByFollowed(currentUser.id)"
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="user-plus" />
|
||||
</template>
|
||||
{{ $t('filter-menu.following') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</li>
|
||||
<li class="item posts-in-my-groups-item">
|
||||
<base-button
|
||||
icon="users"
|
||||
:label="$t('filter-menu.my-groups')"
|
||||
:filled="filteredByPostsInMyGroups"
|
||||
<os-button
|
||||
variant="primary"
|
||||
:appearance="filteredByPostsInMyGroups ? 'filled' : 'outline'"
|
||||
size="sm"
|
||||
:title="$t('contribution.filterMyGroups')"
|
||||
@click="toggleFilteredByMyGroups()"
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="users" />
|
||||
</template>
|
||||
{{ $t('contribution.filterMyGroups') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</li>
|
||||
</div>
|
||||
</template>
|
||||
@ -47,6 +55,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
|
||||
|
||||
@ -54,6 +63,7 @@ export default {
|
||||
name: 'FollowingFilter',
|
||||
components: {
|
||||
FilterMenuSection,
|
||||
OsButton,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
@ -81,7 +91,7 @@ export default {
|
||||
display: flex;
|
||||
margin-left: $space-xx-small;
|
||||
|
||||
& .base-button {
|
||||
& button {
|
||||
margin-right: $space-xx-small;
|
||||
margin-bottom: $space-xx-small;
|
||||
}
|
||||
|
||||
@ -1,27 +1,30 @@
|
||||
<template>
|
||||
<span>
|
||||
<base-button
|
||||
class="my-filter-button my-filter-button-selected"
|
||||
right
|
||||
@click="clickButton"
|
||||
filled
|
||||
>
|
||||
<span class="header-button-wrapper">
|
||||
<os-button class="my-filter-button" variant="primary" appearance="filled" @click="clickButton">
|
||||
{{ title }}
|
||||
</base-button>
|
||||
<base-button
|
||||
</os-button>
|
||||
<os-button
|
||||
class="filter-remove"
|
||||
@click="clickRemove"
|
||||
icon="close"
|
||||
:title="titleRemove"
|
||||
size="small"
|
||||
variant="primary"
|
||||
appearance="filled"
|
||||
circle
|
||||
filled
|
||||
/>
|
||||
size="sm"
|
||||
:title="titleRemove"
|
||||
:aria-label="titleRemove"
|
||||
@click.stop="clickRemove"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="close" />
|
||||
</template>
|
||||
</os-button>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
|
||||
export default {
|
||||
name: 'HeaderButton',
|
||||
components: { OsButton },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
@ -43,14 +46,21 @@ export default {
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.my-filter-button-selected {
|
||||
padding-right: 36px;
|
||||
}
|
||||
|
||||
.base-button.filter-remove {
|
||||
.header-button-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
margin-left: -37px;
|
||||
top: -5px;
|
||||
margin-right: 8px;
|
||||
|
||||
> .my-filter-button {
|
||||
padding-right: 36px !important;
|
||||
}
|
||||
|
||||
> .filter-remove {
|
||||
position: absolute !important;
|
||||
right: 4px !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -48,7 +48,7 @@ describe('mount', () => {
|
||||
|
||||
// describe('mount', () => {
|
||||
// it('starts with all categories button active', () => {
|
||||
// const allLanguagesButton = wrapper.find('.languages-filter .sidebar .base-button')
|
||||
// const allLanguagesButton = wrapper.find('.languages-filter .sidebar button')
|
||||
// expect(allLanguagesButton.attributes().class).toContain('--filled')
|
||||
// })
|
||||
|
||||
@ -56,7 +56,7 @@ describe('mount', () => {
|
||||
// getters['posts/filteredLanguageCodes'] = jest.fn(() => ['es'])
|
||||
// const wrapper = Wrapper()
|
||||
// spanishButton = wrapper
|
||||
// .findAll('.languages-filter .item .base-button')
|
||||
// .findAll('.languages-filter .item button')
|
||||
// .at(languages.findIndex((l) => l.code === 'es'))
|
||||
// expect(spanishButton.attributes().class).toContain('--filled')
|
||||
// })
|
||||
@ -65,7 +65,7 @@ describe('mount', () => {
|
||||
// it('calls TOGGLE_LANGUAGE when clicked', () => {
|
||||
// const wrapper = Wrapper()
|
||||
// englishButton = wrapper
|
||||
// .findAll('.languages-filter .item .base-button')
|
||||
// .findAll('.languages-filter .item button')
|
||||
// .at(languages.findIndex((l) => l.code === 'en'))
|
||||
// englishButton.trigger('click')
|
||||
// expect(mutations['posts/TOGGLE_LANGUAGE']).toHaveBeenCalledWith({}, 'en')
|
||||
@ -76,7 +76,7 @@ describe('mount', () => {
|
||||
// it('when all button is clicked', async () => {
|
||||
// getters['posts/filteredLanguageCodes'] = jest.fn(() => ['en'])
|
||||
// wrapper = await Wrapper()
|
||||
// const allLanguagesButton = wrapper.find('.languages-filter .sidebar .base-button')
|
||||
// const allLanguagesButton = wrapper.find('.languages-filter .sidebar button')
|
||||
// allLanguagesButton.trigger('click')
|
||||
// expect(mutations['posts/RESET_LANGUAGES']).toHaveBeenCalledTimes(1)
|
||||
// })
|
||||
|
||||
@ -38,16 +38,20 @@ describe('OrderByFilter', () => {
|
||||
it('sets "newest-button" attribute `filled`', () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('.order-by-filter .filter-list .base-button[data-test="newest-button"]')
|
||||
.classes('--filled'),
|
||||
.find(
|
||||
'.order-by-filter .filter-list button[data-test="newest-button"][data-appearance="filled"]',
|
||||
)
|
||||
.exists(),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('don\'t sets "oldest-button" attribute `filled`', () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('.order-by-filter .filter-list .base-button[data-test="oldest-button"]')
|
||||
.classes('--filled'),
|
||||
.find(
|
||||
'.order-by-filter .filter-list button[data-test="oldest-button"][data-appearance="filled"]',
|
||||
)
|
||||
.exists(),
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
@ -61,16 +65,20 @@ describe('OrderByFilter', () => {
|
||||
it('don\'t sets "newest-button" attribute `filled`', () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('.order-by-filter .filter-list .base-button[data-test="newest-button"]')
|
||||
.classes('--filled'),
|
||||
.find(
|
||||
'.order-by-filter .filter-list button[data-test="newest-button"][data-appearance="filled"]',
|
||||
)
|
||||
.exists(),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('sets "oldest-button" attribute `filled`', () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('.order-by-filter .filter-list .base-button[data-test="oldest-button"]')
|
||||
.classes('--filled'),
|
||||
.find(
|
||||
'.order-by-filter .filter-list button[data-test="oldest-button"][data-appearance="filled"]',
|
||||
)
|
||||
.exists(),
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
@ -78,7 +86,7 @@ describe('OrderByFilter', () => {
|
||||
describe('click "newest-button"', () => {
|
||||
it('calls TOGGLE_ORDER with "sortDate_desc"', () => {
|
||||
wrapper
|
||||
.find('.order-by-filter .filter-list .base-button[data-test="newest-button"]')
|
||||
.find('.order-by-filter .filter-list button[data-test="newest-button"]')
|
||||
.trigger('click')
|
||||
expect(mutations['posts/TOGGLE_ORDER']).toHaveBeenCalledWith({}, 'sortDate_desc')
|
||||
})
|
||||
@ -87,7 +95,7 @@ describe('OrderByFilter', () => {
|
||||
describe('click "oldest-button"', () => {
|
||||
it('calls TOGGLE_ORDER with "sortDate_asc"', () => {
|
||||
wrapper
|
||||
.find('.order-by-filter .filter-list .base-button[data-test="oldest-button"]')
|
||||
.find('.order-by-filter .filter-list button[data-test="oldest-button"]')
|
||||
.trigger('click')
|
||||
expect(mutations['posts/TOGGLE_ORDER']).toHaveBeenCalledWith({}, 'sortDate_asc')
|
||||
})
|
||||
|
||||
@ -2,36 +2,41 @@
|
||||
<filter-menu-section class="order-by-filter" :title="sectionTitle" :divider="false">
|
||||
<template #filter-list>
|
||||
<li class="item">
|
||||
<base-button
|
||||
icon="sort-amount-asc"
|
||||
:label="buttonLabel('desc')"
|
||||
:filled="orderBy === orderedDesc"
|
||||
<os-button
|
||||
variant="primary"
|
||||
:appearance="orderBy === orderedDesc ? 'filled' : 'outline'"
|
||||
size="sm"
|
||||
:title="buttonTitle('desc')"
|
||||
@click="toggleOrder(orderedDesc)"
|
||||
data-test="newest-button"
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="sort-amount-asc" />
|
||||
</template>
|
||||
{{ buttonLabel('desc') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</li>
|
||||
<li class="item">
|
||||
<base-button
|
||||
icon="sort-amount-desc"
|
||||
:label="buttonLabel('asc')"
|
||||
:filled="orderBy === orderedAsc"
|
||||
<os-button
|
||||
variant="primary"
|
||||
:appearance="orderBy === orderedAsc ? 'filled' : 'outline'"
|
||||
size="sm"
|
||||
:title="buttonTitle('asc')"
|
||||
@click="toggleOrder(orderedAsc)"
|
||||
data-test="oldest-button"
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="sort-amount-desc" />
|
||||
</template>
|
||||
{{ buttonLabel('asc') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</li>
|
||||
</template>
|
||||
</filter-menu-section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
|
||||
|
||||
@ -39,6 +44,7 @@ export default {
|
||||
name: 'OrderByFilter',
|
||||
components: {
|
||||
FilterMenuSection,
|
||||
OsButton,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
|
||||
@ -1,19 +1,28 @@
|
||||
<template>
|
||||
<div>
|
||||
<nuxt-link to="/groups">
|
||||
<base-button
|
||||
icon="users"
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
ghost
|
||||
:aria-label="$t('header.groups.tooltip')"
|
||||
v-tooltip="{
|
||||
content: $t('header.groups.tooltip'),
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
/>
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="users" />
|
||||
</template>
|
||||
</os-button>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
|
||||
export default {
|
||||
components: { OsButton },
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -133,11 +133,17 @@
|
||||
<!-- submit -->
|
||||
<ds-space margin-top="large">
|
||||
<nuxt-link to="/groups">
|
||||
<os-button>{{ $t('actions.cancel') }}</os-button>
|
||||
<os-button variant="default" appearance="filled">{{ $t('actions.cancel') }}</os-button>
|
||||
</nuxt-link>
|
||||
<ds-button type="submit" icon="save" primary :disabled="checkFormError(errors)" fill>
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="filled"
|
||||
type="submit"
|
||||
:disabled="checkFormError(errors)"
|
||||
>
|
||||
<template #icon><base-icon name="save" /></template>
|
||||
{{ update ? $t('group.update') : $t('group.save') }}
|
||||
</ds-button>
|
||||
</os-button>
|
||||
</ds-space>
|
||||
</template>
|
||||
</ds-form>
|
||||
|
||||
@ -34,7 +34,7 @@ describe('HashtagsFilter.vue', () => {
|
||||
|
||||
describe('click clear search button', () => {
|
||||
it('emits clearSearch', () => {
|
||||
wrapper.find('.base-button').trigger('click')
|
||||
wrapper.find('button[data-test="clear-search-button"]').trigger('click')
|
||||
expect(wrapper.emitted().clearSearch).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,17 +1,27 @@
|
||||
<template>
|
||||
<base-card class="hashtags-filter">
|
||||
<h2>{{ $t('hashtags-filter.hashtag-search', { hashtag }) }}</h2>
|
||||
<base-button
|
||||
icon="close"
|
||||
<os-button
|
||||
data-test="clear-search-button"
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
:title="this.$t('hashtags-filter.clearSearch')"
|
||||
:title="$t('hashtags-filter.clearSearch')"
|
||||
:aria-label="$t('hashtags-filter.clearSearch')"
|
||||
@click="clearSearch"
|
||||
/>
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="close" />
|
||||
</template>
|
||||
</os-button>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
|
||||
export default {
|
||||
components: { OsButton },
|
||||
props: {
|
||||
hashtag: {
|
||||
type: String,
|
||||
|
||||
@ -139,12 +139,18 @@
|
||||
</div>
|
||||
</client-only>
|
||||
<!-- hamburger menu -->
|
||||
<base-button
|
||||
icon="bars"
|
||||
@click="toggleMobileMenuView"
|
||||
<os-button
|
||||
variant="primary"
|
||||
:appearance="toggleMobileMenu ? 'filled' : 'outline'"
|
||||
circle
|
||||
class="hamburger-button"
|
||||
/>
|
||||
:aria-label="$t('site.navigation')"
|
||||
@click="toggleMobileMenuView"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="bars" />
|
||||
</template>
|
||||
</os-button>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
<!-- search, filter -->
|
||||
@ -274,6 +280,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { mapGetters } from 'vuex'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import { SHOW_GROUP_BUTTON_IN_HEADER } from '~/constants/groups.js'
|
||||
@ -298,6 +305,7 @@ import GetCategories from '~/mixins/getCategoriesMixin.js'
|
||||
export default {
|
||||
mixins: [GetCategories],
|
||||
components: {
|
||||
OsButton,
|
||||
AvatarMenu,
|
||||
ChatNotificationMenu,
|
||||
CustomButton,
|
||||
@ -359,6 +367,9 @@ export default {
|
||||
mounted() {
|
||||
window.addEventListener('scroll', this.handleScroll)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,16 +1,21 @@
|
||||
<template>
|
||||
<dropdown class="invite-button" offset="8" :placement="placement" noMouseLeaveClosing>
|
||||
<template #default="{ toggleMenu }">
|
||||
<base-button
|
||||
icon="user-plus"
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
ghost
|
||||
:aria-label="$t('invite-codes.button.tooltip')"
|
||||
v-tooltip="{
|
||||
content: $t('invite-codes.button.tooltip'),
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
@click.prevent="toggleMenu"
|
||||
/>
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="user-plus" />
|
||||
</template>
|
||||
</os-button>
|
||||
</template>
|
||||
<template #popover>
|
||||
<div class="invite-list">
|
||||
@ -31,6 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import InvitationList from '~/components/_new/features/Invitations/InvitationList.vue'
|
||||
@ -38,6 +44,7 @@ import { generatePersonalInviteCode, invalidateInviteCode } from '~/graphql/Invi
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OsButton,
|
||||
Dropdown,
|
||||
InvitationList,
|
||||
},
|
||||
@ -66,7 +73,7 @@ export default {
|
||||
},
|
||||
update: (_, { data: { generatePersonalInviteCode } }) => {
|
||||
this.setCurrentUser({
|
||||
...this.currentUser,
|
||||
...this.user,
|
||||
inviteCodes: [...this.user.inviteCodes, generatePersonalInviteCode],
|
||||
})
|
||||
},
|
||||
@ -85,7 +92,7 @@ export default {
|
||||
},
|
||||
update: (_, { data: { _invalidateInviteCode } }) => {
|
||||
this.setCurrentUser({
|
||||
...this.currentUser,
|
||||
...this.user,
|
||||
inviteCodes: this.user.inviteCodes.map((inviteCode) => ({
|
||||
...inviteCode,
|
||||
isValid: inviteCode.code === code ? false : inviteCode.isValid,
|
||||
@ -103,10 +110,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.invite-button {
|
||||
color: $color-secondary;
|
||||
}
|
||||
|
||||
.invite-list {
|
||||
max-width: min(400px, 90vw);
|
||||
padding: $space-small;
|
||||
|
||||
@ -37,7 +37,7 @@ describe('LoginButton.vue', () => {
|
||||
})
|
||||
|
||||
it('open popup', () => {
|
||||
wrapper.find('.base-button').trigger('click')
|
||||
wrapper.find('[data-test="login-btn"]').trigger('click')
|
||||
expect(wrapper.find('.login-button').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,18 @@
|
||||
<template>
|
||||
<dropdown class="login-button" offset="8" :placement="placement">
|
||||
<template #default="{ toggleMenu }">
|
||||
<base-button icon="sign-in" circle ghost @click.prevent="toggleMenu" />
|
||||
<os-button
|
||||
data-test="login-btn"
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
:aria-label="$t('login.login')"
|
||||
@click.prevent="toggleMenu"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="sign-in" />
|
||||
</template>
|
||||
</os-button>
|
||||
</template>
|
||||
<template #popover>
|
||||
<div class="login-button-menu-popover">
|
||||
@ -15,10 +26,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OsButton,
|
||||
Dropdown,
|
||||
},
|
||||
props: {
|
||||
|
||||
@ -36,9 +36,17 @@
|
||||
<nuxt-link to="/password-reset/request">
|
||||
{{ $t('login.forgotPassword') }}
|
||||
</nuxt-link>
|
||||
<base-button :loading="pending" filled name="submit" type="submit" icon="sign-in">
|
||||
<os-button
|
||||
:loading="pending"
|
||||
variant="primary"
|
||||
appearance="filled"
|
||||
full-width
|
||||
name="submit"
|
||||
type="submit"
|
||||
>
|
||||
<template #icon><base-icon name="sign-in" /></template>
|
||||
{{ $t('login.login') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
<p>
|
||||
{{ $t('login.no-account') }}
|
||||
<nuxt-link to="/registration">{{ $t('login.register') }}</nuxt-link>
|
||||
@ -58,12 +66,14 @@ import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParams
|
||||
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
|
||||
import Logo from '~/components/Logo/Logo'
|
||||
import ShowPassword from '../ShowPassword/ShowPassword.vue'
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LocaleSwitch,
|
||||
Logo,
|
||||
OsButton,
|
||||
PageParamsLink,
|
||||
ShowPassword,
|
||||
},
|
||||
@ -134,9 +144,7 @@ export default {
|
||||
max-width: 620px;
|
||||
margin: auto;
|
||||
|
||||
.base-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
button[type='submit'] {
|
||||
margin-top: $space-large;
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
|
||||
@ -1,30 +1,41 @@
|
||||
<template>
|
||||
<div>
|
||||
<nuxt-link to="/map">
|
||||
<base-button
|
||||
<os-button
|
||||
class="map-button"
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
ghost
|
||||
:aria-label="$t('header.map.tooltip')"
|
||||
v-tooltip="{
|
||||
content: $t('header.map.tooltip'),
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
>
|
||||
<base-icon name="globe-detailed" size="large" />
|
||||
</base-button>
|
||||
<template #icon>
|
||||
<base-icon name="globe-detailed" size="large" />
|
||||
</template>
|
||||
</os-button>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
|
||||
export default {
|
||||
name: 'MapButton',
|
||||
components: { OsButton },
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.map-button {
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
margin-left: 3px;
|
||||
margin-right: 3px;
|
||||
|
||||
.base-icon > .svg.--large {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -10,36 +10,44 @@
|
||||
<p v-html="message" />
|
||||
|
||||
<template #footer>
|
||||
<base-button
|
||||
<os-button
|
||||
class="cancel"
|
||||
:danger="!modalData.buttons.confirm.danger"
|
||||
:icon="modalData.buttons.cancel.icon"
|
||||
:variant="!modalData.buttons.confirm.danger ? 'danger' : 'primary'"
|
||||
appearance="outline"
|
||||
@click="cancel"
|
||||
data-test="cancel-button"
|
||||
>
|
||||
<template v-if="modalData.buttons.cancel.icon" #icon>
|
||||
<base-icon :name="modalData.buttons.cancel.icon" />
|
||||
</template>
|
||||
{{ $t(modalData.buttons.cancel.textIdent) }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
|
||||
<base-button
|
||||
:danger="modalData.buttons.confirm.danger"
|
||||
<os-button
|
||||
class="confirm"
|
||||
:icon="modalData.buttons.confirm.icon"
|
||||
:variant="modalData.buttons.confirm.danger ? 'danger' : 'primary'"
|
||||
appearance="filled"
|
||||
:loading="loading"
|
||||
@click="confirm"
|
||||
data-test="confirm-button"
|
||||
>
|
||||
<template v-if="modalData.buttons.confirm.icon" #icon>
|
||||
<base-icon :name="modalData.buttons.confirm.icon" />
|
||||
</template>
|
||||
{{ $t(modalData.buttons.confirm.textIdent) }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</template>
|
||||
</ds-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||
|
||||
export default {
|
||||
name: 'ConfirmModal',
|
||||
components: {
|
||||
OsButton,
|
||||
SweetalertIcon,
|
||||
},
|
||||
emits: ['close'],
|
||||
|
||||
@ -40,9 +40,16 @@
|
||||
<os-button variant="primary" appearance="outline" class="cancel" @click="cancel">
|
||||
{{ $t('actions.cancel') }}
|
||||
</os-button>
|
||||
<base-button danger filled class="confirm" icon="exclamation-circle" @click="openModal">
|
||||
<os-button
|
||||
variant="danger"
|
||||
appearance="filled"
|
||||
class="confirm"
|
||||
:loading="loading"
|
||||
@click="openModal"
|
||||
>
|
||||
<template #icon><base-icon name="exclamation-circle" /></template>
|
||||
{{ $t('settings.deleteUserAccount.name') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</template>
|
||||
</ds-modal>
|
||||
</template>
|
||||
@ -162,10 +169,10 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ds-modal {
|
||||
.delete-user-modal.ds-modal {
|
||||
max-width: 700px !important;
|
||||
}
|
||||
.hc-modal-success {
|
||||
.delete-user-modal .hc-modal-success {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
@ -177,7 +184,7 @@ export default {
|
||||
z-index: $z-index-modal;
|
||||
border-radius: $border-radius-x-large;
|
||||
}
|
||||
.bold {
|
||||
.delete-user-modal .bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -7,9 +7,16 @@
|
||||
<os-button variant="primary" appearance="outline" class="cancel" @click="cancel">
|
||||
{{ $t('disable.cancel') }}
|
||||
</os-button>
|
||||
<base-button danger filled class="confirm" icon="exclamation-circle" @click="confirm">
|
||||
<os-button
|
||||
variant="danger"
|
||||
appearance="filled"
|
||||
class="confirm"
|
||||
:loading="loading"
|
||||
@click="confirm"
|
||||
>
|
||||
<template #icon><base-icon name="exclamation-circle" /></template>
|
||||
{{ $t('disable.submit') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</template>
|
||||
</ds-modal>
|
||||
</template>
|
||||
@ -52,6 +59,7 @@ export default {
|
||||
}, 1000)
|
||||
},
|
||||
async confirm() {
|
||||
this.loading = true
|
||||
try {
|
||||
// TODO: Use the "modalData" structure introduced in "ConfirmModal" and refactor this here. Be aware that all the Jest tests have to be refactored as well !!!
|
||||
// await this.modalData.buttons.confirm.callback()
|
||||
@ -73,6 +81,8 @@ export default {
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
this.isOpen = false
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ds-modal :title="title" :is-open="isOpen" @cancel="cancel">
|
||||
<ds-modal class="report-modal" :title="title" :is-open="isOpen" @cancel="cancel">
|
||||
<transition name="ds-transition-fade">
|
||||
<ds-flex v-if="success" class="hc-modal-success" centered>
|
||||
<sweetalert-icon icon="success" />
|
||||
@ -29,26 +29,32 @@
|
||||
</small>
|
||||
<ds-space />
|
||||
<template #footer>
|
||||
<base-button class="cancel" icon="close" @click="cancel">
|
||||
<os-button class="cancel" variant="primary" appearance="outline" @click="cancel">
|
||||
<template #icon>
|
||||
<base-icon name="close" />
|
||||
</template>
|
||||
{{ $t('report.cancel') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
|
||||
<base-button
|
||||
danger
|
||||
filled
|
||||
<os-button
|
||||
class="confirm"
|
||||
icon="exclamation-circle"
|
||||
variant="danger"
|
||||
appearance="filled"
|
||||
:disabled="!form.reasonCategory"
|
||||
:loading="loading"
|
||||
@click="confirm"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="exclamation-circle" />
|
||||
</template>
|
||||
{{ $t('report.submit') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</template>
|
||||
</ds-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||
import { reportMutation } from '~/graphql/Moderation.js'
|
||||
import { valuesReasonCategoryOptions } from '~/constants/modals.js'
|
||||
@ -57,6 +63,7 @@ import validReport from '~/components/utils/ReportModal'
|
||||
export default {
|
||||
name: 'ReportModal',
|
||||
components: {
|
||||
OsButton,
|
||||
SweetalertIcon,
|
||||
},
|
||||
props: {
|
||||
@ -159,26 +166,27 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ds-modal {
|
||||
max-width: 600px !important;
|
||||
.report-modal.ds-modal {
|
||||
width: 700px !important;
|
||||
max-width: 700px !important;
|
||||
}
|
||||
.ds-radio-option {
|
||||
.report-modal .ds-radio-option {
|
||||
width: 100% !important;
|
||||
}
|
||||
.ds-radio-option-label {
|
||||
.report-modal .ds-radio-option-label {
|
||||
margin: 5px 20px 5px 5px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.reason-description {
|
||||
.report-modal .reason-description {
|
||||
margin-top: $space-x-small !important;
|
||||
margin-bottom: $space-xx-small !important;
|
||||
}
|
||||
.smallTag {
|
||||
.report-modal .smallTag {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
left: 90%;
|
||||
}
|
||||
.hc-modal-success {
|
||||
.report-modal .hc-modal-success {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
||||
@ -4,27 +4,36 @@
|
||||
class="notifications-menu"
|
||||
:to="{ name: 'notifications' }"
|
||||
>
|
||||
<base-button
|
||||
icon="bell"
|
||||
ghost
|
||||
circle
|
||||
v-tooltip="{
|
||||
content: $t('header.notifications.tooltip'),
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
/>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-else-if="noMenu" class="notifications-menu" :to="{ name: 'notifications' }">
|
||||
<base-button
|
||||
ghost
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
:aria-label="$t('header.notifications.tooltip')"
|
||||
v-tooltip="{
|
||||
content: $t('header.notifications.tooltip'),
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
>
|
||||
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
|
||||
</base-button>
|
||||
<template #icon>
|
||||
<base-icon name="bell" />
|
||||
</template>
|
||||
</os-button>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-else-if="noMenu" class="notifications-menu" :to="{ name: 'notifications' }">
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
:aria-label="$t('header.notifications.tooltip')"
|
||||
v-tooltip="{
|
||||
content: $t('header.notifications.tooltip'),
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
>
|
||||
<template #icon>
|
||||
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
|
||||
</template>
|
||||
</os-button>
|
||||
</nuxt-link>
|
||||
<dropdown
|
||||
v-else
|
||||
@ -35,17 +44,21 @@
|
||||
ref="dropdown"
|
||||
>
|
||||
<template #default="{ toggleMenu }">
|
||||
<base-button
|
||||
ghost
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
:aria-label="$t('header.notifications.tooltip')"
|
||||
v-tooltip="{
|
||||
content: $t('header.notifications.tooltip'),
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
|
||||
</base-button>
|
||||
<template #icon>
|
||||
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
|
||||
</template>
|
||||
</os-button>
|
||||
</template>
|
||||
<template #popover="{ closeMenu }">
|
||||
<ds-flex class="notifications-link-container">
|
||||
|
||||
@ -41,10 +41,6 @@ describe('ChangePassword.vue', () => {
|
||||
})
|
||||
|
||||
describe('validations', () => {
|
||||
it('invalid', () => {
|
||||
expect(wrapper.vm.disabled).toBe(true)
|
||||
})
|
||||
|
||||
describe('old password and new password', () => {
|
||||
describe('match', () => {
|
||||
beforeEach(() => {
|
||||
@ -52,10 +48,6 @@ describe('ChangePassword.vue', () => {
|
||||
wrapper.find('input#password').setValue('some secret')
|
||||
})
|
||||
|
||||
it('invalid', () => {
|
||||
expect(wrapper.vm.disabled).toBe(true)
|
||||
})
|
||||
|
||||
it.skip('displays a warning', () => {
|
||||
const calls = mocks.validate.mock.calls
|
||||
const expected = [['change-password.validations.old-and-new-password-match']]
|
||||
|
||||
@ -24,15 +24,22 @@
|
||||
/>
|
||||
<password-strength :password="formData.password" />
|
||||
<ds-space margin-top="base">
|
||||
<base-button :loading="loading" :disabled="errors" filled type="submit">
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="filled"
|
||||
:loading="loading"
|
||||
:disabled="!!errors"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('settings.security.change-password.button') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</ds-space>
|
||||
</template>
|
||||
</ds-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import gql from 'graphql-tag'
|
||||
import PasswordStrength from './Strength'
|
||||
import PasswordForm from '~/components/utils/PasswordFormHelper'
|
||||
@ -40,6 +47,7 @@ import PasswordForm from '~/components/utils/PasswordFormHelper'
|
||||
export default {
|
||||
name: 'ChangePassword',
|
||||
components: {
|
||||
OsButton,
|
||||
PasswordStrength,
|
||||
},
|
||||
data() {
|
||||
@ -58,7 +66,6 @@ export default {
|
||||
...passwordForm.formSchema,
|
||||
},
|
||||
loading: false,
|
||||
disabled: true,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@ -24,9 +24,15 @@
|
||||
/>
|
||||
<password-strength :password="formData.password" />
|
||||
<ds-space margin-top="base" margin-bottom="xxx-small">
|
||||
<base-button :loading="$apollo.loading" :disabled="errors" filled type="submit">
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="filled"
|
||||
:loading="$apollo.loading"
|
||||
:disabled="!!errors"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('settings.security.change-password.button') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</ds-space>
|
||||
</template>
|
||||
</ds-form>
|
||||
@ -61,6 +67,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import emails from '../../constants/emails.js'
|
||||
import PasswordStrength from '../Password/Strength'
|
||||
import gql from 'graphql-tag'
|
||||
@ -69,6 +76,7 @@ import PasswordForm from '~/components/utils/PasswordFormHelper'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OsButton,
|
||||
SweetalertIcon,
|
||||
PasswordStrength,
|
||||
},
|
||||
|
||||
@ -20,16 +20,15 @@
|
||||
<ds-space margin-botton="large">
|
||||
<ds-text align="left">{{ $t('components.password-reset.request.form.description') }}</ds-text>
|
||||
</ds-space>
|
||||
<base-button
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="filled"
|
||||
:disabled="disabled"
|
||||
:loading="$apollo.loading"
|
||||
filled
|
||||
padding
|
||||
name="submit"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('components.password-reset.request.form.submit') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
<slot></slot>
|
||||
</ds-form>
|
||||
<div v-else>
|
||||
@ -43,11 +42,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import gql from 'graphql-tag'
|
||||
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OsButton,
|
||||
SweetalertIcon,
|
||||
},
|
||||
data() {
|
||||
|
||||
@ -30,15 +30,15 @@
|
||||
model="email"
|
||||
name="email"
|
||||
/>
|
||||
<base-button
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="filled"
|
||||
:disabled="disabled"
|
||||
:loading="$apollo.loading"
|
||||
filled
|
||||
name="submit"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('components.registration.signup.form.submit') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
<slot></slot>
|
||||
</ds-form>
|
||||
</ds-space>
|
||||
@ -62,6 +62,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import gql from 'graphql-tag'
|
||||
import metadata from '~/constants/metadata'
|
||||
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||
@ -77,6 +78,7 @@ export const SignupMutation = gql`
|
||||
export default {
|
||||
name: 'Signup',
|
||||
components: {
|
||||
OsButton,
|
||||
SweetalertIcon,
|
||||
},
|
||||
props: {
|
||||
|
||||
@ -7,9 +7,18 @@
|
||||
<os-button variant="primary" appearance="outline" class="cancel" @click="cancel">
|
||||
{{ $t('release.cancel') }}
|
||||
</os-button>
|
||||
<base-button danger filled class="confirm" icon="exclamation-circle" @click="confirm">
|
||||
<os-button
|
||||
variant="danger"
|
||||
appearance="filled"
|
||||
class="confirm"
|
||||
:loading="loading"
|
||||
@click="confirm"
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="exclamation-circle" />
|
||||
</template>
|
||||
{{ $t('release.submit') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</template>
|
||||
</ds-modal>
|
||||
</template>
|
||||
@ -52,6 +61,7 @@ export default {
|
||||
}, 1000)
|
||||
},
|
||||
async confirm() {
|
||||
this.loading = true
|
||||
try {
|
||||
// TODO: Use the "modalData" structure introduced in "ConfirmModal" and refactor this here. Be aware that all the Jest tests have to be refactored as well !!!
|
||||
// await this.modalData.buttons.confirm.callback()
|
||||
@ -73,6 +83,11 @@ export default {
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
this.isOpen = false
|
||||
setTimeout(() => {
|
||||
this.$emit('close')
|
||||
}, 1000)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@ -49,7 +49,7 @@ describe('LocationSelect', () => {
|
||||
})
|
||||
|
||||
it('renders the clearLocationName button by default', () => {
|
||||
expect(wrapper.find('.base-button').exists()).toBe(true)
|
||||
expect(wrapper.find('button[data-test="clear-location-button"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('calls apollo with given value', () => {
|
||||
@ -64,7 +64,7 @@ describe('LocationSelect', () => {
|
||||
|
||||
describe('clearLocationName button click', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.find('.base-button').trigger('click')
|
||||
wrapper.find('button[data-test="clear-location-button"]').trigger('click')
|
||||
})
|
||||
|
||||
it('emits an empty string', () => {
|
||||
@ -81,7 +81,7 @@ describe('LocationSelect', () => {
|
||||
})
|
||||
|
||||
it('does not show clear location name button', () => {
|
||||
expect(wrapper.find('.base-button').exists()).toBe(false)
|
||||
expect(wrapper.find('button[data-test="clear-location-button"]').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -13,24 +13,30 @@
|
||||
:loading="loadingGeo"
|
||||
@input.native="handleCityInput"
|
||||
/>
|
||||
<base-button
|
||||
<os-button
|
||||
v-if="locationName !== '' && canBeCleared"
|
||||
icon="close"
|
||||
ghost
|
||||
size="small"
|
||||
style="position: relative; display: inline-block; right: -94%; top: -48px; width: 29px"
|
||||
@click.native="clearLocationName"
|
||||
></base-button>
|
||||
data-test="clear-location-button"
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
size="sm"
|
||||
:aria-label="$t('actions.clear')"
|
||||
style="right: -94%; top: -48px"
|
||||
@click="clearLocationName"
|
||||
>
|
||||
<template #icon><base-icon name="close" /></template>
|
||||
</os-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { queryLocations } from '~/graphql/location'
|
||||
|
||||
let timeout
|
||||
|
||||
export default {
|
||||
name: 'LocationSelect',
|
||||
components: { OsButton },
|
||||
props: {
|
||||
value: {
|
||||
required: true,
|
||||
@ -128,9 +134,8 @@ export default {
|
||||
this.loadingGeo = false
|
||||
}
|
||||
},
|
||||
clearLocationName(event) {
|
||||
event.target.value = ''
|
||||
this.$emit('input', event.target.value)
|
||||
clearLocationName() {
|
||||
this.currentValue = ''
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -14,16 +14,20 @@
|
||||
</div>
|
||||
</vue-dropzone>
|
||||
<div v-show="!showCropper && hasImage">
|
||||
<base-button
|
||||
<os-button
|
||||
class="delete-image-button"
|
||||
icon="trash"
|
||||
variant="danger"
|
||||
appearance="filled"
|
||||
circle
|
||||
danger
|
||||
filled
|
||||
data-test="delete-button"
|
||||
:title="$t('actions.delete')"
|
||||
:aria-label="$t('actions.delete')"
|
||||
@click.stop="deleteImage"
|
||||
/>
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="trash" />
|
||||
</template>
|
||||
</os-button>
|
||||
</div>
|
||||
<div v-show="!showCropper && imageCanBeCropped" class="crop-overlay">
|
||||
<os-button class="crop-confirm" variant="primary" @click="initCropper">
|
||||
@ -35,15 +39,20 @@
|
||||
<os-button class="crop-confirm" variant="primary" @click="cropImage">
|
||||
{{ $t('contribution.teaserImage.cropperConfirm') }}
|
||||
</os-button>
|
||||
<base-button
|
||||
<os-button
|
||||
class="crop-cancel"
|
||||
icon="close"
|
||||
size="small"
|
||||
variant="danger"
|
||||
appearance="filled"
|
||||
circle
|
||||
danger
|
||||
filled
|
||||
size="sm"
|
||||
:title="$t('actions.cancel')"
|
||||
:aria-label="$t('actions.cancel')"
|
||||
@click="closeCropper"
|
||||
/>
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="close" />
|
||||
</template>
|
||||
</os-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -206,17 +215,17 @@ export default {
|
||||
}
|
||||
|
||||
> .crop-cancel {
|
||||
position: absolute;
|
||||
right: $space-x-small;
|
||||
top: $space-x-small;
|
||||
position: absolute !important;
|
||||
right: $space-x-small !important;
|
||||
top: $space-x-small !important;
|
||||
z-index: $z-index-surface;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-image-button {
|
||||
position: absolute;
|
||||
top: $space-small;
|
||||
right: $space-small;
|
||||
position: absolute !important;
|
||||
top: $space-small !important;
|
||||
right: $space-small !important;
|
||||
z-index: $z-index-surface;
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -247,13 +256,6 @@ export default {
|
||||
opacity: $opacity-soft;
|
||||
}
|
||||
|
||||
> .base-button {
|
||||
position: absolute;
|
||||
top: $space-small;
|
||||
right: $space-small;
|
||||
z-index: $z-index-surface;
|
||||
}
|
||||
|
||||
> .supported-formats {
|
||||
margin-top: 150px;
|
||||
font-weight: bold;
|
||||
|
||||
@ -7,18 +7,25 @@ exports[`ActionButton.vue when disabled renders 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="Click me"
|
||||
class="base-button --icon-only --circle"
|
||||
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px]"
|
||||
data-appearance="outline"
|
||||
data-variant="primary"
|
||||
disabled="disabled"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<div
|
||||
@ -37,17 +44,24 @@ exports[`ActionButton.vue when not disabled renders 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="Click me"
|
||||
class="base-button --icon-only --circle"
|
||||
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px]"
|
||||
data-appearance="outline"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<div
|
||||
|
||||
@ -8,17 +8,24 @@ exports[`ObserveButton observed renders 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="observeButton.observed"
|
||||
class="base-button --icon-only --circle --filled"
|
||||
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.05)] disabled:bg-[var(--color-disabled)] disabled:text-[var(--color-disabled-contrast)] disabled:border-[var(--color-disabled)] disabled:shadow-[inset_0_0_0_1px_transparent] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle bg-[var(--color-primary)] text-[var(--color-primary-contrast)] border-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] hover:border-[var(--color-primary-hover)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px]"
|
||||
data-appearance="filled"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<div
|
||||
@ -38,17 +45,24 @@ exports[`ObserveButton unobserved renders 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="observeButton.observed"
|
||||
class="base-button --icon-only --circle"
|
||||
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px]"
|
||||
data-appearance="outline"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<div
|
||||
|
||||
@ -7,17 +7,24 @@ exports[`ShoutButton.vue renders button and text 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="shoutButton.shouted"
|
||||
class="base-button --icon-only --circle"
|
||||
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px]"
|
||||
data-appearance="outline"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<div
|
||||
@ -36,17 +43,24 @@ exports[`ShoutButton.vue toggle the button 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="shoutButton.shouted"
|
||||
class="base-button --icon-only --circle --filled"
|
||||
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.05)] disabled:bg-[var(--color-disabled)] disabled:text-[var(--color-disabled-contrast)] disabled:border-[var(--color-disabled)] disabled:shadow-[inset_0_0_0_1px_transparent] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle bg-[var(--color-primary)] text-[var(--color-primary-contrast)] border-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] hover:border-[var(--color-primary-hover)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px]"
|
||||
data-appearance="filled"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<div
|
||||
@ -65,17 +79,24 @@ exports[`ShoutButton.vue toggle the button, but backend fails 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="shoutButton.shouted"
|
||||
class="base-button --icon-only --circle --filled"
|
||||
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.05)] disabled:bg-[var(--color-disabled)] disabled:text-[var(--color-disabled-contrast)] disabled:border-[var(--color-disabled)] disabled:shadow-[inset_0_0_0_1px_transparent] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle bg-[var(--color-primary)] text-[var(--color-primary-contrast)] border-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] hover:border-[var(--color-primary-hover)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px]"
|
||||
data-appearance="filled"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<div
|
||||
@ -94,17 +115,24 @@ exports[`ShoutButton.vue when shouted renders 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="shoutButton.shouted"
|
||||
class="base-button --icon-only --circle --filled"
|
||||
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.05)] disabled:bg-[var(--color-disabled)] disabled:text-[var(--color-disabled-contrast)] disabled:border-[var(--color-disabled)] disabled:shadow-[inset_0_0_0_1px_transparent] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle bg-[var(--color-primary)] text-[var(--color-primary-contrast)] border-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] hover:border-[var(--color-primary-hover)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px]"
|
||||
data-appearance="filled"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<div
|
||||
|
||||
@ -8,21 +8,29 @@
|
||||
v-model="comment"
|
||||
:schema="{ type: 'string', max: 30 }"
|
||||
/>
|
||||
<base-button
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
circle
|
||||
class="generate-invite-code"
|
||||
:aria-label="$t('invite-codes.generate-code')"
|
||||
icon="plus"
|
||||
type="submit"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="plus" />
|
||||
</template>
|
||||
</os-button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
|
||||
export default {
|
||||
name: 'CreateInvitation',
|
||||
components: { OsButton },
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
|
||||
@ -16,33 +16,43 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<base-button
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
circle
|
||||
class="copy-button"
|
||||
icon="copy"
|
||||
@click="copyInviteCode(inviteCode.copy)"
|
||||
@click="copyInviteCode"
|
||||
:disabled="!canCopy"
|
||||
:aria-label="$t('invite-codes.copy-code')"
|
||||
/>
|
||||
<base-button
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="copy" />
|
||||
</template>
|
||||
</os-button>
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
circle
|
||||
class="invalidate-button"
|
||||
icon="trash"
|
||||
@click="openDeleteModal"
|
||||
:aria-label="$t('invite-codes.invalidate')"
|
||||
/>
|
||||
>
|
||||
<template #icon>
|
||||
<base-icon name="trash" />
|
||||
</template>
|
||||
</os-button>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { mapMutations } from 'vuex'
|
||||
import BaseButton from '~/components/_new/generic/BaseButton/BaseButton.vue'
|
||||
|
||||
export default {
|
||||
name: 'Invitation',
|
||||
components: {
|
||||
BaseButton,
|
||||
OsButton,
|
||||
},
|
||||
props: {
|
||||
inviteCode: {
|
||||
|
||||
@ -52,17 +52,24 @@ exports[`CreateInvitation.vue renders 1`] = `
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.generate-code"
|
||||
class="generate-invite-code base-button --icon-only --circle"
|
||||
class="generate-invite-code inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] generate-invite-code"
|
||||
data-appearance="outline"
|
||||
data-variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -122,17 +129,24 @@ exports[`CreateInvitation.vue renders with disabled button 1`] = `
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.generate-code"
|
||||
class="generate-invite-code base-button --icon-only --circle"
|
||||
class="generate-invite-code inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] generate-invite-code"
|
||||
data-appearance="outline"
|
||||
data-variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -41,32 +41,46 @@ exports[`Invitation.vue when the invite code was not redeemed renders 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="invite-codes.copy-code"
|
||||
class="copy-button base-button --icon-only --circle"
|
||||
class="copy-button inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] copy-button"
|
||||
data-appearance="outline"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.invalidate"
|
||||
class="invalidate-button base-button --icon-only --circle"
|
||||
class="invalidate-button inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] invalidate-button"
|
||||
data-appearance="outline"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
@ -114,32 +128,46 @@ exports[`Invitation.vue when the invite code was redeemed renders 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="invite-codes.copy-code"
|
||||
class="copy-button base-button --icon-only --circle"
|
||||
class="copy-button inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] copy-button"
|
||||
data-appearance="outline"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.invalidate"
|
||||
class="invalidate-button base-button --icon-only --circle"
|
||||
class="invalidate-button inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] invalidate-button"
|
||||
data-appearance="outline"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@ -46,32 +46,46 @@ exports[`InvitationList.vue renders 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="invite-codes.copy-code"
|
||||
class="copy-button base-button --icon-only --circle"
|
||||
class="copy-button inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] copy-button"
|
||||
data-appearance="outline"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.invalidate"
|
||||
class="invalidate-button base-button --icon-only --circle"
|
||||
class="invalidate-button inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] invalidate-button"
|
||||
data-appearance="outline"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
@ -114,32 +128,46 @@ exports[`InvitationList.vue renders 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="invite-codes.copy-code"
|
||||
class="copy-button base-button --icon-only --circle"
|
||||
class="copy-button inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] copy-button"
|
||||
data-appearance="outline"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.invalidate"
|
||||
class="invalidate-button base-button --icon-only --circle"
|
||||
class="invalidate-button inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] invalidate-button"
|
||||
data-appearance="outline"
|
||||
data-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
@ -196,17 +224,24 @@ exports[`InvitationList.vue renders 1`] = `
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.generate-code"
|
||||
class="generate-invite-code base-button --icon-only --circle"
|
||||
class="generate-invite-code inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] generate-invite-code"
|
||||
data-appearance="outline"
|
||||
data-variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -277,17 +312,24 @@ exports[`InvitationList.vue renders empty state 1`] = `
|
||||
|
||||
<button
|
||||
aria-label="invite-codes.generate-code"
|
||||
class="generate-invite-code base-button --icon-only --circle"
|
||||
class="generate-invite-code inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] generate-invite-code"
|
||||
data-appearance="outline"
|
||||
data-variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<!---->
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<span
|
||||
class="base-icon"
|
||||
>
|
||||
<!---->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -78,23 +78,23 @@ describe('MySomethingList.vue', () => {
|
||||
})
|
||||
|
||||
it('displays the edit button', () => {
|
||||
expect(wrapper.find('.base-button[data-test="edit-button"]').exists()).toBe(true)
|
||||
expect(wrapper.find('button[data-test="edit-button"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('displays the delete button', () => {
|
||||
expect(wrapper.find('.base-button[data-test="delete-button"]').exists()).toBe(true)
|
||||
expect(wrapper.find('button[data-test="delete-button"]').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('editing item', () => {
|
||||
beforeEach(async () => {
|
||||
const editButton = wrapper.find('.base-button[data-test="edit-button"]')
|
||||
const editButton = wrapper.find('button[data-test="edit-button"]')
|
||||
editButton.trigger('click')
|
||||
await Vue.nextTick()
|
||||
})
|
||||
|
||||
it('disables adding items while editing', () => {
|
||||
const submitButton = wrapper.find('.base-button[data-test="add-save-button"]')
|
||||
const submitButton = wrapper.find('button[data-test="add-save-button"]')
|
||||
expect(submitButton.text()).not.toContain('settings.social-media.submit')
|
||||
})
|
||||
|
||||
@ -109,7 +109,7 @@ describe('MySomethingList.vue', () => {
|
||||
|
||||
describe('calls callback functions', () => {
|
||||
it('calls edit', async () => {
|
||||
const editButton = wrapper.find('.base-button[data-test="edit-button"]')
|
||||
const editButton = wrapper.find('button[data-test="edit-button"]')
|
||||
editButton.trigger('click')
|
||||
await Vue.nextTick()
|
||||
const expectedItem = expect.objectContaining({ id: 'id', dummy: 'dummy' })
|
||||
@ -134,7 +134,7 @@ describe('MySomethingList.vue', () => {
|
||||
})
|
||||
|
||||
it('calls delete by committing "modal/SET_OPEN"', async () => {
|
||||
const deleteButton = wrapper.find('.base-button[data-test="delete-button"]')
|
||||
const deleteButton = wrapper.find('button[data-test="delete-button"]')
|
||||
deleteButton.trigger('click')
|
||||
await Vue.nextTick()
|
||||
const expectedModalData = expect.objectContaining({
|
||||
|
||||