refactor(webapp): vue3 migration os button as prop, remove obsolete buttons & inline single user buttons (#9214)

This commit is contained in:
Ulf Gebhardt 2026-02-15 20:30:02 +01:00 committed by GitHub
parent 8b22235b30
commit b60e270f4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 640 additions and 969 deletions

View File

@ -1,6 +1,6 @@
import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
defineStep('the unread counter is removed', () => {
cy.get('.notifications-menu .counter-icon')
cy.get('.notifications-menu .counter-icon .count')
.should('not.exist')
})

View File

@ -106,114 +106,47 @@ OsButton Features:
├─ variant: ✅ primary, secondary, danger, warning, success, info, default
├─ appearance: ✅ filled, outline, ghost
├─ size: ✅ sm, md, lg, xl
├─ disabled: ✅ mit hover/active-Override
├─ disabled: ✅ mit hover/active-Override (nur as="button")
├─ 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)
├─ loading: ✅ animated SVG spinner, aria-busy (Milestone 4b)
└─ as: ✅ polymorphes Rendering (button/a/NuxtLink/RouterLink)
as-Prop Migration: 15 <nuxt-link>/<a>-Wrapper in 15 Webapp-Dateien → as="nuxt-link"/as="a"
```
---
## Aktueller Stand
**Letzte Aktualisierung:** 2026-02-13 (Session 19)
**Letzte Aktualisierung:** 2026-02-14 (Session 20)
**Aktuelle Phase:** Phase 3 ✅ ABGESCHLOSSEN + Code-Review-Feedback eingearbeitet
**Aktuelle Phase:** Phase 3 ✅ ABGESCHLOSSEN + Code-Review-Feedback + `as`-Prop-Migration
**Zuletzt abgeschlossen:**
- [x] Projektordner erstellt
- [x] Planungsdokument erstellt
- [x] Tech-Stack entschieden
- [x] Branding-Architektur definiert
- [x] Migrationsstrategie definiert
- [x] **Phase 0: Komponenten-Analyse** (177 Komponenten katalogisiert)
- [x] Button-Familie detailiert analysiert (Props, Styles, Konsolidierung)
- [x] Modal-Familie detailiert analysiert (Architektur erkannt)
- [x] Menu-Familie detailiert analysiert (3 Patterns identifiziert)
- [x] Priorisierung erstellt (15 Komponenten in 4 Tiers)
- [x] Konsolidierungsplan finalisiert
- [x] **Phase 1: Vue 2.7 Upgrade**
- Vue 2.6.14 → 2.7.16
- vue-template-compiler entfernt
- @vue/composition-api entfernt
- @nuxtjs/composition-api entfernt
- Webpack-Alias für @vue/composition-api → vue
- Webpack-Regel für ESM .mjs Module
- **Unit-Tests: 157 Suites, 979 passed, 87 Snapshots**
- **Integrationstests: bestanden**
- [x] **Phase 2: Projekt-Setup**
- Vite + Vue 3 Projekt initialisiert
- vue-demi für Vue 2/3 Kompatibilität
- Vitest konfiguriert (integriert in vite.config.ts)
- npm Package-Struktur mit korrekten exports
- README.md Grundgerüst
- LICENSE (Apache 2.0)
- Plugin-Tests geschrieben
- Tailwind CSS v4 mit @tailwindcss/vite
- Dual-Build (style.css + tailwind.preset)
- Dark Mode Grundstruktur (via Tailwind dark: Prefix)
- Prop-Types definiert (Size, Rounded, Shadow, Variant)
- Branding-Architektur (keine Defaults, validateCssVariables)
- eslint-config-it4c eingerichtet (v0.8.0)
- ESLint Flat Config mit Vue 3 + Vitest Modulen
- Prettier-Integration via eslint-plugin-prettier
- GitHub Workflows (ui-lint.yml, ui-test.yml, ui-build.yml)
- 100% Test-Coverage Requirement
- .tool-versions (Node 25.5.0, konsistent mit Dockerfiles)
- Example Apps für Kompatibilitätstests (4er-Matrix)
- GitHub Workflow ui-compatibility.yml für Vue 2/3 Tests (inkl. Lint)
- Eigene ESLint + Prettier Configs für Example Apps
- Type Assertions für CI-Kompatibilität (`as unknown as Plugin`)
- Bundle Size Check (size-limit) mit ui-size.yml Workflow
- Package-Validierung (publint, arethetypeswrong) mit CJS/ESM Types
- Kompatibilitätstest-Workflow mit 4 Example Apps (Vue 2/3 × Tailwind/CSS)
- release-please Manifest-Konfiguration (Monorepo-Setup)
- npm Publish Workflow (ui-release.yml)
- CONTRIBUTING.md (Entwickler-Leitfaden)
- Dependabot für UI-Package und Example Apps konfiguriert
- CSS-Build separat via Tailwind CLI (closeBundle Hook)
- CVA (class-variance-authority) für typsichere Varianten
- cn() Utility für Tailwind-Klassen-Merge (clsx + tailwind-merge)
- OsButton Komponente mit CVA-Varianten implementiert
- ESLint-Konfiguration angepasst (vue/max-attributes-per-line, import-x/no-relative-parent-imports)
- Storybook 10 für Dokumentation eingerichtet (Wasserfarben-Theme)
- OsButton.stories.ts mit Playground + allen Varianten/Appearances/Sizes
- Storybook Build-Konfiguration (viteFinal entfernt Library-Plugins)
- Docker Setup (Dockerfile, docker-compose, ui-docker.yml)
- Visual Regression Tests (Playwright, colocated) mit integriertem A11y-Check
- Completeness Check (verify Script prüft Story, Visual, checkA11y, Keyboard, Varianten)
- ESLint Plugins: vuejs-accessibility, playwright, storybook, jsdoc
**Abgeschlossene Phasen:**
- [x] Phase 0: Analyse (177 Komponenten katalogisiert)
- [x] Phase 1: Vue 2.7 Upgrade (2.6.14 → 2.7.16, 979 Tests ✅)
- [x] Phase 2: Projekt-Setup (Vite, vue-demi, Tailwind v4, CVA, Storybook 10, CI/CD, 100% Coverage)
- [x] Phase 3: Webapp-Integration — 133 os-button in 79 Dateien, 0 base-button/ds-button verbleibend
**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 20 - `as`-Prop + nuxt-link Migration):**
- [x] OsButton: `as` Prop implementiert (polymorphe Komponente: `button`, `a`, `nuxt-link`, `router-link`, Custom-Komponenten)
- [x] Naming-Konvention: `tag``as` (moderner Standard: Headless UI, Radix Vue, Chakra UI, PrimeVue)
- [x] `disabled`/`type`/`loading` nur bei `as="button"` (Links haben kein natives `disabled`-Attribut)
- [x] Stories: `Polymorphic` Story + Playground `as`-Selektor (button/a)
- [x] Visual Test: `polymorphic` Screenshot + a11y-Check
- [x] 15 `<nuxt-link>`/`<a>`-Wrapper in 15 Webapp-Dateien → `as="nuxt-link"` / `as="a"` migriert:
- GroupButton.vue, CtaUnblockAuthor.vue, terms-and-conditions-confirm.vue
- CustomButton.vue (v-if/v-else → computed `linkTag`/`linkProps` konsolidiert)
- groups/index.vue, GroupForm.vue, admin/users/index.vue
- pages/index.vue (CSS `button.post-add-button-*``.post-add-button-*`)
- profile/_id/_slug.vue (v-if auf ds-grid-item, symmetrisches Padding `$space-x-small`)
- groups/_id/_slug.vue, MapButton.vue
- ChatNotificationMenu.vue, Chat.vue, UserTeaserPopover.vue
- NotificationMenu.vue (3 Instanzen, 2 zu einem Button konsolidiert via counter-icon)
- [x] Verifiziert: 0 verbleibende `<nuxt-link>`/`<a>`-Wrapper um `<os-button>` in Webapp
**Zuletzt abgeschlossen (Session 19 - CodeRabbit Review Feedback: Cleanup, Accessibility, Bugfixes):**
**Zuvor 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)
@ -253,7 +186,36 @@ OsButton Features:
- [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):**
**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
**Zuvor abgeschlossen (Session 17 - 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
@ -368,13 +330,13 @@ OsButton Features:
- [x] `data-appearance` Attribut: robuste CSS-Selektoren statt fragile escaped Tailwind-Klassen
- [x] Code-Review Feedback eingearbeitet (Unit-Tests, Testnamen, CSS-Selektoren)
**Zuvor abgeschlossen (Milestone 5 + Analyse):**
**Zuvor abgeschlossen (Sessions 9-10 - Milestone 5, Analyse, Disabled-Styles):**
- [x] Visuelle Validierung: 16/16 Buttons validiert ✅
- [x] OsButton Features: `appearance` (outline, ghost), `xs` size, focus/active states
- [x] Disabled-Styles: CSS-Variablen, hover/active-Override, Border-Fix
- [x] Codebase-Analyse: 14 weitere migrierbare Buttons identifiziert (Scope: 16/35)
**Zuletzt erledigt (Phase 3):**
**Zuvor abgeschlossen (Sessions 1-8 - Phase 3 Webapp-Integration):**
- [x] vue-demi zur Webapp hinzugefügt (Vue 2.7 Kompatibilität)
- [x] Webpack-Alias für vue-demi (nutzt Webapp's Vue 2.7 statt UI-Library's Vue 3)
- [x] Webpack-Alias für @ocelot-social/ui (dist Pfade mit $ für exakten Match)
@ -419,23 +381,11 @@ OsButton Features:
- Verhindert Layout-Sprung wenn Button disabled wird
**Nächste Schritte:**
1. ~~Phase 0: Komponenten-Analyse~~
2. ~~Phase 1: Vue 2.7 Upgrade~~
3. ~~**Phase 2: Projekt-Setup**~~ ✅ ABGESCHLOSSEN
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) ✅
- [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
- [ ] Snapshots/Tests aktualisieren (nach `as`-Prop-Migration)
- [ ] 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)
- [ ] BaseButton-Komponente ggf. entfernen
**Manuelle Setup-Aufgaben (außerhalb Code):**
- [ ] `NPM_TOKEN` als GitHub Secret einrichten (für npm publish in ui-release.yml)
@ -722,8 +672,6 @@ Jeder migrierte Button muss manuell geprüft werden: Normal, Hover, Focus, Activ
---
---
# VISION
## 1. Projektziel & Vision
@ -782,7 +730,7 @@ Migration vorbereiten - schrittweise neue Komponenten in Vue 3 entwickeln, die d
| Dateinamen | **PascalCase** | OsButton.vue, OsCard.vue |
| i18n | **Nur Props** | Keine Default-Texte in Komponenten |
| Breakpoints | **Tailwind Standard** | sm:640, md:768, lg:1024, xl:1280, 2xl:1536 |
| Size Props | **Tailwind-Skala (vollständig)** | xs, sm, md, lg, xl, 2xl |
| Size Props | **Tailwind-Skala** | sm, md, lg, xl (komponentenspezifisch) |
| Rounded Props | **Tailwind-Skala (vollständig)** | none, sm, md, lg, xl, 2xl, 3xl, full |
| Shadow Props | **Tailwind-Skala (vollständig)** | none, sm, md, lg, xl, 2xl |
| Variant Props | **Semantisch (vollständig)** | primary, secondary, danger, warning, success, info |
@ -1217,8 +1165,8 @@ Alle grün → Merge erlaubt
**Storybook Deploy (Webhook):**
1. GitHub sendet Webhook bei Release-Event
2. Server empfängt Webhook
3. Server führt `scripts/deploy-histoire.sh` aus (Teil des Repos)
4. Script: git pull → npm ci → histoire build → copy to webroot
3. Server führt `scripts/deploy-storybook.sh` aus (Teil des Repos)
4. Script: git pull → npm ci → storybook build → copy to webroot
### GitHub Workflows (vollständige Liste)
@ -1230,7 +1178,7 @@ Alle grün → Merge erlaubt
| **test-a11y** | Push/PR | axe-core | Accessibility-Tests |
| **test-visual** | Push/PR | Playwright | Visual Regression Screenshots |
| **build** | Push/PR | Vite | Build verifizieren |
| **build-histoire** | Push/PR | Storybook | Dokumentation bauen |
| **build-storybook** | Push/PR | Storybook | Dokumentation bauen |
| **size-check** | Push/PR | size-limit | Bundle-Größe prüfen |
| **release** | Push main | release-please | Release-PR erstellen |
| **publish** | Release | npm | Auf npm veröffentlichen |
@ -1288,7 +1236,7 @@ Die Komponenten werden über Storybook dokumentiert und auf einer öffentlichen
**Hosting:**
- Eigener Server (öffentlich zugänglich)
- Static Build via `histoire build`
- Static Build via `storybook build`
- Deployment bei jedem Release
**Workflow:**
@ -1403,7 +1351,7 @@ Bei der Migration werden:
## 11. Entscheidungen
> 70 Entscheidungen in 9 Kategorien
> 73 Entscheidungen in 9 Kategorien
### Vision & Ziele
@ -1472,7 +1420,7 @@ Bei der Migration werden:
| 29 | Dark Mode | Ja, von Anfang an | Alle Komponenten mit Light/Dark |
| 30 | Icons | Hybrid-Architektur | System-Icons in Library, Feature-Icons in App |
| 51 | Icon-Architektur | Hybrid | ~10 System-Icons in Library, Rest in App (siehe §4) |
| 59 | Size Props | Tailwind-Skala (xs, sm, md, lg, xl, 2xl) | Konsistenz mit Tailwind, intuitive Benennung |
| 59 | Size Props | Tailwind-Skala (sm, md, lg, xl) | Komponentenspezifisch, OsButton nutzt sm-xl |
| 60 | Rounded Props | Tailwind-Skala (none, sm, md, lg, xl, 2xl, 3xl, full) | Konsistenz mit Tailwind border-radius |
| 61 | Shadow Props | Tailwind-Skala (none, sm, md, lg, xl, 2xl) | Konsistenz mit Tailwind box-shadow |
| 62 | Variant Props | Semantisch (primary, secondary, danger, warning, success, info) | Übliche UI-Farbvarianten |
@ -1482,8 +1430,8 @@ Bei der Migration werden:
| 66 | Branding-Hierarchie | Webapp → Spezialisiertes Branding | Default-Branding in Webapp, Overrides pro Instanz |
| 67 | Variable-Validierung | Runtime-Check in Development | `validateCssVariables()` warnt bei fehlenden Variablen |
| 68 | Branding-Test (Webapp) | CI-Test in Webapp | Webapp testet, dass Default-Branding alle Library-Variablen definiert |
| 69 | Webapp ↔ Maintenance Sharing | Webapp als Source of Truth | Kein separates "shared" Package, maintenance importiert aus webapp/ (siehe §16a) |
| 70 | Daten-Entkopplung | ViewModel/Mapper Pattern | Komponenten kennen nur ViewModels, Mapper transformieren API-Daten (siehe §16b) |
| 72 | Webapp ↔ Maintenance Sharing | Webapp als Source of Truth | Kein separates "shared" Package, maintenance importiert aus webapp/ (siehe §16a) |
| 73 | Daten-Entkopplung | ViewModel/Mapper Pattern | Komponenten kennen nur ViewModels, Mapper transformieren API-Daten (siehe §16b) |
### Komponenten-API & Konventionen
@ -1748,6 +1696,15 @@ Bei der Migration werden:
| 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) |
| 2026-02-14 | **`as` Prop** | Polymorphe OsButton-Komponente: `as` Prop für dynamischen Tag/Komponente (`button`, `a`, `nuxt-link`, `router-link`); moderner Standard (Headless UI, Radix Vue) |
| 2026-02-14 | **Naming: tag → as** | `tag``as` umbenannt nach Recherche moderner UI-Libraries (Headless UI, Radix Vue, Chakra UI, PrimeVue nutzen `as`) |
| 2026-02-14 | **Disabled nur für button** | `disabled`/`type`/`loading` nur bei `as="button"` (Links haben kein natives `disabled`); `aria-disabled`/`tabindex` Logik entfernt |
| 2026-02-14 | **Polymorphic Story** | Neue Story `Polymorphic` mit Varianten, Icons, disabled-Vergleich; Playground mit `as`-Selektor (button/a) |
| 2026-02-14 | **nuxt-link Migration** | 15 `<nuxt-link>`/`<a>`-Wrapper → `as="nuxt-link"`/`as="a"` in 15 Webapp-Dateien; invalides HTML (`<button>` in `<a>`) eliminiert |
| 2026-02-14 | **CustomButton konsolidiert** | v-if/v-else für `<a>`/`<nuxt-link>` Wrapper → einzelner `<os-button :as="linkTag" v-bind="linkProps">` mit Computed Properties |
| 2026-02-14 | **NotificationMenu konsolidiert** | 2 separate Buttons (kein Badge / mit Badge) zu einem zusammengeführt — counter-icon zeigt bei `count=0` kein Badge |
| 2026-02-14 | **CSS-Selektor Fix** | pages/index.vue: `button.post-add-button-top/bottom``.post-add-button-top/bottom` (nuxt-link rendert `<a>`, nicht `<button>`) |
| 2026-02-14 | **Profil-Spacing** | profile/_id/_slug.vue: `v-if` auf ds-grid-item (kein leerer Abstand), symmetrisches Padding `$space-x-small` |
---
@ -2268,7 +2225,7 @@ Phase 3 (später): @ocelot-social/auth, @ocelot-social/posts, etc.
| # | Datum | Entscheidung |
|---|-------|--------------|
| 68 | 2026-02-09 | Webapp als Source of Truth für geteilte Business-Komponenten |
| 72 | 2026-02-09 | Webapp als Source of Truth für geteilte Business-Komponenten |
---
@ -2462,7 +2419,7 @@ webapp/
| # | Datum | Entscheidung |
|---|-------|--------------|
| 70 | 2026-02-09 | ViewModel/Mapper Pattern für Daten-Entkopplung |
| 73 | 2026-02-09 | ViewModel/Mapper Pattern für Daten-Entkopplung |
---

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { defineComponent, h } from 'vue-demi'
import OsButton from './OsButton.vue'
@ -548,6 +549,64 @@ describe('osButton', () => {
})
})
describe('as prop', () => {
it('renders as <button> by default', () => {
const wrapper = mount(OsButton)
expect((wrapper.element as HTMLElement).tagName).toBe('BUTTON')
expect(wrapper.attributes('type')).toBe('button')
})
it('renders as <a> when as="a"', () => {
const wrapper = mount(OsButton, {
props: { as: 'a' },
attrs: { href: '/test' },
slots: { default: 'Link' },
})
expect((wrapper.element as HTMLElement).tagName).toBe('A')
expect(wrapper.attributes('href')).toBe('/test')
expect(wrapper.attributes('type')).toBeUndefined()
})
it('renders a component passed as as', () => {
const FakeLink = defineComponent({
props: { to: { type: String, default: undefined } },
setup(props, { slots }) {
return () => h('a', { href: props.to }, slots.default?.())
},
})
const wrapper = mount(OsButton, {
props: { as: FakeLink },
attrs: { to: '/groups' },
slots: { default: 'Groups' },
})
expect((wrapper.element as HTMLElement).tagName).toBe('A')
expect(wrapper.text()).toBe('Groups')
})
it('ignores disabled prop on non-button tags', () => {
const wrapper = mount(OsButton, {
props: { as: 'a', disabled: true },
attrs: { href: '/test' },
})
expect(wrapper.attributes('disabled')).toBeUndefined()
expect(wrapper.attributes('aria-disabled')).toBeUndefined()
expect(wrapper.attributes('tabindex')).toBeUndefined()
})
it('applies variant classes regardless of as', () => {
const wrapper = mount(OsButton, {
props: { as: 'a', variant: 'primary', appearance: 'filled' },
})
expect(wrapper.classes()).toContain('os-button')
expect(wrapper.classes()).toContain('bg-[var(--color-primary)]')
})
})
describe('keyboard accessibility', () => {
it('renders as native button element for keyboard support', () => {
const wrapper = mount(OsButton)

View File

@ -42,6 +42,7 @@ type Story = StoryObj<typeof OsButton>
/** Custom args for Playground (icon selector + label are not real component props) */
interface PlaygroundArgs {
as: string
variant: string
appearance: string
size: string
@ -62,6 +63,10 @@ const iconMap: Record<string, (() => ReturnType<typeof h>) | null> = {
export const Playground: StoryObj<PlaygroundArgs> = {
argTypes: {
as: {
control: 'select',
options: ['button', 'a'],
},
variant: {
control: 'select',
options: ['default', 'primary', 'secondary', 'danger', 'warning', 'success', 'info'],
@ -95,6 +100,7 @@ export const Playground: StoryObj<PlaygroundArgs> = {
},
},
args: {
as: 'button',
variant: 'primary',
appearance: 'filled',
size: 'md',
@ -117,7 +123,7 @@ export const Playground: StoryObj<PlaygroundArgs> = {
return { buttonProps, IconComponent, label }
},
template: `
<OsButton v-bind="buttonProps">
<OsButton v-bind="buttonProps" :href="buttonProps.as === 'a' ? '#' : undefined">
<template v-if="IconComponent" #icon><component :is="IconComponent" /></template>
{{ label }}
</OsButton>
@ -663,6 +669,44 @@ export const CircleAppearances: Story = {
}),
}
export const Polymorphic: Story = {
render: () => ({
components: { OsButton, CheckIcon, PlusIcon },
template: `
<div class="flex flex-col gap-4">
<div>
<h3 class="text-sm font-bold mb-2">as="a" (anchor element)</h3>
<div class="flex flex-wrap gap-2">
<OsButton as="a" href="#" variant="primary">Primary Link</OsButton>
<OsButton as="a" href="#" variant="primary" appearance="outline">Outline Link</OsButton>
<OsButton as="a" href="#" variant="primary" appearance="ghost">Ghost Link</OsButton>
<OsButton as="a" href="#" variant="danger">Danger Link</OsButton>
</div>
</div>
<div>
<h3 class="text-sm font-bold mb-2">as="a" with icon</h3>
<div class="flex flex-wrap gap-2">
<OsButton as="a" href="#" variant="primary">
<template #icon><CheckIcon /></template>
Save
</OsButton>
<OsButton as="a" href="#" variant="success" circle aria-label="Add">
<template #icon><PlusIcon /></template>
</OsButton>
</div>
</div>
<div>
<h3 class="text-sm font-bold mb-2">as="button" (default)</h3>
<div class="flex flex-wrap gap-2">
<OsButton variant="primary">Button (default)</OsButton>
<OsButton as="button" variant="primary">Button (explicit)</OsButton>
</div>
</div>
</div>
`,
}),
}
export const Loading: Story = {
render: () => ({
components: { OsButton, CheckIcon },

View File

@ -218,6 +218,15 @@ test.describe('OsButton visual regression', () => {
await checkA11y(page)
})
test('polymorphic', async ({ page }) => {
await page.goto(`${STORY_URL}--polymorphic&viewMode=story`)
const root = page.locator(STORY_ROOT)
await root.waitFor()
await waitForFonts(page)
await expect(root.locator('.flex-col').first()).toHaveScreenshot('polymorphic.png')
await checkA11y(page)
})
test('loading', async ({ page }) => {
await page.goto(`${STORY_URL}--loading&viewMode=story`)
const root = page.locator(STORY_ROOT)

View File

@ -6,10 +6,19 @@
import { buttonVariants } from './button.variants'
import type { ButtonVariants } from './button.variants'
import type { PropType } from 'vue-demi'
import type { Component, PropType } from 'vue-demi'
/**
* Flexible button component with optional icon slot.
*
* Use the `as` prop to render as a different element or component (e.g. nuxt-link)
* without creating a dependency on any router library:
*
* ```vue
* <os-button :as="NuxtLink" to="/groups">Groups</os-button>
* <os-button as="a" href="/external">External</os-button>
* ```
*
* @slot default - Button content (text or HTML)
* @slot icon - Optional icon (rendered left of text). Use aria-label for icon-only buttons.
*/
@ -68,6 +77,15 @@
name: 'OsButton',
inheritAttrs: false,
props: {
/**
* Element or component to render as (e.g. 'a', NuxtLink, RouterLink).
* Note: `disabled` and `type` apply only when as="button" (default).
* `loading` shows the spinner for any tag, but only disables native buttons.
*/
as: {
type: [String, Object, Function] as PropType<string | Component>,
default: 'button',
},
variant: {
type: String as PropType<ButtonVariants['variant']>,
default: 'default',
@ -184,14 +202,20 @@
props.circle && CIRCLE_WIDTHS[size], // eslint-disable-line security/detect-object-injection
)
const buttonData = {
type: props.type,
disabled: isDisabled || undefined,
const tag = props.as
const isNativeButton = tag === 'button'
const buttonData: Record<string, unknown> = {
'data-variant': props.variant,
'data-appearance': props.appearance,
'aria-busy': isLoading || undefined,
}
if (isNativeButton) {
buttonData.type = props.type
buttonData.disabled = isDisabled || undefined
}
/* v8 ignore start -- Vue 2 branch tested in webapp Jest tests */
if (isVue2) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -201,7 +225,7 @@
const parentDynClass = proxy?.$vnode?.data?.class
return h(
'button',
tag,
{
class: [buttonClass, parentClass, parentDynClass].filter(Boolean),
attrs: { ...buttonData, ...attrs },
@ -214,7 +238,7 @@
const { class: attrClass, ...restAttrs } = attrs as Record<string, unknown>
return h(
'button',
tag,
{
...buttonData,
class: cn(buttonClass, attrClass || ''),

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`JoinLeaveButton.vue shallowMount renders 1`] = `
<os-button-stub variant="primary" appearance="outline" size="md" fullwidth="true" type="button" data-original-title="null" class=" has-tooltip">
<os-button-stub as="button" variant="primary" appearance="outline" size="md" fullwidth="true" type="button" data-original-title="null" class=" has-tooltip">
group.joinLeaveButton.join
</os-button-stub>
`;

View File

@ -34,19 +34,19 @@
<div v-if="selectedRoom && selectedRoom.roomId" slot="room-options" class="chat-room-options">
<ds-flex v-if="singleRoom">
<ds-flex-item centered class="single-chat-bubble">
<nuxt-link :to="{ name: 'chat' }">
<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>
<os-button
as="nuxt-link"
:to="{ name: 'chat' }"
variant="primary"
appearance="ghost"
circle
size="sm"
:aria-label="$t('chat.expandChat')"
>
<template #icon>
<base-icon name="expand" />
</template>
</os-button>
</ds-flex-item>
<ds-flex-item centered>
<div class="vac-svg-button vac-room-options">

View File

@ -1,20 +1,21 @@
<template>
<nuxt-link class="chat-notification-menu" :to="{ name: 'chat' }">
<os-button
variant="primary"
appearance="ghost"
circle
:aria-label="$t('header.chats.tooltip')"
v-tooltip="{
content: $t('header.chats.tooltip'),
placement: 'bottom-start',
}"
>
<template #icon>
<counter-icon icon="chat-bubble" :count="unreadRoomCount" danger />
</template>
</os-button>
</nuxt-link>
<os-button
as="nuxt-link"
:to="{ name: 'chat' }"
class="chat-notification-menu"
variant="primary"
appearance="ghost"
circle
:aria-label="$t('header.chats.tooltip')"
v-tooltip="{
content: $t('header.chats.tooltip'),
placement: 'bottom-start',
}"
>
<template #icon>
<counter-icon icon="chat-bubble" :count="unreadRoomCount" danger />
</template>
</os-button>
</template>
<script>

View File

@ -1,45 +1,25 @@
<template>
<div>
<a v-if="settings.url" :href="settings.url" :target="settings.target">
<os-button
class="custom-button"
variant="primary"
appearance="ghost"
circle
:aria-label="$t(settings.toolTipIdent)"
v-tooltip="{
content: $t(settings.toolTipIdent),
placement: 'bottom-start',
}"
>
<img
class="logo-svg"
:src="settings.iconPath"
:alt="settings.iconAltText"
:style="logoWidthStyle"
/>
</os-button>
</a>
<nuxt-link v-else :to="settings.path">
<os-button
class="custom-button"
variant="primary"
appearance="ghost"
circle
:aria-label="$t(settings.toolTipIdent)"
v-tooltip="{
content: $t(settings.toolTipIdent),
placement: 'bottom-start',
}"
>
<img
class="logo-svg"
:src="settings.iconPath"
:alt="settings.iconAltText"
:style="logoWidthStyle"
/>
</os-button>
</nuxt-link>
<os-button
:as="linkTag"
v-bind="linkProps"
class="custom-button"
variant="primary"
appearance="ghost"
circle
:aria-label="$t(settings.toolTipIdent)"
v-tooltip="{
content: $t(settings.toolTipIdent),
placement: 'bottom-start',
}"
>
<img
class="logo-svg"
:src="settings.iconPath"
:alt="settings.iconAltText"
:style="logoWidthStyle"
/>
</os-button>
</div>
</template>
@ -54,6 +34,14 @@ export default {
settings: { type: Object, required: true },
},
computed: {
linkTag() {
return this.settings.url ? 'a' : 'nuxt-link'
},
linkProps() {
return this.settings.url
? { href: this.settings.url, target: this.settings.target }
: { to: this.settings.path }
},
logoWidthStyle() {
const width = isEmpty(this.settings.iconWidth) ? '26px' : this.settings.iconWidth
return `width: ${width};`

View File

@ -11,12 +11,17 @@
:query="query"
:select-item="selectItem"
/>
<link-input
v-show="isLinkInputActive"
ref="linkInput"
:toggle-link-input="toggleLinkInput"
:set-link-url="setLinkUrl"
/>
<div v-show="isLinkInputActive" ref="linkInput">
<ds-input
id="linkInputId"
v-model="linkUrl"
class="editor-menu-link-input"
placeholder="https://"
@blur.native.capture="toggleLinkInput()"
@keydown.native.esc.prevent="toggleLinkInput()"
@keydown.native.enter.prevent="enterLink()"
/>
</div>
</div>
</template>
@ -35,7 +40,6 @@ import Mention from './nodes/Mention'
import MenuBar from './MenuBar'
import ContextMenu from './ContextMenu'
import SuggestionList from './SuggestionList'
import LinkInput from './LinkInput'
let throttleInputEvent
@ -43,7 +47,6 @@ export default {
components: {
ContextMenu,
EditorContent,
LinkInput,
MenuBar,
SuggestionList,
},
@ -58,6 +61,7 @@ export default {
lastValueHash: null,
editor: null,
isLinkInputActive: false,
linkUrl: null,
suggestionType: '',
query: null,
suggestionRange: null,
@ -247,16 +251,20 @@ export default {
insertReply(message) {
this.editor.commands.mention({ id: message.id, label: message.slug })
},
enterLink() {
this.setLinkUrl(this.linkUrl)
this.linkUrl = null
},
toggleLinkInput(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.linkUrl = attrs.href
this.isLinkInputActive = true
this.$nextTick(() => {
this.$refs.contextMenu.displayContextMenu(element, this.$refs.linkInput.$el, 'link')
this.$refs.contextMenu.displayContextMenu(element, this.$refs.linkInput, 'link')
})
} else {
this.isLinkInputActive = false

View File

@ -1,33 +0,0 @@
<template>
<div>
<ds-input
id="linkInputId"
v-model="linkUrl"
class="editor-menu-link-input"
placeholder="https://"
@blur.native.capture="toggleLinkInput()"
@keydown.native.esc.prevent="toggleLinkInput()"
@keydown.native.enter.prevent="enterLink()"
/>
</div>
</template>
<script>
export default {
props: {
toggleLinkInput: Function,
setLinkUrl: Function,
},
data() {
return {
linkUrl: null,
}
},
methods: {
enterLink() {
this.setLinkUrl(this.linkUrl)
this.linkUrl = null
},
},
}
</script>

View File

@ -1,124 +0,0 @@
import { mount } from '@vue/test-utils'
import Emotions from './Emotions.vue'
import Vuex from 'vuex'
import PostMutations from '~/graphql/PostMutations.js'
const localVue = global.localVue
describe('Emotions.vue', () => {
let wrapper
let mocks
let propsData
let getters
let funnyButton
let funnyImage
const funnyImageSrc = '/img/svg/emoji/funny_color.svg'
beforeEach(() => {
mocks = {
$apollo: {
mutate: jest
.fn()
.mockResolvedValueOnce({
data: {
AddPostEmotions: {
to: { id: 'p143' },
data: { emotion: 'happy' },
},
},
})
.mockResolvedValueOnce({
data: {
RemovePostEmotions: {
from: { id: 'u176' },
to: { id: 'p143' },
data: { emotion: 'happy' },
},
},
}),
query: jest.fn().mockResolvedValue({
data: {
PostsEmotionsCountByEmotion: 1,
},
}),
},
$t: jest.fn(),
}
propsData = {
post: { id: 'p143' },
}
getters = {
'auth/user': () => {
return { id: 'u176' }
},
}
})
describe('mount', () => {
const Wrapper = () => {
const store = new Vuex.Store({
getters,
})
return mount(Emotions, { mocks, propsData, store, localVue })
}
beforeEach(() => {
wrapper = Wrapper()
})
it("queries the post's emotions count for each of the 5 emotions", () => {
expect(mocks.$apollo.query).toHaveBeenCalledTimes(5)
})
describe('adding emotions', () => {
let expectedParams
beforeEach(() => {
wrapper.vm.PostsEmotionsCountByEmotion.funny = 0
funnyButton = wrapper.findAll('button').at(0)
funnyButton.trigger('click')
})
it('shows the colored image when the button is active', () => {
funnyImage = wrapper.findAll('img').at(0)
expect(funnyImage.attributes().src).toEqual(funnyImageSrc)
})
it('sends the AddPostEmotionsMutation for an emotion when clicked', () => {
expectedParams = {
mutation: PostMutations().AddPostEmotionsMutation,
variables: { to: { id: 'p143' }, data: { emotion: 'funny' } },
}
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
it('increases the PostsEmotionsCountByEmotion for the emotion clicked', () => {
expect(wrapper.vm.PostsEmotionsCountByEmotion.funny).toEqual(1)
})
it('adds an emotion to selectedEmotions to show the colored image when the button is active', () => {
expect(wrapper.vm.selectedEmotions).toEqual(['funny'])
})
describe('removing emotions', () => {
beforeEach(() => {
funnyButton.trigger('click')
})
it('sends the RemovePostEmotionsMutation when a user clicks on an active emotion', () => {
expectedParams = {
mutation: PostMutations().RemovePostEmotionsMutation,
variables: { to: { id: 'p143' }, data: { emotion: 'funny' } },
}
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
it('decreases the PostsEmotionsCountByEmotion for the emotion clicked', async () => {
expect(wrapper.vm.PostsEmotionsCountByEmotion.funny).toEqual(0)
})
it('removes an emotion from selectedEmotions to show the default image', async () => {
expect(wrapper.vm.selectedEmotions).toEqual([])
})
})
})
})
})

View File

@ -1,120 +0,0 @@
<template>
<div class="emotions-button-group">
<emotion-button
v-for="emotion in Object.keys(PostsEmotionsCountByEmotion)"
:key="emotion"
:emojiPath="iconPath(emotion)"
:emotion="emotion"
:emotionCount="PostsEmotionsCountByEmotion[emotion]"
@toggleEmotion="toggleEmotion"
/>
</div>
</template>
<script>
import gql from 'graphql-tag'
import { mapGetters } from 'vuex'
import EmotionButton from '~/components/EmotionButton/EmotionButton'
import { PostsEmotionsByCurrentUser } from '~/graphql/PostQuery.js'
import PostMutations from '~/graphql/PostMutations.js'
export default {
components: {
EmotionButton,
},
props: {
post: { type: Object, default: () => {} },
},
data() {
return {
selectedEmotions: [],
PostsEmotionsCountByEmotion: { funny: 0, happy: 0, surprised: 0, cry: 0, angry: 0 },
}
},
computed: {
...mapGetters({
currentUser: 'auth/user',
}),
},
created() {
Object.keys(this.PostsEmotionsCountByEmotion).map((emotion) => {
this.emotionsCount(emotion)
})
},
methods: {
iconPath(emotion) {
if (this.isActive(emotion)) {
return `/img/svg/emoji/${emotion}_color.svg`
}
return `/img/svg/emoji/${emotion}.svg`
},
toggleEmotion(emotion) {
this.$apollo
.mutate({
mutation: this.isActive(emotion)
? PostMutations().RemovePostEmotionsMutation
: PostMutations().AddPostEmotionsMutation,
variables: {
to: { id: this.post.id },
data: { emotion },
},
})
.then(() => {
this.isActive(emotion)
? this.PostsEmotionsCountByEmotion[emotion]--
: this.PostsEmotionsCountByEmotion[emotion]++
const index = this.selectedEmotions.indexOf(emotion)
if (index > -1) {
this.selectedEmotions.splice(index, 1)
} else {
this.selectedEmotions.push(emotion)
}
})
},
isActive(emotion) {
const index = this.selectedEmotions.indexOf(emotion)
if (index > -1) {
return true
}
return false
},
emotionsCount(emotion) {
this.$apollo
.query({
query: gql`
query ($postId: ID!, $data: _EMOTEDInput!) {
PostsEmotionsCountByEmotion(postId: $postId, data: $data)
}
`,
variables: { postId: this.post.id, data: { emotion } },
fetchPolicy: 'no-cache',
})
.then(({ data: { PostsEmotionsCountByEmotion } }) => {
this.PostsEmotionsCountByEmotion[emotion] = PostsEmotionsCountByEmotion
})
},
},
apollo: {
PostsEmotionsByCurrentUser: {
query() {
return PostsEmotionsByCurrentUser()
},
variables() {
return {
postId: this.post.id,
}
},
update({ PostsEmotionsByCurrentUser }) {
this.selectedEmotions = PostsEmotionsByCurrentUser
},
},
},
}
</script>
<style lang="scss">
.emotions-button-group {
display: flex;
}
</style>

View File

@ -7,16 +7,14 @@
<ds-text>
{{ $t('contribution.comment.commenting-disabled.blocked-author.call-to-action') }}
</ds-text>
<nuxt-link :to="authorLink">
<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,
})
}}
</os-button>
</nuxt-link>
<os-button as="nuxt-link" :to="authorLink" variant="primary" appearance="filled">
<template #icon><base-icon name="arrow-right" /></template>
{{
$t('contribution.comment.commenting-disabled.blocked-author.button-label', {
name: author.name,
})
}}
</os-button>
</ds-space>
</template>

View File

@ -11,7 +11,7 @@ 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" data-variant="primary" data-appearance="filled" class="os-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)] 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] [&amp;>svg]:h-full [&amp;>svg]:w-auto [&amp;>svg]:fill-current -ml-1"><span class="base-icon"><!----></span></span>
</p> <button data-variant="primary" data-appearance="filled" type="button" class="os-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)] 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] [&amp;>svg]:h-full [&amp;>svg]:w-auto [&amp;>svg]:fill-current -ml-1"><span class="base-icon"><!----></span></span>
group.joinLeaveButton.join
</span></button>
</div>

View File

@ -9,10 +9,8 @@ exports[`CtaUnblockAuthor.vue shallowMount renders 1`] = `
<ds-text-stub tag="p">
contribution.comment.commenting-disabled.blocked-author.call-to-action
</ds-text-stub>
<nuxt-link-stub to="[object Object]">
<os-button-stub variant="primary" appearance="filled" size="md" type="button">
contribution.comment.commenting-disabled.blocked-author.button-label
</os-button-stub>
</nuxt-link-stub>
<os-button-stub as="nuxt-link" variant="primary" appearance="filled" size="md" type="button" to="[object Object]">
contribution.comment.commenting-disabled.blocked-author.button-label
</os-button-stub>
</ds-space-stub>
`;

View File

@ -1,50 +0,0 @@
<template>
<dropdown ref="category-menu" placement="top-start" :offset="8" class="category-menu">
<a href="#" slot="default" slot-scope="{ toggleMenu }" @click.prevent="toggleMenu()">
<ds-text bold size="large">{{ $t('admin.categories.name') }}</ds-text>
</a>
<template #popover>
<div class="category-menu-options">
<h2 class="title">{{ $t('filter-menu.filter-by') }}</h2>
<categories-filter v-if="categoriesActive" />
</div>
</template>
</dropdown>
</template>
<script>
import Dropdown from '~/components/Dropdown'
import { mapGetters } from 'vuex'
import CategoriesFilter from './CategoriesFilter'
import GetCategories from '~/mixins/getCategoriesMixin.js'
export default {
name: 'CategoriesMenu',
mixins: [GetCategories],
components: {
Dropdown,
CategoriesFilter,
},
props: {
placement: { type: String },
offset: { type: [String, Number] },
},
computed: {
...mapGetters({
// TODO: implement visibility of active filter later on
filterActive: 'posts/isActive',
}),
},
}
</script>
<style lang="scss">
.category-menu-options {
max-width: $size-max-width-filter-menu;
padding: $space-small $space-x-small;
> .title {
font-size: $font-size-large;
}
}
</style>

View File

@ -1,28 +0,0 @@
<template>
<div>
<nuxt-link to="/groups">
<os-button
variant="primary"
appearance="ghost"
circle
: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>
import { OsButton } from '@ocelot-social/ui'
export default {
components: { OsButton },
}
</script>

View File

@ -132,9 +132,9 @@
</div>
<!-- submit -->
<ds-space margin-top="large">
<nuxt-link to="/groups">
<os-button variant="default" appearance="filled">{{ $t('actions.cancel') }}</os-button>
</nuxt-link>
<os-button as="nuxt-link" to="/groups" variant="default" appearance="filled">
{{ $t('actions.cancel') }}
</os-button>
<os-button
variant="primary"
appearance="filled"

View File

@ -1,15 +0,0 @@
<template>
<div>
<div>
<ds-space><h3>Link zur Gruppe</h3></ds-space>
<ds-space>
<ds-copy-field>Copy Link for Invite Member please!</ds-copy-field>
</ds-space>
</div>
</div>
</template>
<script>
export default {
name: 'GroupLink',
}
</script>

View File

@ -84,11 +84,42 @@
</div>
<!-- group button -->
<client-only v-if="SHOW_GROUP_BUTTON_IN_HEADER">
<group-button />
<os-button
as="nuxt-link"
to="/groups"
variant="primary"
appearance="ghost"
circle
: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>
</client-only>
<!-- map button -->
<client-only v-if="!isEmpty(this.$env.MAPBOX_TOKEN)">
<map-button />
<os-button
as="nuxt-link"
to="/map"
class="map-button"
variant="primary"
appearance="ghost"
circle
:aria-label="$t('header.map.tooltip')"
v-tooltip="{
content: $t('header.map.tooltip'),
placement: 'bottom-start',
}"
>
<template #icon>
<base-icon name="globe-detailed" size="large" />
</template>
</os-button>
</client-only>
<!-- custom button -->
<client-only v-if="!isEmpty(customButton)">
@ -202,7 +233,22 @@
>
<client-only>
<div @click="toggleMobileMenuView">
<group-button />
<os-button
as="nuxt-link"
to="/groups"
variant="primary"
appearance="ghost"
circle
: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>
</div>
</client-only>
</ds-flex-item>
@ -214,7 +260,23 @@
>
<client-only>
<div @click="toggleMobileMenuView">
<map-button />
<os-button
as="nuxt-link"
to="/map"
class="map-button"
variant="primary"
appearance="ghost"
circle
:aria-label="$t('header.map.tooltip')"
v-tooltip="{
content: $t('header.map.tooltip'),
placement: 'bottom-start',
}"
>
<template #icon>
<base-icon name="globe-detailed" size="large" />
</template>
</os-button>
</div>
</client-only>
</ds-flex-item>
@ -290,12 +352,10 @@ import AvatarMenu from '~/components/AvatarMenu/AvatarMenu'
import ChatNotificationMenu from '~/components/ChatNotificationMenu/ChatNotificationMenu'
import CustomButton from '~/components/CustomButton/CustomButton'
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
import GroupButton from '~/components/Group/GroupButton'
import headerMenuBranded from '~/constants/headerMenuBranded.js'
import InviteButton from '~/components/InviteButton/InviteButton'
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
import Logo from '~/components/Logo/Logo'
import MapButton from '~/components/Map/MapButton'
import SearchField from '~/components/features/SearchField/SearchField.vue'
import NotificationMenu from '~/components/NotificationMenu/NotificationMenu'
import links from '~/constants/links.js'
@ -310,11 +370,9 @@ export default {
ChatNotificationMenu,
CustomButton,
FilterMenu,
GroupButton,
InviteButton,
LocaleSwitch,
Logo,
MapButton,
NotificationMenu,
PageParamsLink,
SearchField,
@ -473,4 +531,12 @@ export default {
.hide-mobile-menu {
display: none;
}
.map-button {
margin-left: 3px;
margin-right: 3px;
.base-icon > .svg.--large {
margin-left: 0;
}
}
</style>

View File

@ -1,41 +0,0 @@
<template>
<div>
<nuxt-link to="/map">
<os-button
class="map-button"
variant="primary"
appearance="ghost"
circle
:aria-label="$t('header.map.tooltip')"
v-tooltip="{
content: $t('header.map.tooltip'),
placement: 'bottom-start',
}"
>
<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: 3px;
margin-right: 3px;
.base-icon > .svg.--large {
margin-left: 0;
}
}
</style>

View File

@ -1,40 +1,22 @@
<template>
<nuxt-link
v-if="!unreadNotificationsCount"
class="notifications-menu"
<os-button
v-if="!unreadNotificationsCount || noMenu"
as="nuxt-link"
:to="{ name: 'notifications' }"
class="notifications-menu"
variant="primary"
appearance="ghost"
circle
:aria-label="$t('header.notifications.tooltip')"
v-tooltip="{
content: $t('header.notifications.tooltip'),
placement: 'bottom-start',
}"
>
<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>
<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>
<template #icon>
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
</template>
</os-button>
<dropdown
v-else
class="notifications-menu"
@ -63,11 +45,14 @@
<template #popover="{ closeMenu }">
<ds-flex class="notifications-link-container">
<ds-flex-item>
<nuxt-link :to="{ name: 'notifications' }">
<os-button appearance="ghost" variant="primary">
{{ $t('notifications.pageLink') }}
</os-button>
</nuxt-link>
<os-button
as="nuxt-link"
:to="{ name: 'notifications' }"
appearance="ghost"
variant="primary"
>
{{ $t('notifications.pageLink') }}
</os-button>
<os-button
appearance="ghost"
variant="primary"

View File

@ -27,9 +27,15 @@
/>
</li>
</ul>
<nuxt-link v-if="isTouchDevice && userLink" :to="userLink" class="link">
<os-button variant="primary">{{ $t('user-teaser.popover.open-profile') }}</os-button>
</nuxt-link>
<os-button
v-if="isTouchDevice && userLink"
as="nuxt-link"
:to="userLink"
class="link"
variant="primary"
>
{{ $t('user-teaser.popover.open-profile') }}
</os-button>
</div>
</template>

View File

@ -739,29 +739,25 @@ exports[`Users given badges are enabled renders 1`] = `
class="ds-table-col ds-table-col-right"
>
<nuxt-link-stub
aria-label="actions.edit"
class="os-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)] 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"
to="[object Object]"
>
<button
aria-label="actions.edit"
class="os-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)] 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="inline-flex items-center"
>
<span
class="inline-flex items-center"
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
>
<span
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
class="base-icon"
>
<span
class="base-icon"
>
<!---->
</span>
<!---->
</span>
</span>
</button>
</span>
</nuxt-link-stub>
</td>
</tr>
@ -841,29 +837,25 @@ exports[`Users given badges are enabled renders 1`] = `
class="ds-table-col ds-table-col-right"
>
<nuxt-link-stub
aria-label="actions.edit"
class="os-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)] 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"
to="[object Object]"
>
<button
aria-label="actions.edit"
class="os-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)] 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="inline-flex items-center"
>
<span
class="inline-flex items-center"
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
>
<span
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
class="base-icon"
>
<span
class="base-icon"
>
<!---->
</span>
<!---->
</span>
</span>
</button>
</span>
</nuxt-link-stub>
</td>
</tr>

View File

@ -73,21 +73,19 @@
</template>
</template>
<template #badges="scope">
<nuxt-link
<os-button
as="nuxt-link"
:to="{
name: 'admin-users-id',
params: { id: scope.row.id },
}"
variant="primary"
appearance="filled"
circle
:aria-label="$t('actions.edit')"
>
<os-button
variant="primary"
appearance="filled"
circle
:aria-label="$t('actions.edit')"
>
<template #icon><base-icon name="pencil" /></template>
</os-button>
</nuxt-link>
<template #icon><base-icon name="pencil" /></template>
</os-button>
</template>
</ds-table>
<pagination-buttons :hasNext="hasNext" :hasPrevious="hasPrevious" @next="next" @back="back" />

View File

@ -934,30 +934,26 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
style="margin-bottom: 32px;"
>
<nuxt-link-stub
aria-label="contribution.newPost"
class="profile-post-add-button os-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)] 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] profile-post-add-button has-tooltip"
data-appearance="filled"
data-original-title="null"
data-variant="primary"
to="[object Object]"
>
<button
aria-label="contribution.newPost"
class="profile-post-add-button os-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)] 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] profile-post-add-button has-tooltip"
data-appearance="filled"
data-original-title="null"
data-variant="primary"
type="button"
<span
class="inline-flex items-center"
>
<span
class="inline-flex items-center"
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
>
<span
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
class="base-icon"
>
<span
class="base-icon"
>
<!---->
</span>
<!---->
</span>
</span>
</button>
</span>
</nuxt-link-stub>
</div>
@ -2930,30 +2926,26 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
style="margin-bottom: 32px;"
>
<nuxt-link-stub
aria-label="contribution.newPost"
class="profile-post-add-button os-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)] 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] profile-post-add-button has-tooltip"
data-appearance="filled"
data-original-title="null"
data-variant="primary"
to="[object Object]"
>
<button
aria-label="contribution.newPost"
class="profile-post-add-button os-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)] 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] profile-post-add-button has-tooltip"
data-appearance="filled"
data-original-title="null"
data-variant="primary"
type="button"
<span
class="inline-flex items-center"
>
<span
class="inline-flex items-center"
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
>
<span
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
class="base-icon"
>
<span
class="base-icon"
>
<!---->
</span>
<!---->
</span>
</span>
</button>
</span>
</nuxt-link-stub>
</div>
@ -3945,30 +3937,26 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
style="margin-bottom: 32px;"
>
<nuxt-link-stub
aria-label="contribution.newPost"
class="profile-post-add-button os-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)] 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] profile-post-add-button has-tooltip"
data-appearance="filled"
data-original-title="null"
data-variant="primary"
to="[object Object]"
>
<button
aria-label="contribution.newPost"
class="profile-post-add-button os-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)] 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] profile-post-add-button has-tooltip"
data-appearance="filled"
data-original-title="null"
data-variant="primary"
type="button"
<span
class="inline-flex items-center"
>
<span
class="inline-flex items-center"
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
>
<span
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
class="base-icon"
>
<span
class="base-icon"
>
<!---->
</span>
<!---->
</span>
</span>
</button>
</span>
</nuxt-link-stub>
</div>
@ -6543,30 +6531,26 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
style="margin-bottom: 32px;"
>
<nuxt-link-stub
aria-label="contribution.newPost"
class="profile-post-add-button os-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)] 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] profile-post-add-button has-tooltip"
data-appearance="filled"
data-original-title="null"
data-variant="primary"
to="[object Object]"
>
<button
aria-label="contribution.newPost"
class="profile-post-add-button os-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)] 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] profile-post-add-button has-tooltip"
data-appearance="filled"
data-original-title="null"
data-variant="primary"
type="button"
<span
class="inline-flex items-center"
>
<span
class="inline-flex items-center"
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
>
<span
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
class="base-icon"
>
<span
class="base-icon"
>
<!---->
</span>
<!---->
</span>
</span>
</button>
</span>
</nuxt-link-stub>
</div>
@ -7623,30 +7607,26 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
style="margin-bottom: 32px;"
>
<nuxt-link-stub
aria-label="contribution.newPost"
class="profile-post-add-button os-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)] 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] profile-post-add-button has-tooltip"
data-appearance="filled"
data-original-title="null"
data-variant="primary"
to="[object Object]"
>
<button
aria-label="contribution.newPost"
class="profile-post-add-button os-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)] 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] profile-post-add-button has-tooltip"
data-appearance="filled"
data-original-title="null"
data-variant="primary"
type="button"
<span
class="inline-flex items-center"
>
<span
class="inline-flex items-center"
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
>
<span
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
class="base-icon"
>
<span
class="base-icon"
>
<!---->
</span>
<!---->
</span>
</span>
</button>
</span>
</nuxt-link-stub>
</div>
@ -8644,30 +8624,26 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
style="margin-bottom: 32px;"
>
<nuxt-link-stub
aria-label="contribution.newPost"
class="profile-post-add-button os-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)] 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] profile-post-add-button has-tooltip"
data-appearance="filled"
data-original-title="null"
data-variant="primary"
to="[object Object]"
>
<button
aria-label="contribution.newPost"
class="profile-post-add-button os-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)] 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] profile-post-add-button has-tooltip"
data-appearance="filled"
data-original-title="null"
data-variant="primary"
type="button"
<span
class="inline-flex items-center"
>
<span
class="inline-flex items-center"
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
>
<span
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
class="base-icon"
>
<span
class="base-icon"
>
<!---->
</span>
<!---->
</span>
</span>
</button>
</span>
</nuxt-link-stub>
</div>

View File

@ -218,28 +218,26 @@
</base-card>
</ds-space>
<ds-space v-if="isGroupMemberNonePending" centered>
<nuxt-link
<os-button
as="nuxt-link"
:to="{
name: 'post-create-type',
query: { groupId: group.id },
}"
class="profile-post-add-button"
variant="primary"
appearance="filled"
circle
:aria-label="$t('contribution.newPost')"
v-tooltip="{
content: $t('contribution.newPost'),
placement: 'left',
}"
>
<os-button
class="profile-post-add-button"
variant="primary"
appearance="filled"
circle
:aria-label="$t('contribution.newPost')"
v-tooltip="{
content: $t('contribution.newPost'),
placement: 'left',
}"
>
<template #icon>
<base-icon name="plus" />
</template>
</os-button>
</nuxt-link>
<template #icon>
<base-icon name="plus" />
</template>
</os-button>
</ds-space>
<masonry-grid>
<!-- TapNavigation -->

View File

@ -7,24 +7,24 @@
<ds-space>
<!-- create group -->
<ds-space centered>
<nuxt-link :to="{ name: 'groups-create' }">
<os-button
class="group-add-button"
variant="primary"
appearance="filled"
circle
size="xl"
:aria-label="$t('group.createNewGroup.tooltip')"
v-tooltip="{
content: $t('group.createNewGroup.tooltip'),
placement: 'left',
}"
>
<template #icon>
<base-icon name="plus" />
</template>
</os-button>
</nuxt-link>
<os-button
as="nuxt-link"
:to="{ name: 'groups-create' }"
class="group-add-button"
variant="primary"
appearance="filled"
circle
size="xl"
:aria-label="$t('group.createNewGroup.tooltip')"
v-tooltip="{
content: $t('group.createNewGroup.tooltip'),
placement: 'left',
}"
>
<template #icon>
<base-icon name="plus" />
</template>
</os-button>
</ds-space>
<!-- group list -->
<ds-space centered v-if="showPagination">

View File

@ -3,24 +3,27 @@
<!-- create post -->
<div :class="POST_ADD_BUTTON_POSITION_TOP ? 'box-add-button-top' : ''">
<client-only>
<nuxt-link :to="{ name: 'post-create-type' }" :class="{ 'hide-filter': hideByScroll }">
<os-button
v-tooltip="{
content: $t('contribution.newPost'),
placement: 'left',
}"
class="post-add-button"
:class="POST_ADD_BUTTON_POSITION_TOP ? 'post-add-button-top' : 'post-add-button-bottom'"
variant="primary"
appearance="filled"
circle
size="xl"
>
<template #icon>
<base-icon name="plus" />
</template>
</os-button>
</nuxt-link>
<os-button
as="nuxt-link"
:to="{ name: 'post-create-type' }"
v-tooltip="{
content: $t('contribution.newPost'),
placement: 'left',
}"
class="post-add-button"
:class="[
POST_ADD_BUTTON_POSITION_TOP ? 'post-add-button-top' : 'post-add-button-bottom',
{ 'hide-filter': hideByScroll },
]"
variant="primary"
appearance="filled"
circle
size="xl"
>
<template #icon>
<base-icon name="plus" />
</template>
</os-button>
</client-only>
</div>
<div>
@ -356,7 +359,7 @@ export default {
float: right;
}
button.post-add-button-bottom {
.post-add-button-bottom {
height: 54px !important;
width: 54px !important;
min-height: 54px !important;
@ -370,7 +373,7 @@ button.post-add-button-bottom {
box-shadow: $box-shadow-x-large !important;
}
button.post-add-button-top {
.post-add-button-top {
height: 54px !important;
width: 54px !important;
min-height: 54px !important;
@ -467,7 +470,7 @@ button.post-add-button-top {
.box-add-button-top {
padding-right: 40px;
}
button.post-add-button-top {
.post-add-button-top {
height: 44px !important;
width: 44px !important;
min-height: 44px !important;

View File

@ -558,20 +558,7 @@ exports[`ProfileSlug given an authenticated user given another profile user and
</article>
</div>
<div
style="grid-row-end: span 2; grid-column-start: 1; grid-column-end: -1;"
>
<div
class="ds-space ds-space-centered"
style="margin-bottom: 32px;"
>
<nuxt-link-stub
to="[object Object]"
>
<!---->
</nuxt-link-stub>
</div>
</div>
<!---->
<div
style="grid-row-end: span 4; grid-column-start: 1; grid-column-end: -1;"
@ -1264,20 +1251,7 @@ exports[`ProfileSlug given an authenticated user given another profile user and
</article>
</div>
<div
style="grid-row-end: span 2; grid-column-start: 1; grid-column-end: -1;"
>
<div
class="ds-space ds-space-centered"
style="margin-bottom: 32px;"
>
<nuxt-link-stub
to="[object Object]"
>
<!---->
</nuxt-link-stub>
</div>
</div>
<!---->
<div
style="grid-row-end: span 4; grid-column-start: 1; grid-column-end: -1;"
@ -1845,34 +1819,29 @@ exports[`ProfileSlug given an authenticated user given the logged in user as pro
style="grid-row-end: span 2; grid-column-start: 1; grid-column-end: -1;"
>
<div
class="ds-space ds-space-centered"
style="margin-bottom: 32px;"
class="profile-post-add-button-container"
>
<nuxt-link-stub
aria-label="contribution.newPost"
class="profile-post-add-button os-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)] 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] profile-post-add-button has-tooltip"
data-appearance="filled"
data-original-title="null"
data-variant="primary"
to="[object Object]"
>
<button
aria-label="contribution.newPost"
class="profile-post-add-button os-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)] 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] profile-post-add-button has-tooltip"
data-appearance="filled"
data-original-title="null"
data-variant="primary"
type="button"
<span
class="inline-flex items-center"
>
<span
class="inline-flex items-center"
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
>
<span
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
class="base-icon"
>
<span
class="base-icon"
>
<!---->
</span>
<!---->
</span>
</span>
</button>
</span>
</nuxt-link-stub>
</div>
</div>
@ -2485,34 +2454,29 @@ exports[`ProfileSlug given an authenticated user given the logged in user as pro
style="grid-row-end: span 2; grid-column-start: 1; grid-column-end: -1;"
>
<div
class="ds-space ds-space-centered"
style="margin-bottom: 32px;"
class="profile-post-add-button-container"
>
<nuxt-link-stub
aria-label="contribution.newPost"
class="profile-post-add-button os-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)] 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] profile-post-add-button has-tooltip"
data-appearance="filled"
data-original-title="null"
data-variant="primary"
to="[object Object]"
>
<button
aria-label="contribution.newPost"
class="profile-post-add-button os-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)] 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] profile-post-add-button has-tooltip"
data-appearance="filled"
data-original-title="null"
data-variant="primary"
type="button"
<span
class="inline-flex items-center"
>
<span
class="inline-flex items-center"
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
>
<span
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
class="base-icon"
>
<span
class="base-icon"
>
<!---->
</span>
<!---->
</span>
</span>
</button>
</span>
</nuxt-link-stub>
</div>
</div>

View File

@ -145,27 +145,26 @@
<tab-navigation :tabs="tabOptions" :activeTab="tabActive" @switch-tab="handleTab" />
<!-- feed -->
<ds-grid-item :row-span="2" column-span="fullWidth">
<ds-space centered>
<nuxt-link :to="{ name: 'post-create-type' }">
<os-button
v-if="myProfile"
v-tooltip="{
content: $t('contribution.newPost'),
placement: 'left',
}"
class="profile-post-add-button"
variant="primary"
appearance="filled"
circle
:aria-label="$t('contribution.newPost')"
>
<template #icon>
<base-icon name="plus" />
</template>
</os-button>
</nuxt-link>
</ds-space>
<ds-grid-item v-if="myProfile" :row-span="2" column-span="fullWidth">
<div class="profile-post-add-button-container">
<os-button
as="nuxt-link"
:to="{ name: 'post-create-type' }"
v-tooltip="{
content: $t('contribution.newPost'),
placement: 'left',
}"
class="profile-post-add-button"
variant="primary"
appearance="filled"
circle
:aria-label="$t('contribution.newPost')"
>
<template #icon>
<base-icon name="plus" />
</template>
</os-button>
</div>
</ds-grid-item>
<template v-if="posts.length">
@ -505,6 +504,11 @@ export default {
right: $space-x-small;
}
}
.profile-post-add-button-container {
display: flex;
justify-content: center;
padding: $space-x-small 0;
}
.profile-post-add-button {
box-shadow: $box-shadow-x-large !important;
}

View File

@ -3,11 +3,15 @@
<base-card>
<base-icon name="balance-scale" />
<h2 class="title">{{ $t(`termsAndConditions.newTermsAndConditions`) }}</h2>
<nuxt-link :to="{ name: 'terms-and-conditions' }" target="_blank">
<os-button appearance="outline" variant="primary">
{{ $t(`termsAndConditions.termsAndConditionsNewConfirmText`) }}
</os-button>
</nuxt-link>
<os-button
as="nuxt-link"
:to="{ name: 'terms-and-conditions' }"
target="_blank"
appearance="outline"
variant="primary"
>
{{ $t(`termsAndConditions.termsAndConditionsNewConfirmText`) }}
</os-button>
<label for="checkbox">
<input id="checkbox" type="checkbox" v-model="checked" :checked="checked" />
{{ $t('termsAndConditions.termsAndConditionsNewConfirm') }}