refactor(webapp): vue3 migration - phase 3 - integration (#9180)
5
.github/file-filters.yml
vendored
@ -1,5 +1,9 @@
|
||||
# These file filter patterns are used by the action https://github.com/dorny/paths-filter
|
||||
|
||||
ui: &ui
|
||||
- '.github/workflows/ui-*.yml'
|
||||
- 'packages/ui/**/*'
|
||||
|
||||
backend: &backend
|
||||
- '.github/workflows/test-backend.yml'
|
||||
- 'backend/**/*'
|
||||
@ -14,6 +18,7 @@ webapp: &webapp
|
||||
- 'webapp/**/*'
|
||||
- 'styleguide/**/*'
|
||||
- 'package.json'
|
||||
- *ui
|
||||
|
||||
docs-check: &docs-check
|
||||
- '.github/workflows/check-documentation.yml'
|
||||
|
||||
20
.github/workflows/ui-build.yml
vendored
@ -5,18 +5,38 @@ on:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-build.yml'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-build.yml'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
build:
|
||||
name: Build
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
23
.github/workflows/ui-compatibility.yml
vendored
@ -5,14 +5,34 @@ on:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-compatibility.yml'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-compatibility.yml'
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
build-library:
|
||||
name: Build Library
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
@ -44,7 +64,8 @@ jobs:
|
||||
|
||||
test-compatibility:
|
||||
name: Test ${{ matrix.example }}
|
||||
needs: build-library
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: [files-changed, build-library]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
20
.github/workflows/ui-docker.yml
vendored
@ -5,14 +5,34 @@ on:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-docker.yml'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-docker.yml'
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
build:
|
||||
name: Build Docker Image
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
20
.github/workflows/ui-lint.yml
vendored
@ -5,18 +5,38 @@ on:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-lint.yml'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-lint.yml'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
lint:
|
||||
name: ESLint
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
20
.github/workflows/ui-size.yml
vendored
@ -5,18 +5,38 @@ on:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-size.yml'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-size.yml'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
size:
|
||||
name: Bundle Size Check
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
20
.github/workflows/ui-storybook.yml
vendored
@ -5,18 +5,38 @@ on:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-storybook.yml'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-storybook.yml'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
build:
|
||||
name: Build Storybook
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
20
.github/workflows/ui-test.yml
vendored
@ -5,18 +5,38 @@ on:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-test.yml'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-test.yml'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
test:
|
||||
name: Unit Tests
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
20
.github/workflows/ui-verify.yml
vendored
@ -5,18 +5,38 @@ on:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-verify.yml'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-verify.yml'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
verify:
|
||||
name: Completeness Check
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
20
.github/workflows/ui-visual.yml
vendored
@ -5,18 +5,38 @@ on:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-visual.yml'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/ui/**'
|
||||
- '.github/workflows/ui-visual.yml'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/ui
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ui: ${{ steps.changes.outputs.ui }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
|
||||
visual:
|
||||
name: Visual Regression
|
||||
if: needs.files-changed.outputs.ui == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
// eslint-disable-next-line import-x/no-unassigned-import
|
||||
import '@fontsource-variable/inter'
|
||||
// eslint-disable-next-line import-x/no-unassigned-import
|
||||
import './storybook.css'
|
||||
|
||||
export const parameters = {
|
||||
|
||||
@ -1,34 +1,54 @@
|
||||
/* Inter font for consistent rendering across platforms */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Greyscale theme for Storybook - demonstrates that colors come from the app */
|
||||
/* Watercolor theme for Storybook - vibrant colors inspired by aquarelle paints */
|
||||
/* All colors meet WCAG AA contrast requirements (4.5:1 for normal text) */
|
||||
:root {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
font-family: 'Inter Variable', system-ui, sans-serif;
|
||||
|
||||
--color-primary: #404040;
|
||||
--color-primary-hover: #262626;
|
||||
/* Default (Buff / Raw Umber) - neutral button */
|
||||
--color-default: #d8c9b8;
|
||||
--color-default-hover: #c4b19c;
|
||||
--color-default-active: #a89478;
|
||||
--color-default-contrast: #3d2e1e;
|
||||
--color-default-contrast-inverse: #ffffff;
|
||||
|
||||
/* Primary (Ultramarine Blue) */
|
||||
--color-primary: #4a5a9e;
|
||||
--color-primary-hover: #5e6db2;
|
||||
--color-primary-active: #3b4882;
|
||||
--color-primary-contrast: #ffffff;
|
||||
|
||||
--color-secondary: #595959;
|
||||
--color-secondary-hover: #404040;
|
||||
/* Secondary (Dioxazine Violet) */
|
||||
--color-secondary: #7b519a;
|
||||
--color-secondary-hover: #9068ad;
|
||||
--color-secondary-active: #654280;
|
||||
--color-secondary-contrast: #ffffff;
|
||||
|
||||
--color-danger: #525252;
|
||||
--color-danger-hover: #404040;
|
||||
/* Danger (Alizarin Crimson) */
|
||||
--color-danger: #b83a4e;
|
||||
--color-danger-hover: #cc5567;
|
||||
--color-danger-active: #9a2e3f;
|
||||
--color-danger-contrast: #ffffff;
|
||||
|
||||
--color-warning: #d4d4d4;
|
||||
--color-warning-hover: #a3a3a3;
|
||||
--color-warning-contrast: #000000;
|
||||
/* Warning (Yellow Ochre / Raw Sienna) - dark amber, works as text on white */
|
||||
--color-warning: #946b1d;
|
||||
--color-warning-hover: #a87c28;
|
||||
--color-warning-active: #7d5a15;
|
||||
--color-warning-contrast: #ffffff;
|
||||
|
||||
--color-success: #525252;
|
||||
--color-success-hover: #404040;
|
||||
/* Success (Viridian Green) */
|
||||
--color-success: #3d7a5e;
|
||||
--color-success-hover: #509470;
|
||||
--color-success-active: #30644c;
|
||||
--color-success-contrast: #ffffff;
|
||||
|
||||
--color-info: #595959;
|
||||
--color-info-hover: #404040;
|
||||
/* Info (Cerulean Blue) */
|
||||
--color-info: #387598;
|
||||
--color-info-hover: #4e8bae;
|
||||
--color-info-active: #2d6080;
|
||||
--color-info-contrast: #ffffff;
|
||||
|
||||
/* Disabled (Payne's Grey - muted watercolor grey) */
|
||||
--color-disabled: #c4bdb5;
|
||||
--color-disabled-contrast: #5a4f45;
|
||||
}
|
||||
|
||||
@ -10,8 +10,9 @@
|
||||
### Übersicht
|
||||
```
|
||||
Phase 0: Analyse ██████████ 100% (8/8 Schritte)
|
||||
Phase 3: Migration ████░░░░░░ 36% (32/90 Buttons)
|
||||
───────────────────────────────────────────
|
||||
Nächste Phase: Phase 2 (Projekt-Setup)
|
||||
Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
||||
```
|
||||
|
||||
### Statistiken
|
||||
@ -24,6 +25,109 @@ Nächste Phase: Phase 2 (Projekt-Setup)
|
||||
| Duplikate gefunden | 5 direkte + 3 Familien |
|
||||
| Zur Migration priorisiert | 15 Kern-Komponenten |
|
||||
|
||||
### 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 |
|
||||
|
||||
**Migrierte Komponenten (32):**
|
||||
|
||||
*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)
|
||||
|
||||
*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)
|
||||
|
||||
*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)
|
||||
|
||||
---
|
||||
|
||||
## Styleguide Komponenten (38)
|
||||
@ -50,7 +154,7 @@ Nächste Phase: Phase 2 (Projekt-Setup)
|
||||
### Data Input
|
||||
| # | Komponente | Status | Webapp-Duplikat | Varianten | Priorität | Notizen |
|
||||
|---|------------|--------|-----------------|-----------|-----------|---------|
|
||||
| 13 | Button | ⬜ Ausstehend | BaseButton, CustomButton, ActionButton, ... | | | VIELE Varianten! |
|
||||
| 13 | Button | ⏳ Migration | BaseButton, CustomButton, ActionButton, ... | | | → OsButton (16/90 migriert) |
|
||||
| 14 | CopyField | ⬜ Ausstehend | | | | |
|
||||
| 15 | Form | ⬜ Ausstehend | | | | |
|
||||
| 16 | FormItem | ⬜ Ausstehend | | | | |
|
||||
@ -115,7 +219,7 @@ Nächste Phase: Phase 2 (Projekt-Setup)
|
||||
| 7 | BadgeSelection | ⬜ Ausstehend | Input | | |
|
||||
| 8 | Badges | ⬜ Ausstehend | Display | | |
|
||||
| 9 | BadgesSection | ⬜ Ausstehend | Display | | |
|
||||
| 10 | BaseButton | ⬜ Ausstehend | Button | Button | 🔄 Button-Familie |
|
||||
| 10 | BaseButton | ⏳ Migration | Button | Button | 🔄 → OsButton (16/90 migriert) |
|
||||
| 11 | BaseCard | ⬜ Ausstehend | Layout | Card | 🔗 DUPLIKAT |
|
||||
| 12 | BaseIcon | ⬜ Ausstehend | Display | Icon | 🔗 DUPLIKAT |
|
||||
|
||||
@ -129,7 +233,7 @@ Nächste Phase: Phase 2 (Projekt-Setup)
|
||||
| 17 | Change | ⬜ Ausstehend | Feature | | |
|
||||
| 18 | Chat | ⬜ Ausstehend | Feature | | Chat-spezifisch |
|
||||
| 19 | ChatNotificationMenu | ⬜ Ausstehend | Feature | | Chat-spezifisch |
|
||||
| 20 | CommentCard | ⬜ Ausstehend | Display | Card | |
|
||||
| 20 | CommentCard | ⏳ Teilweise | Display | Card | 1/2 Buttons → OsButton |
|
||||
| 21 | CommentForm | ⬜ Ausstehend | Input | Form | |
|
||||
| 22 | CommentList | ⬜ Ausstehend | Display | List | |
|
||||
| 23 | ComponentSlider | ⬜ Ausstehend | Layout | | |
|
||||
@ -152,12 +256,12 @@ Nächste Phase: Phase 2 (Projekt-Setup)
|
||||
| 36 | DeleteData | ⬜ Ausstehend | Feature | | |
|
||||
| 37 | DeleteUserModal | ⬜ Ausstehend | Feedback | Modal | 🔄 Modal-Familie |
|
||||
| 38 | DisableModal | ⬜ Ausstehend | Feedback | Modal | 🔄 Modal-Familie |
|
||||
| 39 | DonationInfo | ⬜ Ausstehend | Display | | |
|
||||
| 39 | DonationInfo | ✅ Migriert | Display | | Button → OsButton |
|
||||
| 40 | Dropdown | ⬜ Ausstehend | Input | Select | |
|
||||
| 41 | DropdownFilter | ⬜ Ausstehend | Filter | Select | |
|
||||
| 42 | Editor | ⬜ Ausstehend | Input | | Rich-Text |
|
||||
| 43 | EmailDisplayAndVerify | ⬜ Ausstehend | Feature | | |
|
||||
| 44 | EmbedComponent | ⬜ Ausstehend | Display | | |
|
||||
| 44 | EmbedComponent | ⏳ Teilweise | Display | | 2/3 Buttons → OsButton |
|
||||
| 45 | EmotionButton | ⬜ Ausstehend | Button | Button | |
|
||||
| 46 | Emotions | ⬜ Ausstehend | Feature | | |
|
||||
| 47 | Empty | ⬜ Ausstehend | Feedback | Placeholder | |
|
||||
@ -176,10 +280,10 @@ Nächste Phase: Phase 2 (Projekt-Setup)
|
||||
| 56 | FollowList | ⬜ Ausstehend | Display | List | |
|
||||
| 57 | GroupButton | ⬜ Ausstehend | Button | Button | |
|
||||
| 58 | GroupContentMenu | ⬜ Ausstehend | Navigation | Menu | |
|
||||
| 59 | GroupForm | ⬜ Ausstehend | Input | Form | |
|
||||
| 59 | GroupForm | ⏳ Teilweise | Input | Form | 1/2 Buttons → OsButton |
|
||||
| 60 | GroupLink | ⬜ Ausstehend | Navigation | | |
|
||||
| 61 | GroupList | ⬜ Ausstehend | Display | List | |
|
||||
| 62 | GroupMember | ⬜ Ausstehend | Display | | |
|
||||
| 62 | GroupMember | ✅ Migriert | Display | | Button → OsButton |
|
||||
| 63 | GroupTeaser | ⬜ Ausstehend | Display | Card | |
|
||||
|
||||
### H-L
|
||||
@ -211,7 +315,7 @@ Nächste Phase: Phase 2 (Projekt-Setup)
|
||||
| # | Komponente | Status | Kategorie | Styleguide-Pendant | Notizen |
|
||||
|---|------------|--------|-----------|-------------------|---------|
|
||||
| 85 | MapButton | ⬜ Ausstehend | Button | Button | |
|
||||
| 86 | MapStylesButtons | ⬜ Ausstehend | Button | Button | |
|
||||
| 86 | MapStylesButtons | ✅ Migriert | Button | Button | Button → OsButton |
|
||||
| 87 | MasonryGrid | ⬜ Ausstehend | Layout | Grid | |
|
||||
| 88 | MasonryGridItem | ⬜ Ausstehend | Layout | GridItem | |
|
||||
| 89 | MenuBar | ⬜ Ausstehend | Navigation | Menu | |
|
||||
@ -274,7 +378,7 @@ Nächste Phase: Phase 2 (Projekt-Setup)
|
||||
| 134 | UserTeaser | ⬜ Ausstehend | Display | Card | |
|
||||
| 135 | UserTeaserHelper | ⬜ Ausstehend | Display | | |
|
||||
| 136 | UserTeaserNonAnonymous | ⬜ Ausstehend | Display | | |
|
||||
| 137 | UserTeaserPopover | ⬜ Ausstehend | Display | | |
|
||||
| 137 | UserTeaserPopover | ✅ Migriert | Display | | Button → OsButton |
|
||||
|
||||
---
|
||||
|
||||
@ -308,7 +412,7 @@ Nächste Phase: Phase 2 (Projekt-Setup)
|
||||
| EmotionButton | Emotion | Feature-spezifisch |
|
||||
| JoinLeaveButton | Beitreten/Verlassen | Feature-spezifisch |
|
||||
| MapButton | Karten-Button | Feature-spezifisch |
|
||||
| MapStylesButtons | Kartenstile | Feature-spezifisch |
|
||||
| MapStylesButtons | Kartenstile | ✅ → OsButton |
|
||||
| CtaJoinLeaveGroup | CTA | Feature-spezifisch |
|
||||
| CtaUnblockAuthor | CTA | Feature-spezifisch |
|
||||
|
||||
@ -374,11 +478,18 @@ Diese sollten zuerst migriert werden:
|
||||
| 2026-02-04 | Claude | Priorisierung | 15 Komponenten in 4 Tiers priorisiert |
|
||||
| 2026-02-04 | Claude | Konsolidierungsplan | 3 Phasen definiert, Token-Liste erstellt |
|
||||
| 2026-02-04 | Claude | **Phase 0 abgeschlossen** | Bereit für Phase 2 (Projekt-Setup) |
|
||||
| 2026-02-08 | Claude | OsButton entwickelt | CVA-Varianten, Vue 2/3 kompatibel via vue-demi |
|
||||
| 2026-02-08 | Claude | Webapp-Integration | Jest Mock, Docker Build, CI-Kompatibilität |
|
||||
| 2026-02-08 | Claude | **16 Buttons migriert** | Alle ohne icon/circle/loading Props, validiert |
|
||||
| 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) |
|
||||
|
||||
---
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
### Phase 0: Analyse ✅
|
||||
1. [x] Webapp-Komponenten auflisten
|
||||
2. [x] Styleguide-Komponenten auflisten
|
||||
3. [x] Offensichtliche Duplikate identifizieren
|
||||
@ -388,9 +499,33 @@ Diese sollten zuerst migriert werden:
|
||||
7. [x] Priorisierung festlegen
|
||||
8. [x] Konsolidierungsplan finalisieren
|
||||
|
||||
### Phase 3: OsButton Migration (in Arbeit)
|
||||
9. [x] OsButton entwickeln (CVA, vue-demi)
|
||||
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 4b: Props für ~60 Buttons hinzufügen**
|
||||
16. [ ] icon-Prop zu OsButton hinzufügen
|
||||
17. [ ] circle-Variant zu OsButton hinzufügen
|
||||
18. [ ] loading-Prop zu OsButton hinzufügen
|
||||
|
||||
**Milestone 4c: ~60 Buttons mit neuen Props migrieren**
|
||||
19. [ ] Button-Komponenten (~15)
|
||||
20. [ ] Navigation (~8)
|
||||
21. [ ] Editor (~15)
|
||||
22. [ ] Filter/Chat (~10)
|
||||
23. [ ] Forms/Modals (~5)
|
||||
24. [ ] Features/Pages (~12)
|
||||
|
||||
---
|
||||
|
||||
**✅ Phase 0 abgeschlossen!** Weiter mit Phase 2 (Projekt-Setup).
|
||||
**✅ Phase 0 abgeschlossen!** Phase 3 zu 27% erledigt (24/90 Buttons migriert). Milestone 4a: 8/14 Buttons.
|
||||
|
||||
---
|
||||
|
||||
@ -1073,3 +1208,99 @@ $z-index-dropdown: 8888
|
||||
$box-shadow-x-large: 0 15px 30px 0 rgba(0,0,0,.11), ...
|
||||
$box-shadow-small-inset: inset 0 0 0 1px rgba(0,0,0,.05)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Webapp-Integration (Tracking)
|
||||
|
||||
### OsButton Migration - Abgeschlossen (16/90)
|
||||
|
||||
| # | Datei | Button | Status |
|
||||
|---|-------|--------|--------|
|
||||
| 1 | UserTeaserPopover.vue | Open Profile | ✅ Migriert |
|
||||
| 2 | GroupForm.vue | Cancel | ✅ Migriert |
|
||||
| 3 | EmbedComponent.vue | Cancel | ✅ Migriert |
|
||||
| 4 | EmbedComponent.vue | Play Now | ✅ Migriert |
|
||||
| 5 | DonationInfo.vue | Donate | ✅ Migriert |
|
||||
| 6 | CommentCard.vue | Show More | ✅ Migriert |
|
||||
| 7 | MapStylesButtons.vue | Style Toggle | ✅ Migriert |
|
||||
| 8 | GroupMember.vue | Remove | ✅ Migriert |
|
||||
| 9 | embeds.vue | Allow All | ✅ Migriert |
|
||||
| 10 | embeds.vue | Deny All | ✅ Migriert |
|
||||
| 11 | notifications.vue | Check All | ✅ Migriert |
|
||||
| 12 | notifications.vue | Uncheck All | ✅ Migriert |
|
||||
| 13 | notifications.vue | Save | ✅ Migriert |
|
||||
| 14 | privacy.vue | Save | ✅ Migriert |
|
||||
| 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)
|
||||
|
||||
| # | 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 |
|
||||
|
||||
### OsButton Migration - Ausstehend mit neuen Props (Milestone 4c: ~60/90)
|
||||
|
||||
> 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 |
|
||||
|
||||
### 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)
|
||||
|
||||
### Integrations-Protokoll
|
||||
|
||||
| Datum | Aktion | Details |
|
||||
|-------|--------|---------|
|
||||
| 2026-02-08 | Analyse | 6 Einsatzstellen identifiziert, 2 minimal (nur variant) |
|
||||
|
||||
@ -51,6 +51,8 @@
|
||||
| # | Abschnitt |
|
||||
|---|-----------|
|
||||
| 16 | [Library vs. Webapp](#16-library-vs-webapp) |
|
||||
| 16a | [Webapp ↔ Maintenance Code-Sharing](#16a-webapp--maintenance-code-sharing) |
|
||||
| 16b | [Daten-Entkopplung (ViewModel/Mapper)](#16b-daten-entkopplung-viewmodelmapper-pattern) |
|
||||
| 17 | [Externe Abhängigkeiten](#17-externe-abhängigkeiten) |
|
||||
| 18 | [Kompatibilitätstests (Details)](#18-kompatibilitätstests-details) |
|
||||
| 19 | [Komplexitätsanalyse](#19-komplexitätsanalyse) |
|
||||
@ -78,12 +80,11 @@
|
||||
Phase 0: ██████████ 100% (6/6 Aufgaben) ✅
|
||||
Phase 1: ██████████ 100% (6/6 Aufgaben) ✅
|
||||
Phase 2: ██████████ 100% (26/26 Aufgaben) ✅
|
||||
Phase 3: ░░░░░░░░░░ 0% (0/7 Aufgaben)
|
||||
Phase 3: █████████░ 83% (20/24 Aufgaben) - Webapp-Integration (M4a ✅, M5 ✅)
|
||||
Phase 4: █░░░░░░░░░ 6% (1/17 Aufgaben) - OsButton ✅
|
||||
Phase 5: ░░░░░░░░░░ 0% (0/7 Aufgaben)
|
||||
Webapp: ░░░░░░░░░░ 0% (0/1 Aufgaben)
|
||||
───────────────────────────────────────
|
||||
Gesamt: █████░░░░░ 56% (39/70 Aufgaben)
|
||||
Gesamt: ███████░░░ 69% (59/86 Aufgaben)
|
||||
```
|
||||
|
||||
### Katalogisierung (Details in KATALOG.md)
|
||||
@ -93,22 +94,30 @@ Styleguide: ██████████ 100% (38 Komponenten erfasst)
|
||||
Analyse: ██████████ 100% (Button, Modal, Menu detailiert)
|
||||
```
|
||||
|
||||
### Komponenten-Migration (Priorisiert: 15)
|
||||
### OsButton Migration (Phase 3)
|
||||
```
|
||||
Analysiert: 3 Familien (Button, Modal, Menu)
|
||||
Spezifiziert: 1 (OsButton)
|
||||
Entwickelt: 1 (OsButton mit CVA)
|
||||
QA bestanden: 1 (OsButton: 100% Coverage, Visual, A11y, Keyboard)
|
||||
Integriert: 0
|
||||
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)
|
||||
|
||||
OsButton Features:
|
||||
├─ variant: ✅ primary, secondary, danger, warning, success, info, default
|
||||
├─ appearance: ✅ filled, outline, ghost
|
||||
├─ size: ✅ xs, sm, md, lg, xl
|
||||
├─ disabled: ✅ mit hover/active-Override
|
||||
├─ icon: ⬜ TODO (Milestone 4b)
|
||||
├─ circle: ⬜ TODO (Milestone 4b)
|
||||
└─ loading: ⬜ TODO (Milestone 4b)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Aktueller Stand
|
||||
|
||||
**Letzte Aktualisierung:** 2026-02-08
|
||||
**Letzte Aktualisierung:** 2026-02-10 (Session 12)
|
||||
|
||||
**Aktuelle Phase:** Phase 3 (Token-System & Basis) - Bereit zum Start
|
||||
**Aktuelle Phase:** Phase 3 (Webapp-Integration) - Milestone 4a abgeschlossen ✅ (32 Buttons migriert, nächster: Milestone 4b)
|
||||
|
||||
**Zuletzt abgeschlossen:**
|
||||
- [x] Projektordner erstellt
|
||||
@ -166,8 +175,8 @@ Integriert: 0
|
||||
- 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 (Greyscale-Theme)
|
||||
- OsButton.stories.ts mit allen Varianten
|
||||
- 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
|
||||
@ -175,21 +184,91 @@ Integriert: 0
|
||||
- ESLint Plugins: vuejs-accessibility, playwright, storybook, jsdoc
|
||||
|
||||
**Aktuell in Arbeit:**
|
||||
- Bereit für Phase 3: Token-System & Basis
|
||||
- Phase 3, Milestone 4b: icon/circle/loading Props in OsButton implementieren
|
||||
- Phase 3, Milestone 4c: ~60 Buttons mit icon/circle/loading migrieren
|
||||
|
||||
**Zuletzt 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`
|
||||
- [x] CI-Workflow-Trigger optimiert: 9 UI-Workflows von `on: push` auf Branch+Path-Filter (`master`, `packages/ui/**`)
|
||||
- [x] `custom-class` → `class` Migration: 4 Stellen in 3 Webapp-Dateien (notifications, MapStylesButtons, EmbedComponent)
|
||||
- [x] Vue 3 Template-Fix: `this.$t()` → `$t()` in CommentCard.vue (Zeile 5 + 58)
|
||||
- [x] Pre-existing Fix: `async` Arrow-Function in OsButton.visual.spec.ts
|
||||
|
||||
**Zuvor abgeschlossen (Session 11 - Storybook & Code-Review Fixes):**
|
||||
- [x] Wasserfarben-Farbschema für Storybook (Ultramarin, Dioxazin-Violett, Alizarin, Ocker, Viridian, Cöruleum)
|
||||
- [x] Stories erweitert: Playground (interaktive Controls), alle Varianten in allen Stories
|
||||
- [x] Einzelne Stories (Primary, Secondary, Danger, Default) durch AllVariants ersetzt
|
||||
- [x] AllAppearances zeigt alle 7 Varianten × 3 Appearances
|
||||
- [x] Einheitlicher Border (0.8px) über alle Appearances (kein Layout-Shift mehr)
|
||||
- [x] WCAG 2.4.7 Fix: Default-Variante hat jetzt `focus:outline-dashed focus:outline-current`
|
||||
- [x] Keyboard Accessibility Test: prüft Focus-Indikator auf allen Buttons im Browser
|
||||
- [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):**
|
||||
- [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):**
|
||||
- [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)
|
||||
- [x] OsButton mit isVue2 Render-Funktion (Vue 2: attrs-Objekt, Vue 3: flat props)
|
||||
- [x] CSS-Reihenfolge angepasst (UI-Library nach Styleguide für korrekte Spezifität)
|
||||
- [x] Manueller visueller Vergleich ✅
|
||||
- [x] **Jest-Integration für vue-demi** ✅
|
||||
- Custom Mock (`test/__mocks__/@ocelot-social/ui.js`) statt direktem Import
|
||||
- Problem: Jest's moduleNameMapper greift nicht für verschachtelte requires in CJS
|
||||
- Problem: Jest lädt `vue.runtime.common.js` mit exports unter `default`
|
||||
- Lösung: Module._load Patch für vue-demi + defineComponent von Vue.default
|
||||
- Setup-File (`test/vueDemiSetup.js`) für Module._resolveFilename Patch
|
||||
- **979 Tests bestehen ✅**
|
||||
- [x] Button-Variants an ds-button angepasst (font-semibold, rounded, box-shadow)
|
||||
- [x] UserTeaserPopover.vue migriert (verwendet `<os-button>`)
|
||||
- [x] **Docker Build für UI-Library** ✅
|
||||
- ui-library Stage in Dockerfile + Dockerfile.maintenance
|
||||
- COPY --from=ui-library ./app/ /packages/ui/
|
||||
- [x] **CI-Kompatibilität** ✅
|
||||
- Relativer Pfad `file:../packages/ui` statt absolut `/packages/ui`
|
||||
- Funktioniert lokal, in CI und in Docker
|
||||
- [x] **OsButton attrs/listeners Forwarding** ✅
|
||||
- getCurrentInstance() für $listeners Zugriff in Vue 2
|
||||
- inheritAttrs: false für manuelle Weiterleitung
|
||||
- Jest Mock um alle Composition API Funktionen erweitert
|
||||
- [x] **16 Buttons migriert** (ohne icon/circle/loading) ✅
|
||||
- GroupForm.vue, EmbedComponent.vue, DonationInfo.vue, CommentCard.vue
|
||||
- MapStylesButtons.vue, GroupMember.vue, embeds.vue
|
||||
- notifications.vue, privacy.vue, terms-and-conditions-confirm.vue, UserTeaserPopover.vue
|
||||
- [x] **Disabled-Styles korrigiert** ✅
|
||||
- CSS-Variablen `--color-disabled` und `--color-disabled-contrast` hinzugefügt
|
||||
- Filled-Buttons: Grauer Hintergrund statt opacity (wie buttonStates Mixin)
|
||||
- Outline/Ghost: Graue Border/Text
|
||||
- [x] terms-and-conditions-confirm.vue: Read T&C Button → `appearance="outline" variant="primary"`
|
||||
- [x] **Disabled:active/hover Spezifität** ✅
|
||||
- CSS-Regeln in index.css mit höherer Spezifität für disabled:hover und disabled:active
|
||||
- Button zeigt sofort disabled-Farben, auch wenn während :active disabled wird
|
||||
- [x] notifications.vue: Check All + Uncheck All → `appearance="outline" variant="primary"`
|
||||
- [x] embeds.vue: Allow All → `appearance="outline" variant="primary"`
|
||||
- [x] **Disabled Border-Fix** ✅
|
||||
- CSS-Regeln in index.css: `border-style: solid` und `border-width: 0.8px` bei disabled
|
||||
- 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
|
||||
- [x] CSS Custom Properties Token-System aufsetzen
|
||||
- [x] Storybook für Dokumentation einrichten
|
||||
- [x] Docker Setup (Dockerfile, docker-compose, ui-docker.yml)
|
||||
- [x] Visual Regression Tests (Playwright + @axe-core/playwright)
|
||||
- [x] Keyboard Accessibility Tests
|
||||
- [x] ESLint Plugins (storybook, playwright, vuejs-accessibility, jsdoc)
|
||||
- [x] Storybook Build Workflow (ui-storybook.yml)
|
||||
- [x] Completeness Check Script (verify: Story, Visual, checkA11y, Keyboard, Varianten)
|
||||
4. **Phase 3: Token-System & Basis** - Nächste Phase
|
||||
4. **Phase 3: Webapp-Integration** - 32/90 Buttons migriert (36%)
|
||||
- [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
|
||||
|
||||
**Manuelle Setup-Aufgaben (außerhalb Code):**
|
||||
- [ ] `NPM_TOKEN` als GitHub Secret einrichten (für npm publish in ui-release.yml)
|
||||
@ -239,7 +318,7 @@ Integriert: 0
|
||||
- [x] Visual Regression Tests einrichten (Playwright, colocated mit Komponenten)
|
||||
- [x] Accessibility Tests in Visual Tests integriert (@axe-core/playwright)
|
||||
- [x] Keyboard Accessibility Tests (describe('keyboard accessibility'))
|
||||
- [x] ESLint Plugins: vuejs-accessibility, playwright, storybook, jsdoc
|
||||
- [x] ESLint Plugins: vuejs-accessibility, playwright, storybook, jsdoc, @eslint/css
|
||||
- [x] Bundle Size Check einrichten (size-limit, ui-size.yml)
|
||||
- [x] Package-Validierung einrichten (publint, arethetypeswrong)
|
||||
- [x] Example Apps erstellen (vue3-tailwind, vue3-css, vue2-tailwind, vue2-css)
|
||||
@ -252,14 +331,175 @@ Integriert: 0
|
||||
- [x] CONTRIBUTING.md
|
||||
- [x] Completeness Check Script (Story, Visual+checkA11y, Keyboard, Varianten)
|
||||
|
||||
### Phase 3: Token-System & Basis
|
||||
- [ ] Base Tokens definieren (Farben, Spacing, Typography)
|
||||
- [ ] Semantic Tokens definieren
|
||||
- [ ] Component Tokens definieren
|
||||
- [ ] Branding-System implementieren (CSS Variables)
|
||||
- [ ] Beispiel-Branding erstellen (Standard + Yunite)
|
||||
- [ ] Storybook Theme-Farben anpassen (ocelot.social Branding)
|
||||
- [ ] Token-Dokumentation in Storybook
|
||||
### Phase 3: Webapp-Integration (Validierung)
|
||||
|
||||
**Ziel:** OsButton in der Webapp einbinden, ohne visuelle oder funktionale Änderungen.
|
||||
|
||||
**Ansatz:** Integration First - Library einbinden, dann schrittweise OsButton ersetzen, beginnend mit einfachsten Stellen.
|
||||
|
||||
**Milestone 1: Library-Einbindung** ✅
|
||||
- [x] @ocelot-social/ui in Webapp installieren (yarn link + Webpack-Alias)
|
||||
- [x] vue-demi zur Webapp hinzugefügt (für Vue 2.7 Kompatibilität)
|
||||
- [x] Webpack-Alias für vue-demi (nutzt Webapp's Vue 2.7)
|
||||
- [x] Webpack-Alias für @ocelot-social/ui$ und style.css$
|
||||
- [x] CSS Custom Properties in Webapp definieren (ocelot-ui-variables.scss)
|
||||
- [x] CSS-Reihenfolge angepasst (UI-Library nach Styleguide)
|
||||
- [x] Import-Pfade testen
|
||||
- [x] Docker Build Stage für UI-Library (Dockerfile + Dockerfile.maintenance)
|
||||
- [x] Relativer Pfad für CI-Kompatibilität (file:../packages/ui)
|
||||
- [x] Jest Mock für @ocelot-social/ui (test/__mocks__/@ocelot-social/ui.js)
|
||||
|
||||
**Milestone 2: Erste Integration (Minimaler Aufwand)** ✅
|
||||
- [x] OsButton mit isVue2 Render-Funktion (Vue 2/3 kompatibel)
|
||||
- [x] Button-Variants an ds-button angepasst (font-semibold, rounded, box-shadow)
|
||||
- [x] OsButton in UserTeaserPopover.vue eingesetzt (`variant="primary"`)
|
||||
- [x] Manueller visueller Vergleich ✅
|
||||
- [x] Webapp-Tests bestehen ✅ (979 Tests, jest moduleNameMapper für vue-demi)
|
||||
|
||||
**Milestone 3: Schrittweise Erweiterung** ✅
|
||||
- [x] GroupForm.vue Cancel-Button migriert
|
||||
- [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 4b: OsButton Props erweitern**
|
||||
- [ ] `icon` Prop implementieren (slot-basiert oder Icon-Komponente)
|
||||
- [ ] `circle` Variant zu CVA hinzufügen
|
||||
- [ ] `loading` Prop mit Spinner implementieren
|
||||
|
||||
**Milestone 4c: Buttons mit icon/circle/loading migrieren (~60 Buttons)**
|
||||
|
||||
*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)
|
||||
|
||||
*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)
|
||||
|
||||
*Editor:*
|
||||
- [ ] Editor/MenuBarButton.vue (icon, circle)
|
||||
- [ ] Editor/MenuLegend.vue (~10× icon, circle)
|
||||
|
||||
*Filter & Input:*
|
||||
- [ ] HashtagsFilter.vue (icon, circle)
|
||||
- [ ] CategoriesSelect.vue (icon)
|
||||
- [ ] SearchableInput.vue (icon, circle)
|
||||
- [ ] Select/LocationSelect.vue (icon)
|
||||
- [ ] PaginationButtons.vue (2× icon, circle)
|
||||
|
||||
*Chat:*
|
||||
- [ ] Chat/Chat.vue (2× icon, circle)
|
||||
- [ ] 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)
|
||||
|
||||
*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)
|
||||
|
||||
*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)
|
||||
|
||||
*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)
|
||||
|
||||
**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)
|
||||
- [ ] 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** |
|
||||
|
||||
**Details siehe KATALOG.md** (vollständige Tracking-Tabellen)
|
||||
|
||||
**Erfolgskriterien:**
|
||||
| Kriterium | Prüfung |
|
||||
|-----------|---------|
|
||||
| Visuell identisch | Manueller Screenshot-Vergleich |
|
||||
| Funktional identisch | Click, Disabled funktionieren |
|
||||
| Keine Regression | Webapp Unit-Tests bestehen |
|
||||
|
||||
**Visuelle Validierung (OsButton vs Original):**
|
||||
|
||||
Jeder migrierte Button muss manuell geprüft werden: Normal, Hover, Focus, Active, Disabled.
|
||||
|
||||
| Datei | Button | Props | Validiert |
|
||||
|-------|--------|-------|-----------|
|
||||
| `components/Group/GroupForm.vue` | Cancel | `default` | ✅ |
|
||||
| `components/Group/GroupMember.vue` | Remove Member | `appearance="outline" variant="primary" size="sm"` | ✅ |
|
||||
| `components/CommentCard/CommentCard.vue` | Show more/less | `appearance="ghost" variant="primary" size="sm"` | ✅ |
|
||||
| `components/UserTeaser/UserTeaserPopover.vue` | Open Profile | `variant="primary"` | ✅ |
|
||||
| `components/DonationInfo/DonationInfo.vue` | Donate Now | `size="sm" variant="primary"` | ✅ |
|
||||
| `components/Map/MapStylesButtons.vue` | Map Styles | `:appearance` dynamisch + custom CSS | ✅ |
|
||||
| `components/Embed/EmbedComponent.vue` | Cancel | `appearance="outline" variant="danger"` + custom CSS | ✅ |
|
||||
| `components/Embed/EmbedComponent.vue` | Play Now | `variant="primary"` + custom CSS | ✅ |
|
||||
| `pages/terms-and-conditions-confirm.vue` | Read T&C | `appearance="outline" variant="primary"` | ✅ |
|
||||
| `pages/terms-and-conditions-confirm.vue` | Save | `variant="primary"` + disabled | ✅ |
|
||||
| `pages/settings/privacy.vue` | Save | `variant="primary"` + disabled | ✅ |
|
||||
| `pages/settings/notifications.vue` | Check All | `appearance="outline" variant="primary"` + disabled | ✅ |
|
||||
| `pages/settings/notifications.vue` | Uncheck All | `appearance="outline" variant="primary"` + disabled | ✅ |
|
||||
| `pages/settings/notifications.vue` | Save | `variant="primary"` + disabled | ✅ |
|
||||
| `pages/settings/embeds.vue` | Allow All | `appearance="outline" variant="primary"` + disabled | ✅ |
|
||||
| `pages/settings/embeds.vue` | Deny All | `variant="primary"` + disabled | ✅ |
|
||||
|
||||
**Validierung abgeschlossen:** 16/16 (100%) ✅
|
||||
|
||||
**Nach Abschluss aller Validierungen:**
|
||||
- [ ] Gesamt-Regressionstest durchführen
|
||||
- [ ] Alle Unit-Tests bestehen
|
||||
- [ ] Dokumentation aktualisieren
|
||||
|
||||
### Phase 4: Komponenten-Migration (15 Komponenten + 2 Infrastruktur)
|
||||
|
||||
@ -1061,6 +1301,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) |
|
||||
|
||||
### Komponenten-API & Konventionen
|
||||
|
||||
@ -1185,6 +1427,65 @@ Bei der Migration werden:
|
||||
| 2026-02-08 | **Projekt-Optimierung** | src/test/setup.ts entfernt, @storybook/vue3 entfernt, README.md fix |
|
||||
| 2026-02-08 | **Package Updates** | size-limit 12.0.0, eslint-plugin-jsdoc 62.5.4, vite-tsconfig-paths 6.1.0 |
|
||||
| 2026-02-08 | **TODO: eslint-config-it4c** | Muss auf ESLint 10 aktualisiert werden (aktuell inkompatibel) |
|
||||
| 2026-02-08 | **Phase 3: vue-demi Integration** | vue-demi zur Webapp hinzugefügt, Webpack-Alias für Vue 2.7 Kompatibilität |
|
||||
| 2026-02-08 | **Phase 3: Webpack-Alias** | @ocelot-social/ui$ und style.css$ Aliase für yarn-linked Package |
|
||||
| 2026-02-08 | **Phase 3: isVue2 Render** | OsButton mit isVue2 Check: Vue 2 attrs-Objekt, Vue 3 flat props |
|
||||
| 2026-02-08 | **Phase 3: CSS-Spezifität** | UI-Library CSS nach Styleguide laden (styleguide.js Plugin) |
|
||||
| 2026-02-08 | **Phase 3: Jest vue-demi** | Custom Mock (`__mocks__/@ocelot-social/ui.js`) mit Module._load Patch, defineComponent von Vue.default, vueDemiSetup.js, 979 Tests ✅ |
|
||||
| 2026-02-08 | **Phase 3: Button-Styles** | Variants angepasst: font-semibold, rounded, box-shadow, h-[37.5px] |
|
||||
| 2026-02-08 | **Phase 3: Erste Integration** | UserTeaserPopover.vue verwendet `<os-button>` |
|
||||
| 2026-02-08 | **Phase 3: Visueller Test** | Manueller Vergleich OsButton vs ds-button erfolgreich ✅ |
|
||||
| 2026-02-08 | **Phase 3: v8 ignore** | Vue 2 Branch in OsButton mit `/* v8 ignore */` für 100% Coverage in Vitest |
|
||||
| 2026-02-08 | **Phase 3: Docker Build** | ui-library Stage in Dockerfile + Dockerfile.maintenance, COPY --from=ui-library |
|
||||
| 2026-02-08 | **Phase 3: CI-Fix** | Relativer Pfad `file:../packages/ui` statt absolut für yarn install außerhalb Docker |
|
||||
| 2026-02-08 | **Phase 3: Storybook Fix** | TypeScript-Fehler in Stories behoben (`default` aus args entfernt) |
|
||||
| 2026-02-08 | **Phase 3: attrs/listeners** | OsButton forwarded jetzt attrs + $listeners für Vue 2 (getCurrentInstance) |
|
||||
| 2026-02-08 | **Phase 3: Jest Mock erweitert** | Alle Composition API Funktionen (computed, ref, watch, etc.) im Mock |
|
||||
| 2026-02-08 | **Phase 3: 15 Buttons migriert** | GroupForm, EmbedComponent, DonationInfo, CommentCard, MapStylesButtons, GroupMember, embeds, notifications, privacy, terms-and-conditions-confirm |
|
||||
| 2026-02-08 | **Phase 3: Test-Updates** | privacy.spec.js Selektoren, notifications Snapshot, DonationInfo.spec.js |
|
||||
| 2026-02-08 | **OsButton: appearance Prop** | Neue `appearance` Prop: `filled` (default), `outline`, `ghost` - ermöglicht base-button Stile |
|
||||
| 2026-02-08 | **OsButton: xs Size** | Exakte Pixel-Werte für base-button --small: h-26px, px-8px, text-12px, rounded-5px |
|
||||
| 2026-02-08 | **OsButton: outline primary** | Grüner Rahmen + grüner Text + hellgrüner Hintergrund-Tint (rgba(25,122,49,0.18)) |
|
||||
| 2026-02-08 | **OsButton: ghost primary** | Transparenter Hintergrund, grüner Text, Hover füllt grün, Active dunkler |
|
||||
| 2026-02-08 | **OsButton: Focus Style** | `focus:outline-dashed focus:outline-1` statt ring (wie base-button) |
|
||||
| 2026-02-08 | **OsButton: Active State** | `active:bg-[var(--color-*-hover)]` für dunkleren Hintergrund beim Drücken |
|
||||
| 2026-02-08 | **Visuelle Validierung** | Tracking-Tabelle in PROJEKT.md für manuelle Button-Vergleiche (4/16 validiert) |
|
||||
| 2026-02-08 | **Storybook Grayscale Theme** | Vollständige CSS-Variablen: default, active-states, contrast-inverse |
|
||||
| 2026-02-08 | **Tailwind Source Filter** | `@import "tailwindcss" source(none)` - verhindert Markdown-Scanning |
|
||||
| 2026-02-08 | **Button Variants Konsistenz** | Alle 21 compound variants mit korrekten active-states (`--color-*-active`) |
|
||||
| 2026-02-08 | **CSS-Variablen erweitert** | `--color-secondary/warning/success/info-active` in ocelot-ui-variables.scss |
|
||||
| 2026-02-08 | **Story Dokumentation** | "Medium (37.5px)" → "Medium (36px)" korrigiert |
|
||||
| 2026-02-08 | **Playwright Toleranz** | `maxDiffPixelRatio: 0.03` für Cross-Platform Font-Rendering |
|
||||
| 2026-02-09 | **Disabled-Styles korrigiert** | CSS-Variablen `--color-disabled`, filled: grauer Hintergrund statt opacity |
|
||||
| 2026-02-09 | **terms-and-conditions-confirm** | Read T&C Button → `appearance="outline" variant="primary"` |
|
||||
| 2026-02-09 | **Visuelle Validierung** | 10/16 Buttons validiert (terms-and-conditions-confirm.vue abgeschlossen) |
|
||||
| 2026-02-09 | **Disabled:active/hover Fix** | CSS-Regeln in index.css mit höherer Spezifität für sofortige disabled-Darstellung |
|
||||
| 2026-02-09 | **notifications.vue** | Check All + Uncheck All → `appearance="outline" variant="primary"` |
|
||||
| 2026-02-09 | **Visuelle Validierung** | 14/16 Buttons validiert (notifications.vue abgeschlossen) |
|
||||
| 2026-02-09 | **embeds.vue** | Allow All → `appearance="outline" variant="primary"` |
|
||||
| 2026-02-09 | **Disabled Border-Fix** | CSS-Regeln in index.css: `border-style: solid` + `border-width: 0.8px` bei :disabled |
|
||||
| 2026-02-09 | **Visuelle Validierung abgeschlossen** | 16/16 Buttons validiert (100%) ✅ Milestone 5 erfolgreich |
|
||||
| 2026-02-09 | **Button-Analyse erweitert** | 14 weitere Buttons identifiziert (ohne icon/circle/loading) → Scope: 16/35 |
|
||||
| 2026-02-09 | **Scope auf ~90 erweitert** | ~60 weitere Buttons mit icon/circle/loading identifiziert |
|
||||
| 2026-02-09 | **Milestone 4a: 8 Buttons** | DisableModal, DeleteUserModal, ReleaseModal, ContributionForm, EnterNonce, MySomethingList, ImageUploader (2x) |
|
||||
| 2026-02-09 | **ImageUploader CSS-Fix** | `position: absolute !important` für crop-confirm (überschreibt OsButton `relative`) |
|
||||
| 2026-02-09 | **§16a hinzugefügt** | Webapp ↔ Maintenance Code-Sharing: Webapp als Source of Truth (Entscheidung #69) |
|
||||
| 2026-02-09 | **§16b hinzugefügt** | Daten-Entkopplung: ViewModel/Mapper Pattern für API-agnostische Komponenten (Entscheidung #70) |
|
||||
| 2026-02-09 | **NotificationMenu.vue** | 2 Buttons migriert (ghost primary), padding-top Fix für vertical-align Unterschied |
|
||||
| 2026-02-09 | **Milestone 4a abgeschlossen** | 6 weitere Buttons migriert: donations.vue (Save), profile/_id/_slug.vue (Unblock, Unmute), badges.vue (Remove), notifications/index.vue (Mark All Read), ReportRow.vue (More Details) |
|
||||
| 2026-02-10 | **Wasserfarben-Farbschema** | Greyscale-Theme → Aquarell-Farben (Ultramarin, Dioxazin-Violett, Alizarin, Ocker, Viridian, Cöruleum), WCAG AA konform |
|
||||
| 2026-02-10 | **Stories konsolidiert** | Primary/Secondary/Danger/Default entfernt → AllVariants; AllSizes/AllAppearances/Disabled/FullWidth zeigen alle 7 Varianten |
|
||||
| 2026-02-10 | **Appearance: Filled/Outline/Ghost** | Einzelne Stories umbenannt und mit allen 7 Varianten erweitert |
|
||||
| 2026-02-10 | **Playground-Story** | Interaktive Controls (argTypes nur in Playground, nicht global) |
|
||||
| 2026-02-10 | **Einheitlicher Border** | `border-[0.8px] border-solid border-transparent` als Base-Klasse für alle Appearances |
|
||||
| 2026-02-10 | **WCAG 2.4.7 Fix** | Default-Variante: `focus:outline-none` → `focus:outline-dashed focus:outline-current` |
|
||||
| 2026-02-10 | **Keyboard A11y Test** | Playwright-Test fokussiert alle Buttons und prüft `outlineStyle !== 'none'` |
|
||||
| 2026-02-10 | **data-appearance Attribut** | OsButton rendert `data-appearance` auf `<button>`; CSS-Selektoren nutzen `[data-appearance="filled"]` statt escaped Tailwind-Klassen |
|
||||
| 2026-02-10 | **Code-Review Fixes** | Unit-Tests: spezifischere Assertions (Compound-Variant-Logik), Trailing Spaces in Testnamen, ESLint restrict-template-expressions Fix |
|
||||
| 2026-02-10 | **CSS-Linting** | `@eslint/css` + `tailwind-csstree` für Tailwind v4 Custom Syntax; `excludeCSS()` Helper verhindert JS-Regel-Konflikte; Regeln: no-empty-blocks, no-duplicate-imports, no-invalid-at-rules |
|
||||
| 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) |
|
||||
|
||||
---
|
||||
|
||||
@ -1608,6 +1909,301 @@ Vor dem Erstellen einer Komponente diese Fragen beantworten:
|
||||
|
||||
---
|
||||
|
||||
## 16a. Webapp ↔ Maintenance Code-Sharing
|
||||
|
||||
### Problemstellung
|
||||
|
||||
Die Webapp und Maintenance-App sind aktuell verschachtelt und sollen getrennt werden.
|
||||
Einige Business-Komponenten werden in beiden Apps benötigt, gehören aber nicht in die UI-Library.
|
||||
|
||||
**Das DX-Problem:** "shared" hat kein logisches Kriterium außer "wird in beiden gebraucht".
|
||||
|
||||
### Analysierte Optionen
|
||||
|
||||
| Option | Beschreibung | Bewertung |
|
||||
|--------|--------------|-----------|
|
||||
| **A: Domain Packages** | `@ocelot-social/auth`, `@ocelot-social/posts`, etc. | Gut bei vielen Komponenten, aber Overhead |
|
||||
| **B: Core + Duplikation** | Composables teilen, Komponenten duplizieren | Gut wenn UI unterschiedlich |
|
||||
| **C: Webapp als Source** | Maintenance importiert aus Webapp | Einfachste Lösung |
|
||||
|
||||
### Empfehlung: Option C (Webapp als Source of Truth)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ @ocelot-social/ui │
|
||||
│ ───────────────── │
|
||||
│ • OsButton, OsModal, OsCard, OsInput │
|
||||
│ • Rein präsentational, keine Abhängigkeiten │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ webapp/ │
|
||||
│ ─────── │
|
||||
│ • Alle Business-Komponenten (Source of Truth) │
|
||||
│ • Composables in webapp/lib/composables/ │
|
||||
│ • GraphQL in webapp/graphql/ │
|
||||
│ • Ist die "Haupt-App" │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ maintenance/ │
|
||||
│ ──────────── │
|
||||
│ • Importiert aus @ocelot-social/ui │
|
||||
│ • Importiert aus webapp/ via Alias │
|
||||
│ • Nur maintenance-spezifische Komponenten lokal │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Umsetzung
|
||||
|
||||
**maintenance/nuxt.config.js:**
|
||||
```javascript
|
||||
export default {
|
||||
alias: {
|
||||
'@webapp': '../webapp',
|
||||
'@ocelot-social/ui': '../packages/ui/dist'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Import in Maintenance:**
|
||||
```typescript
|
||||
// UI-Komponenten aus Library
|
||||
import { OsButton, OsModal } from '@ocelot-social/ui'
|
||||
|
||||
// Business-Komponenten aus Webapp
|
||||
import FollowButton from '@webapp/components/FollowButton.vue'
|
||||
import PostTeaser from '@webapp/components/PostTeaser.vue'
|
||||
|
||||
// Composables aus Webapp
|
||||
import { useAuth } from '@webapp/lib/composables/useAuth'
|
||||
import { useFollow } from '@webapp/lib/composables/useFollow'
|
||||
```
|
||||
|
||||
### Kriterien für Entwickler
|
||||
|
||||
| Frage | Antwort |
|
||||
|-------|---------|
|
||||
| Wo suche ich eine UI-Komponente? | `@ocelot-social/ui` |
|
||||
| Wo suche ich eine Business-Komponente? | `webapp/components/` |
|
||||
| Wo erstelle ich eine neue geteilte Komponente? | `webapp/components/` |
|
||||
| Wo erstelle ich maintenance-spezifische Komponenten? | `maintenance/components/` |
|
||||
|
||||
### Vorteile
|
||||
|
||||
1. **Klare Regel:** Alles Business-bezogene ist in Webapp
|
||||
2. **Kein neues Package:** Weniger Overhead
|
||||
3. **Eine Source of Truth:** Keine Sync-Probleme
|
||||
4. **Einfache Migration:** Später ggf. Domain-Packages extrahieren
|
||||
|
||||
### Spätere Evolution (optional)
|
||||
|
||||
Wenn klare Patterns entstehen, können Domain-Packages extrahiert werden:
|
||||
|
||||
```
|
||||
Phase 1 (jetzt): Webapp ist Source of Truth
|
||||
Phase 2 (später): Patterns identifizieren
|
||||
Phase 3 (später): @ocelot-social/auth, @ocelot-social/posts, etc.
|
||||
```
|
||||
|
||||
### Entscheidung
|
||||
|
||||
| # | Datum | Entscheidung |
|
||||
|---|-------|--------------|
|
||||
| 68 | 2026-02-09 | Webapp als Source of Truth für geteilte Business-Komponenten |
|
||||
|
||||
---
|
||||
|
||||
## 16b. Daten-Entkopplung (ViewModel/Mapper Pattern)
|
||||
|
||||
### Problemstellung
|
||||
|
||||
Komponenten sind oft direkt an API/GraphQL-Strukturen gekoppelt:
|
||||
|
||||
```vue
|
||||
<!-- ❌ Tight Coupling -->
|
||||
<UserCard :user="graphqlResponse.User" />
|
||||
|
||||
// Komponente kennt GraphQL-Struktur
|
||||
props.user.avatar.url
|
||||
props.user._followedByCurrentUserCount // Underscore?!
|
||||
props.user.__typename // Leaked!
|
||||
```
|
||||
|
||||
**Probleme:**
|
||||
- Schema-Änderung = alle Komponenten anpassen
|
||||
- `__typename`, `_count` etc. leaken in die UI
|
||||
- Schwer testbar (braucht echte GraphQL-Struktur)
|
||||
- Komponenten nicht wiederverwendbar
|
||||
|
||||
### Lösung: ViewModel + Mapper Pattern
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ GraphQL / API Layer │
|
||||
│ • Queries & Mutations │
|
||||
│ • Generated Types (graphql-codegen) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Mappers (einziger Ort der API-Struktur kennt) │
|
||||
│ • toUserCardViewModel(graphqlUser) → UserCardViewModel │
|
||||
│ • toPostTeaserViewModel(graphqlPost) → PostTeaserViewModel │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ViewModels (was die UI braucht) │
|
||||
│ • UserCardViewModel { displayName, avatarUrl, ... } │
|
||||
│ • PostTeaserViewModel { title, excerpt, ... } │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Presentational Components (kennen nur ViewModels) │
|
||||
│ • <UserCard :user="UserCardViewModel" /> │
|
||||
│ • <PostTeaser :post="PostTeaserViewModel" /> │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Implementierung
|
||||
|
||||
**1. ViewModels definieren:**
|
||||
```typescript
|
||||
// types/viewModels.ts
|
||||
export interface UserCardViewModel {
|
||||
id: string
|
||||
displayName: string
|
||||
avatarUrl: string | null
|
||||
followerCount: number
|
||||
isFollowedByMe: boolean
|
||||
}
|
||||
|
||||
export interface PostTeaserViewModel {
|
||||
id: string
|
||||
title: string
|
||||
excerpt: string
|
||||
authorName: string
|
||||
authorAvatarUrl: string | null
|
||||
createdAt: Date
|
||||
commentCount: number
|
||||
canEdit: boolean
|
||||
}
|
||||
```
|
||||
|
||||
**2. Mapper-Funktionen:**
|
||||
```typescript
|
||||
// mappers/userMapper.ts
|
||||
import type { UserCardViewModel } from '~/types/viewModels'
|
||||
import type { UserGraphQL } from '~/graphql/types'
|
||||
|
||||
export function toUserCardViewModel(
|
||||
user: UserGraphQL,
|
||||
currentUserId?: string
|
||||
): UserCardViewModel {
|
||||
return {
|
||||
id: user.id,
|
||||
displayName: user.name || user.slug || 'Anonymous',
|
||||
avatarUrl: user.avatar?.url ?? null,
|
||||
followerCount: user._followedByCurrentUserCount ?? 0,
|
||||
isFollowedByMe: user.followedByCurrentUser ?? false,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**3. Komponenten nutzen nur ViewModels:**
|
||||
```vue
|
||||
<!-- components/UserCard.vue -->
|
||||
<script setup lang="ts">
|
||||
import type { UserCardViewModel } from '~/types/viewModels'
|
||||
|
||||
// Komponente kennt NUR das ViewModel, nicht GraphQL
|
||||
defineProps<{
|
||||
user: UserCardViewModel
|
||||
}>()
|
||||
</script>
|
||||
```
|
||||
|
||||
**4. Composables kapseln Mapping:**
|
||||
```typescript
|
||||
// composables/useUser.ts
|
||||
import { computed } from 'vue'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { GET_USER } from '~/graphql/queries'
|
||||
import { toUserCardViewModel } from '~/mappers/userMapper'
|
||||
import type { UserCardViewModel } from '~/types/viewModels'
|
||||
|
||||
export function useUser(userId: string) {
|
||||
const { result, loading, error } = useQuery(GET_USER, { id: userId })
|
||||
|
||||
const user = computed<UserCardViewModel | null>(() => {
|
||||
if (!result.value?.User) return null
|
||||
return toUserCardViewModel(result.value.User)
|
||||
})
|
||||
|
||||
return { user, loading, error }
|
||||
}
|
||||
```
|
||||
|
||||
### Ordnerstruktur
|
||||
|
||||
```
|
||||
webapp/
|
||||
├── graphql/
|
||||
│ ├── queries/
|
||||
│ ├── mutations/
|
||||
│ └── types/ # Generated by graphql-codegen
|
||||
├── types/
|
||||
│ └── viewModels.ts # UI-spezifische Interfaces
|
||||
├── mappers/
|
||||
│ ├── userMapper.ts
|
||||
│ ├── postMapper.ts
|
||||
│ └── index.ts
|
||||
├── lib/
|
||||
│ └── composables/
|
||||
│ ├── useAuth.ts
|
||||
│ ├── useUser.ts # Gibt ViewModel zurück
|
||||
│ └── usePost.ts
|
||||
├── components/ # Presentational (nur ViewModels)
|
||||
│ ├── UserCard.vue
|
||||
│ └── PostTeaser.vue
|
||||
└── pages/ # Nutzen Composables
|
||||
└── users/[id].vue
|
||||
```
|
||||
|
||||
### Vorteile
|
||||
|
||||
| Aspekt | Ohne Mapper | Mit Mapper |
|
||||
|--------|-------------|------------|
|
||||
| **API-Änderung** | Alle Komponenten anpassen | Nur Mapper anpassen |
|
||||
| **Testing** | Mock GraphQL Response | Einfaches ViewModel-Objekt |
|
||||
| **Wiederverwendung** | Komponente an API gebunden | Komponente API-agnostisch |
|
||||
| **TypeScript** | Komplexe/generierte Types | Klare, einfache Interfaces |
|
||||
| **webapp ↔ maintenance** | Verschiedene Strukturen | Gleiche ViewModels |
|
||||
|
||||
### Regeln
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Regel 1: Komponenten definieren was sie BRAUCHEN │
|
||||
│ (ViewModel), nicht was die API LIEFERT │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Regel 2: Mapper sind der EINZIGE Ort der API kennt │
|
||||
│ API-Änderung = nur Mapper ändern │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Regel 3: Composables kapseln Fetching + Mapping │
|
||||
│ useUser() gibt UserCardViewModel zurück │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Regel 4: Presentational Components sind API-agnostisch │
|
||||
│ Einfach testbar, wiederverwendbar │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Entscheidung
|
||||
|
||||
| # | Datum | Entscheidung |
|
||||
|---|-------|--------------|
|
||||
| 70 | 2026-02-09 | ViewModel/Mapper Pattern für Daten-Entkopplung |
|
||||
|
||||
---
|
||||
|
||||
## 17. Externe Abhängigkeiten
|
||||
|
||||
### Übersicht
|
||||
|
||||
@ -1,9 +1,22 @@
|
||||
// TODO: Update eslint-config-it4c to support ESLint 10 (currently incompatible)
|
||||
import css from '@eslint/css'
|
||||
import config, { vue3, vitest } from 'eslint-config-it4c'
|
||||
import jsdocPlugin from 'eslint-plugin-jsdoc'
|
||||
import playwrightPlugin from 'eslint-plugin-playwright'
|
||||
import storybookPlugin from 'eslint-plugin-storybook'
|
||||
import vuejsAccessibilityPlugin from 'eslint-plugin-vuejs-accessibility'
|
||||
import { tailwind4 } from 'tailwind-csstree'
|
||||
|
||||
import type { Linter } from 'eslint'
|
||||
|
||||
/** Exclude CSS files from JS-focused config blocks (JS rules crash on CSS language) */
|
||||
function excludeCSS(configs: Linter.Config[]): Linter.Config[] {
|
||||
return configs.map((c) => {
|
||||
// Don't touch global-ignores-only blocks
|
||||
if (Object.keys(c).length === 1 && 'ignores' in c) return c
|
||||
return { ...c, ignores: [...(c.ignores ?? []), '**/*.css'] }
|
||||
})
|
||||
}
|
||||
|
||||
export default [
|
||||
{
|
||||
@ -17,10 +30,11 @@ export default [
|
||||
'playwright-report/',
|
||||
],
|
||||
},
|
||||
...config,
|
||||
...vue3,
|
||||
...vitest,
|
||||
...excludeCSS(config),
|
||||
...excludeCSS(vue3),
|
||||
...excludeCSS(vitest),
|
||||
{
|
||||
ignores: ['**/*.css'],
|
||||
rules: {
|
||||
// TODO: replace with alias
|
||||
'import-x/no-relative-parent-imports': 'off',
|
||||
@ -82,4 +96,18 @@ export default [
|
||||
...vuejsAccessibilityPlugin.configs.recommended.rules,
|
||||
},
|
||||
},
|
||||
{
|
||||
// CSS files with Tailwind v4 syntax support
|
||||
files: ['**/*.css'],
|
||||
plugins: { css },
|
||||
language: 'css/css',
|
||||
languageOptions: {
|
||||
customSyntax: tailwind4,
|
||||
},
|
||||
rules: {
|
||||
'css/no-empty-blocks': 'error',
|
||||
'css/no-duplicate-imports': 'error',
|
||||
'css/no-invalid-at-rules': 'error',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
123
packages/ui/package-lock.json
generated
@ -17,6 +17,8 @@
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "^0.18.2",
|
||||
"@axe-core/playwright": "^4.11.1",
|
||||
"@eslint/css": "^0.14.1",
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@size-limit/file": "^12.0.0",
|
||||
"@storybook/vue3-vite": "^10.2.7",
|
||||
@ -37,6 +39,7 @@
|
||||
"publint": "^0.3.17",
|
||||
"size-limit": "^12.0.0",
|
||||
"storybook": "^10.2.7",
|
||||
"tailwind-csstree": "^0.1.4",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
@ -1314,6 +1317,42 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/css": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/css/-/css-0.14.1.tgz",
|
||||
"integrity": "sha512-NXiteSacmpaXqgyIW3+GcNzexXyfC0kd+gig6WTjD4A74kBGJeNx1tV0Hxa0v7x0+mnIyKfGPhGNs1uhRFdh+w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/css-tree": "^3.6.6",
|
||||
"@eslint/plugin-kit": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/css-tree": {
|
||||
"version": "3.6.8",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/css-tree/-/css-tree-3.6.8.tgz",
|
||||
"integrity": "sha512-s0f40zY7dlMp8i0Jf0u6l/aSswS0WRAgkhgETgiCJRcxIWb4S/Sp9uScKHWbkM3BnoFLbJbmOYk5AZUDFVxaLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.23.0",
|
||||
"source-map-js": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/css-tree/node_modules/mdn-data": {
|
||||
"version": "2.23.0",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.23.0.tgz",
|
||||
"integrity": "sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
|
||||
@ -1400,6 +1439,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fontsource-variable/inter": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.2.8.tgz",
|
||||
"integrity": "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==",
|
||||
"dev": true,
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@graphql-eslint/eslint-plugin": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-eslint/eslint-plugin/-/eslint-plugin-4.4.0.tgz",
|
||||
@ -3927,66 +3976,6 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.7.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.1.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.7.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.1.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
|
||||
@ -12845,6 +12834,20 @@
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-csstree": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-csstree/-/tailwind-csstree-0.1.4.tgz",
|
||||
"integrity": "sha512-FzD187HuFIZEyeR7Xy6sJbJll2d4SybS90satC8SKIuaNRC05CxMvdzN7BUsfDQffcnabckRM5OIcfArjsZ0mg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
|
||||
|
||||
@ -72,6 +72,8 @@
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "^0.18.2",
|
||||
"@axe-core/playwright": "^4.11.1",
|
||||
"@eslint/css": "^0.14.1",
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@size-limit/file": "^12.0.0",
|
||||
"@storybook/vue3-vite": "^10.2.7",
|
||||
@ -92,6 +94,7 @@
|
||||
"publint": "^0.3.17",
|
||||
"size-limit": "^12.0.0",
|
||||
"storybook": "^10.2.7",
|
||||
"tailwind-csstree": "^0.1.4",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
@ -50,7 +50,7 @@ export default defineConfig({
|
||||
/* Snapshot configuration */
|
||||
expect: {
|
||||
toHaveScreenshot: {
|
||||
/* Allow slight differences due to anti-aliasing */
|
||||
/* Allow slight differences due to font rendering across platforms (self-hosted font) */
|
||||
maxDiffPixelRatio: 0.01,
|
||||
},
|
||||
},
|
||||
|
||||
@ -11,24 +11,73 @@ describe('osButton', () => {
|
||||
expect(wrapper.text()).toBe('Click me')
|
||||
})
|
||||
|
||||
it('applies default variant classes', () => {
|
||||
const wrapper = mount(OsButton)
|
||||
expect(wrapper.classes()).toContain('bg-[var(--color-primary)]')
|
||||
describe('variant prop', () => {
|
||||
it('applies default variant classes by default', () => {
|
||||
const wrapper = mount(OsButton)
|
||||
// Default variant with filled appearance
|
||||
expect(wrapper.classes()).toContain('bg-[var(--color-default)]')
|
||||
})
|
||||
|
||||
it('applies primary variant classes', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { variant: 'primary' },
|
||||
})
|
||||
expect(wrapper.classes()).toContain('bg-[var(--color-primary)]')
|
||||
})
|
||||
|
||||
it('applies danger variant classes', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { variant: 'danger' },
|
||||
})
|
||||
expect(wrapper.classes()).toContain('bg-[var(--color-danger)]')
|
||||
})
|
||||
})
|
||||
|
||||
it('applies size variant classes', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { size: 'sm' },
|
||||
describe('appearance prop', () => {
|
||||
it('applies filled appearance by default', () => {
|
||||
const wrapper = mount(OsButton)
|
||||
expect(wrapper.classes()).toContain('shadow-[inset_0_0_0_1px_rgba(0,0,0,0.05)]')
|
||||
})
|
||||
|
||||
it('applies outline appearance classes', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { appearance: 'outline', variant: 'primary' },
|
||||
})
|
||||
expect(wrapper.classes()).toContain('bg-transparent')
|
||||
expect(wrapper.classes()).toContain('border-[var(--color-primary)]')
|
||||
expect(wrapper.classes()).toContain('text-[var(--color-primary)]')
|
||||
})
|
||||
|
||||
it('applies ghost appearance classes', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { appearance: 'ghost', variant: 'primary' },
|
||||
})
|
||||
expect(wrapper.classes()).toContain('bg-transparent')
|
||||
expect(wrapper.classes()).toContain('text-[var(--color-primary)]')
|
||||
expect(wrapper.classes()).not.toContain('border-[var(--color-primary)]')
|
||||
})
|
||||
expect(wrapper.classes()).toContain('h-8')
|
||||
expect(wrapper.classes()).toContain('text-sm')
|
||||
})
|
||||
|
||||
it('applies variant classes', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { variant: 'danger' },
|
||||
describe('size prop', () => {
|
||||
it('applies md size by default', () => {
|
||||
const wrapper = mount(OsButton)
|
||||
expect(wrapper.classes()).toContain('h-[36px]')
|
||||
})
|
||||
|
||||
it('applies sm size classes', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { size: 'sm' },
|
||||
})
|
||||
expect(wrapper.classes()).toContain('h-[26px]')
|
||||
expect(wrapper.classes()).toContain('text-[12px]')
|
||||
})
|
||||
|
||||
it('applies lg size classes', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { size: 'lg' },
|
||||
})
|
||||
expect(wrapper.classes()).toContain('h-12')
|
||||
})
|
||||
expect(wrapper.classes()).toContain('bg-[var(--color-danger)]')
|
||||
})
|
||||
|
||||
it('applies fullWidth class', () => {
|
||||
@ -40,7 +89,7 @@ describe('osButton', () => {
|
||||
|
||||
it('merges custom classes', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { class: 'my-custom-class' },
|
||||
attrs: { class: 'my-custom-class' },
|
||||
})
|
||||
expect(wrapper.classes()).toContain('my-custom-class')
|
||||
})
|
||||
@ -65,6 +114,23 @@ describe('osButton', () => {
|
||||
expect(wrapper.emitted('click')).toHaveLength(1)
|
||||
})
|
||||
|
||||
describe('focus styles', () => {
|
||||
it('default variant has dashed outline focus style using currentColor', () => {
|
||||
const wrapper = mount(OsButton)
|
||||
expect(wrapper.classes()).toContain('focus:outline-dashed')
|
||||
expect(wrapper.classes()).toContain('focus:outline-current')
|
||||
expect(wrapper.classes()).toContain('focus:outline-1')
|
||||
})
|
||||
|
||||
it('colored variants have dashed outline focus style', () => {
|
||||
const wrapper = mount(OsButton, {
|
||||
props: { variant: 'primary' },
|
||||
})
|
||||
expect(wrapper.classes()).toContain('focus:outline-dashed')
|
||||
expect(wrapper.classes()).toContain('focus:outline-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('keyboard accessibility', () => {
|
||||
it('renders as native button element for keyboard support', () => {
|
||||
const wrapper = mount(OsButton)
|
||||
|
||||
@ -6,14 +6,24 @@ const meta: Meta<typeof OsButton> = {
|
||||
title: 'Components/OsButton',
|
||||
component: OsButton,
|
||||
tags: ['autodocs'],
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof OsButton>
|
||||
|
||||
export const Playground: Story = {
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['primary', 'secondary', 'danger', 'warning', 'success', 'info', 'ghost', 'outline'],
|
||||
options: ['default', 'primary', 'secondary', 'danger', 'warning', 'success', 'info'],
|
||||
},
|
||||
appearance: {
|
||||
control: 'select',
|
||||
options: ['filled', 'outline', 'ghost'],
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['xs', 'sm', 'md', 'lg', 'xl'],
|
||||
options: ['sm', 'md', 'lg', 'xl'],
|
||||
},
|
||||
fullWidth: {
|
||||
control: 'boolean',
|
||||
@ -22,50 +32,19 @@ const meta: Meta<typeof OsButton> = {
|
||||
control: 'boolean',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof OsButton>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
variant: 'primary',
|
||||
default: 'Primary Button',
|
||||
appearance: 'filled',
|
||||
size: 'md',
|
||||
fullWidth: false,
|
||||
disabled: false,
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { OsButton },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<OsButton v-bind="args">{{ args.default }}</OsButton>',
|
||||
}),
|
||||
}
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
variant: 'secondary',
|
||||
default: 'Secondary Button',
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { OsButton },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<OsButton v-bind="args">{{ args.default }}</OsButton>',
|
||||
}),
|
||||
}
|
||||
|
||||
export const Danger: Story = {
|
||||
args: {
|
||||
variant: 'danger',
|
||||
default: 'Danger Button',
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { OsButton },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<OsButton v-bind="args">{{ args.default }}</OsButton>',
|
||||
template: '<OsButton v-bind="args">Button</OsButton>',
|
||||
}),
|
||||
}
|
||||
|
||||
@ -74,14 +53,13 @@ export const AllVariants: Story = {
|
||||
components: { OsButton },
|
||||
template: `
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<OsButton variant="default">Default</OsButton>
|
||||
<OsButton variant="primary">Primary</OsButton>
|
||||
<OsButton variant="secondary">Secondary</OsButton>
|
||||
<OsButton variant="danger">Danger</OsButton>
|
||||
<OsButton variant="warning">Warning</OsButton>
|
||||
<OsButton variant="success">Success</OsButton>
|
||||
<OsButton variant="info">Info</OsButton>
|
||||
<OsButton variant="ghost">Ghost</OsButton>
|
||||
<OsButton variant="outline">Outline</OsButton>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
@ -91,41 +69,219 @@ export const AllSizes: Story = {
|
||||
render: () => ({
|
||||
components: { OsButton },
|
||||
template: `
|
||||
<div class="flex flex-col gap-2 items-start">
|
||||
<OsButton size="xs">Extra Small</OsButton>
|
||||
<OsButton size="sm">Small</OsButton>
|
||||
<OsButton size="md">Medium</OsButton>
|
||||
<OsButton size="lg">Large</OsButton>
|
||||
<OsButton size="xl">Extra Large</OsButton>
|
||||
<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 size="sm" variant="default">Default</OsButton>
|
||||
<OsButton size="sm" variant="primary">Primary</OsButton>
|
||||
<OsButton size="sm" variant="secondary">Secondary</OsButton>
|
||||
<OsButton size="sm" variant="danger">Danger</OsButton>
|
||||
<OsButton size="sm" variant="warning">Warning</OsButton>
|
||||
<OsButton size="sm" variant="success">Success</OsButton>
|
||||
<OsButton size="sm" variant="info">Info</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 size="md" variant="default">Default</OsButton>
|
||||
<OsButton size="md" variant="primary">Primary</OsButton>
|
||||
<OsButton size="md" variant="secondary">Secondary</OsButton>
|
||||
<OsButton size="md" variant="danger">Danger</OsButton>
|
||||
<OsButton size="md" variant="warning">Warning</OsButton>
|
||||
<OsButton size="md" variant="success">Success</OsButton>
|
||||
<OsButton size="md" variant="info">Info</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Large</h3>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<OsButton size="lg" variant="default">Default</OsButton>
|
||||
<OsButton size="lg" variant="primary">Primary</OsButton>
|
||||
<OsButton size="lg" variant="secondary">Secondary</OsButton>
|
||||
<OsButton size="lg" variant="danger">Danger</OsButton>
|
||||
<OsButton size="lg" variant="warning">Warning</OsButton>
|
||||
<OsButton size="lg" variant="success">Success</OsButton>
|
||||
<OsButton size="lg" variant="info">Info</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Extra Large</h3>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<OsButton size="xl" variant="default">Default</OsButton>
|
||||
<OsButton size="xl" variant="primary">Primary</OsButton>
|
||||
<OsButton size="xl" variant="secondary">Secondary</OsButton>
|
||||
<OsButton size="xl" variant="danger">Danger</OsButton>
|
||||
<OsButton size="xl" variant="warning">Warning</OsButton>
|
||||
<OsButton size="xl" variant="success">Success</OsButton>
|
||||
<OsButton size="xl" variant="info">Info</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const AllAppearances: Story = {
|
||||
render: () => ({
|
||||
components: { OsButton },
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Filled (default)</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<OsButton appearance="filled" variant="default">Default</OsButton>
|
||||
<OsButton appearance="filled" variant="primary">Primary</OsButton>
|
||||
<OsButton appearance="filled" variant="secondary">Secondary</OsButton>
|
||||
<OsButton appearance="filled" variant="danger">Danger</OsButton>
|
||||
<OsButton appearance="filled" variant="warning">Warning</OsButton>
|
||||
<OsButton appearance="filled" variant="success">Success</OsButton>
|
||||
<OsButton appearance="filled" variant="info">Info</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="default">Default</OsButton>
|
||||
<OsButton appearance="outline" variant="primary">Primary</OsButton>
|
||||
<OsButton appearance="outline" variant="secondary">Secondary</OsButton>
|
||||
<OsButton appearance="outline" variant="danger">Danger</OsButton>
|
||||
<OsButton appearance="outline" variant="warning">Warning</OsButton>
|
||||
<OsButton appearance="outline" variant="success">Success</OsButton>
|
||||
<OsButton appearance="outline" variant="info">Info</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="default">Default</OsButton>
|
||||
<OsButton appearance="ghost" variant="primary">Primary</OsButton>
|
||||
<OsButton appearance="ghost" variant="secondary">Secondary</OsButton>
|
||||
<OsButton appearance="ghost" variant="danger">Danger</OsButton>
|
||||
<OsButton appearance="ghost" variant="warning">Warning</OsButton>
|
||||
<OsButton appearance="ghost" variant="success">Success</OsButton>
|
||||
<OsButton appearance="ghost" variant="info">Info</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const AppearanceFilled: Story = {
|
||||
name: 'Appearance: Filled',
|
||||
render: () => ({
|
||||
components: { OsButton },
|
||||
template: `
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<OsButton appearance="filled" variant="default">Default</OsButton>
|
||||
<OsButton appearance="filled" variant="primary">Primary</OsButton>
|
||||
<OsButton appearance="filled" variant="secondary">Secondary</OsButton>
|
||||
<OsButton appearance="filled" variant="danger">Danger</OsButton>
|
||||
<OsButton appearance="filled" variant="warning">Warning</OsButton>
|
||||
<OsButton appearance="filled" variant="success">Success</OsButton>
|
||||
<OsButton appearance="filled" variant="info">Info</OsButton>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const AppearanceOutline: Story = {
|
||||
name: 'Appearance: Outline',
|
||||
render: () => ({
|
||||
components: { OsButton },
|
||||
template: `
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<OsButton appearance="outline" variant="default">Default</OsButton>
|
||||
<OsButton appearance="outline" variant="primary">Primary</OsButton>
|
||||
<OsButton appearance="outline" variant="secondary">Secondary</OsButton>
|
||||
<OsButton appearance="outline" variant="danger">Danger</OsButton>
|
||||
<OsButton appearance="outline" variant="warning">Warning</OsButton>
|
||||
<OsButton appearance="outline" variant="success">Success</OsButton>
|
||||
<OsButton appearance="outline" variant="info">Info</OsButton>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const AppearanceGhost: Story = {
|
||||
name: 'Appearance: Ghost',
|
||||
render: () => ({
|
||||
components: { OsButton },
|
||||
template: `
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<OsButton appearance="ghost" variant="default">Default</OsButton>
|
||||
<OsButton appearance="ghost" variant="primary">Primary</OsButton>
|
||||
<OsButton appearance="ghost" variant="secondary">Secondary</OsButton>
|
||||
<OsButton appearance="ghost" variant="danger">Danger</OsButton>
|
||||
<OsButton appearance="ghost" variant="warning">Warning</OsButton>
|
||||
<OsButton appearance="ghost" variant="success">Success</OsButton>
|
||||
<OsButton appearance="ghost" variant="info">Info</OsButton>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
default: 'Disabled Button',
|
||||
},
|
||||
render: (args) => ({
|
||||
render: () => ({
|
||||
components: { OsButton },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<OsButton v-bind="args">{{ args.default }}</OsButton>',
|
||||
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 disabled appearance="filled" variant="default">Default</OsButton>
|
||||
<OsButton disabled appearance="filled" variant="primary">Primary</OsButton>
|
||||
<OsButton disabled appearance="filled" variant="secondary">Secondary</OsButton>
|
||||
<OsButton disabled appearance="filled" variant="danger">Danger</OsButton>
|
||||
<OsButton disabled appearance="filled" variant="warning">Warning</OsButton>
|
||||
<OsButton disabled appearance="filled" variant="success">Success</OsButton>
|
||||
<OsButton disabled appearance="filled" variant="info">Info</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Outline</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<OsButton disabled appearance="outline" variant="default">Default</OsButton>
|
||||
<OsButton disabled appearance="outline" variant="primary">Primary</OsButton>
|
||||
<OsButton disabled appearance="outline" variant="secondary">Secondary</OsButton>
|
||||
<OsButton disabled appearance="outline" variant="danger">Danger</OsButton>
|
||||
<OsButton disabled appearance="outline" variant="warning">Warning</OsButton>
|
||||
<OsButton disabled appearance="outline" variant="success">Success</OsButton>
|
||||
<OsButton disabled appearance="outline" variant="info">Info</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold mb-2">Ghost</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<OsButton disabled appearance="ghost" variant="default">Default</OsButton>
|
||||
<OsButton disabled appearance="ghost" variant="primary">Primary</OsButton>
|
||||
<OsButton disabled appearance="ghost" variant="secondary">Secondary</OsButton>
|
||||
<OsButton disabled appearance="ghost" variant="danger">Danger</OsButton>
|
||||
<OsButton disabled appearance="ghost" variant="warning">Warning</OsButton>
|
||||
<OsButton disabled appearance="ghost" variant="success">Success</OsButton>
|
||||
<OsButton disabled appearance="ghost" variant="info">Info</OsButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const FullWidth: Story = {
|
||||
args: {
|
||||
fullWidth: true,
|
||||
default: 'Full Width Button',
|
||||
},
|
||||
render: (args) => ({
|
||||
render: () => ({
|
||||
components: { OsButton },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<OsButton v-bind="args">{{ args.default }}</OsButton>',
|
||||
template: `
|
||||
<div class="flex flex-col gap-2">
|
||||
<OsButton fullWidth variant="default">Default</OsButton>
|
||||
<OsButton fullWidth variant="primary">Primary</OsButton>
|
||||
<OsButton fullWidth variant="secondary">Secondary</OsButton>
|
||||
<OsButton fullWidth variant="danger">Danger</OsButton>
|
||||
<OsButton fullWidth variant="warning">Warning</OsButton>
|
||||
<OsButton fullWidth variant="success">Success</OsButton>
|
||||
<OsButton fullWidth variant="info">Info</OsButton>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
@ -14,6 +14,13 @@ import type { Page } from '@playwright/test'
|
||||
const STORY_URL = '/iframe.html?id=components-osbutton'
|
||||
const STORY_ROOT = '#storybook-root'
|
||||
|
||||
/**
|
||||
* Wait for all fonts to be loaded before taking screenshots
|
||||
*/
|
||||
async function waitForFonts(page: Page) {
|
||||
await page.evaluate(async () => document.fonts.ready)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to run accessibility check on the current page
|
||||
*/
|
||||
@ -23,35 +30,32 @@ async function checkA11y(page: Page) {
|
||||
expect(results.violations).toEqual([])
|
||||
}
|
||||
|
||||
test.describe('OsButton keyboard accessibility', () => {
|
||||
test('all variants show visible focus indicator', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--all-appearances&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
|
||||
const buttons = root.locator('button:not([disabled])')
|
||||
const count = await buttons.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const button = buttons.nth(i)
|
||||
await button.focus()
|
||||
const outline = await button.evaluate((el) => getComputedStyle(el).outlineStyle)
|
||||
const label = (await button.textContent()) ?? ''
|
||||
expect(outline, `Button "${label}" must have visible focus outline`).not.toBe('none')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('OsButton visual regression', () => {
|
||||
test('primary variant', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--primary&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await expect(root.locator('button')).toHaveScreenshot('primary.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('secondary variant', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--secondary&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await expect(root.locator('button')).toHaveScreenshot('secondary.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('danger variant', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--danger&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await expect(root.locator('button')).toHaveScreenshot('danger.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('all variants', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--all-variants&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
await expect(root.locator('.flex')).toHaveScreenshot('all-variants.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
@ -60,7 +64,44 @@ test.describe('OsButton visual regression', () => {
|
||||
await page.goto(`${STORY_URL}--all-sizes&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await expect(root.locator('.flex')).toHaveScreenshot('all-sizes.png')
|
||||
await waitForFonts(page)
|
||||
await expect(root.locator('.flex-col').first()).toHaveScreenshot('all-sizes.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('appearance filled', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--appearance-filled&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
await expect(root.locator('.flex')).toHaveScreenshot('appearance-filled.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('appearance outline', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--appearance-outline&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
await expect(root.locator('.flex')).toHaveScreenshot('appearance-outline.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('appearance ghost', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--appearance-ghost&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
await expect(root.locator('.flex')).toHaveScreenshot('appearance-ghost.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('all appearances', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--all-appearances&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
await expect(root.locator('.flex-col').first()).toHaveScreenshot('all-appearances.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
@ -68,7 +109,8 @@ test.describe('OsButton visual regression', () => {
|
||||
await page.goto(`${STORY_URL}--disabled&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await expect(root.locator('button')).toHaveScreenshot('disabled.png')
|
||||
await waitForFonts(page)
|
||||
await expect(root.locator('.flex-col').first()).toHaveScreenshot('disabled.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
@ -76,7 +118,8 @@ test.describe('OsButton visual regression', () => {
|
||||
await page.goto(`${STORY_URL}--full-width&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await expect(root.locator('button')).toHaveScreenshot('full-width.png')
|
||||
await waitForFonts(page)
|
||||
await expect(root.locator('.flex-col').first()).toHaveScreenshot('full-width.png')
|
||||
await checkA11y(page)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,50 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue-demi'
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, getCurrentInstance, h, isVue2 } from 'vue-demi'
|
||||
|
||||
import { cn } from '../../utils'
|
||||
|
||||
import { buttonVariants } from './button.variants'
|
||||
|
||||
import type { ButtonVariants } from './button.variants'
|
||||
import type { PropType } from 'vue-demi'
|
||||
|
||||
export interface OsButtonProps {
|
||||
/** Visual style variant */
|
||||
variant?: ButtonVariants['variant']
|
||||
/** Size of the button */
|
||||
size?: ButtonVariants['size']
|
||||
/** Whether button takes full width of container */
|
||||
fullWidth?: boolean
|
||||
/** HTML button type attribute */
|
||||
type?: 'button' | 'submit' | 'reset'
|
||||
/** Whether the button is disabled */
|
||||
disabled?: boolean
|
||||
/** Additional CSS classes */
|
||||
class?: string
|
||||
}
|
||||
export default defineComponent({
|
||||
name: 'OsButton',
|
||||
// In Vue 2, inheritAttrs must be false to manually forward attrs
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
variant: {
|
||||
type: String as PropType<ButtonVariants['variant']>,
|
||||
default: 'default',
|
||||
},
|
||||
appearance: {
|
||||
type: String as PropType<ButtonVariants['appearance']>,
|
||||
default: 'filled',
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<ButtonVariants['size']>,
|
||||
default: 'md',
|
||||
},
|
||||
fullWidth: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<'button' | 'submit' | 'reset'>,
|
||||
default: 'button',
|
||||
},
|
||||
disabled: {
|
||||
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 props = withDefaults(defineProps<OsButtonProps>(), {
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
fullWidth: false,
|
||||
type: 'button',
|
||||
disabled: false,
|
||||
class: '',
|
||||
// Get component instance for Vue 2 $listeners access
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
return () => {
|
||||
const children = slots.default?.()
|
||||
/* 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,
|
||||
},
|
||||
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) || ''),
|
||||
...restAttrs,
|
||||
},
|
||||
children,
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const classes = computed(() =>
|
||||
cn(
|
||||
buttonVariants({
|
||||
variant: props.variant,
|
||||
size: props.size,
|
||||
fullWidth: props.fullWidth,
|
||||
}),
|
||||
props.class,
|
||||
),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button :type="type" :disabled="disabled" :class="classes">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
@ -11,68 +11,257 @@ import type { VariantProps } from 'class-variance-authority'
|
||||
* - Easy customization via class prop
|
||||
*/
|
||||
export const buttonVariants = cva(
|
||||
// Base classes (always applied)
|
||||
// Base classes (always applied) - matching ds-button styles
|
||||
[
|
||||
'inline-flex items-center justify-center',
|
||||
'font-medium',
|
||||
'transition-colors duration-200',
|
||||
'focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
|
||||
'disabled:pointer-events-none disabled:opacity-50',
|
||||
'inline-flex items-center justify-center align-middle [white-space-collapse:collapse]',
|
||||
'relative appearance-none',
|
||||
'font-semibold tracking-[0.05em]', // 0.75px at 15px font-size
|
||||
'rounded-[4px]',
|
||||
'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 border-transparent', // consistent border across all appearances
|
||||
'focus:outline-1', // outline width, style set per variant
|
||||
'disabled:pointer-events-none disabled:cursor-default',
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: [
|
||||
'bg-[var(--color-primary)] text-[var(--color-primary-contrast)]',
|
||||
'hover:bg-[var(--color-primary-hover)]',
|
||||
'focus-visible:ring-[var(--color-primary)]',
|
||||
// Color variants - actual styling comes from compound variants with appearance
|
||||
default: ['focus:outline-dashed focus:outline-current'],
|
||||
primary: ['focus:outline-dashed focus:outline-[var(--color-primary)]'],
|
||||
secondary: ['focus:outline-dashed focus:outline-[var(--color-secondary)]'],
|
||||
danger: ['focus:outline-dashed focus:outline-[var(--color-danger)]'],
|
||||
warning: ['focus:outline-dashed focus:outline-[var(--color-warning)]'],
|
||||
success: ['focus:outline-dashed focus:outline-[var(--color-success)]'],
|
||||
info: ['focus:outline-dashed focus:outline-[var(--color-info)]'],
|
||||
},
|
||||
appearance: {
|
||||
filled: [
|
||||
'shadow-[inset_0_0_0_1px_rgba(0,0,0,0.05)]',
|
||||
// Disabled: gray background with white text (matches buttonStates mixin)
|
||||
// Keep inset shadow to prevent layout shift
|
||||
'disabled:bg-[var(--color-disabled)] disabled:text-[var(--color-disabled-contrast)]',
|
||||
'disabled:border-[var(--color-disabled)] disabled:shadow-[inset_0_0_0_1px_transparent]',
|
||||
],
|
||||
secondary: [
|
||||
'bg-[var(--color-secondary)] text-[var(--color-secondary-contrast)]',
|
||||
'hover:bg-[var(--color-secondary-hover)]',
|
||||
'focus-visible:ring-[var(--color-secondary)]',
|
||||
],
|
||||
danger: [
|
||||
'bg-[var(--color-danger)] text-[var(--color-danger-contrast)]',
|
||||
'hover:bg-[var(--color-danger-hover)]',
|
||||
'focus-visible:ring-[var(--color-danger)]',
|
||||
],
|
||||
warning: [
|
||||
'bg-[var(--color-warning)] text-[var(--color-warning-contrast)]',
|
||||
'hover:bg-[var(--color-warning-hover)]',
|
||||
'focus-visible:ring-[var(--color-warning)]',
|
||||
],
|
||||
success: [
|
||||
'bg-[var(--color-success)] text-[var(--color-success-contrast)]',
|
||||
'hover:bg-[var(--color-success-hover)]',
|
||||
'focus-visible:ring-[var(--color-success)]',
|
||||
],
|
||||
info: [
|
||||
'bg-[var(--color-info)] text-[var(--color-info-contrast)]',
|
||||
'hover:bg-[var(--color-info-hover)]',
|
||||
'focus-visible:ring-[var(--color-info)]',
|
||||
],
|
||||
ghost: ['bg-transparent', 'hover:bg-gray-100', 'focus-visible:ring-gray-400'],
|
||||
outline: [
|
||||
'border border-current bg-transparent',
|
||||
'hover:bg-gray-100',
|
||||
'focus-visible:ring-gray-400',
|
||||
'bg-transparent shadow-none',
|
||||
// Disabled: gray border and text
|
||||
'disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)]',
|
||||
],
|
||||
ghost: [
|
||||
'bg-transparent shadow-none',
|
||||
// Disabled: gray text
|
||||
'disabled:text-[var(--color-disabled)]',
|
||||
],
|
||||
},
|
||||
size: {
|
||||
xs: 'h-6 px-2 text-xs rounded',
|
||||
sm: 'h-8 px-3 text-sm rounded-md',
|
||||
md: 'h-10 px-4 text-base rounded-md',
|
||||
lg: 'h-12 px-6 text-lg rounded-lg',
|
||||
xl: 'h-14 px-8 text-xl rounded-lg',
|
||||
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',
|
||||
},
|
||||
fullWidth: {
|
||||
true: 'w-full',
|
||||
false: '',
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
// Filled variants (default appearance)
|
||||
{
|
||||
variant: 'default',
|
||||
appearance: 'filled',
|
||||
class: [
|
||||
'bg-[var(--color-default)] text-[var(--color-default-contrast)] border-[var(--color-default)]',
|
||||
'hover:bg-[var(--color-default-hover)] hover:border-[var(--color-default-hover)]',
|
||||
'active:bg-[var(--color-default-active)] active:border-[var(--color-default-active)] active:text-[var(--color-default-contrast-inverse)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'primary',
|
||||
appearance: 'filled',
|
||||
class: [
|
||||
'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)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'secondary',
|
||||
appearance: 'filled',
|
||||
class: [
|
||||
'bg-[var(--color-secondary)] text-[var(--color-secondary-contrast)] border-[var(--color-secondary)]',
|
||||
'hover:bg-[var(--color-secondary-hover)] hover:border-[var(--color-secondary-hover)] hover:text-[var(--color-secondary-contrast)]',
|
||||
'active:bg-[var(--color-secondary-active)] active:border-[var(--color-secondary-active)] active:text-[var(--color-secondary-contrast)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'danger',
|
||||
appearance: 'filled',
|
||||
class: [
|
||||
'bg-[var(--color-danger)] text-[var(--color-danger-contrast)] border-[var(--color-danger)]',
|
||||
'hover:bg-[var(--color-danger-hover)] hover:border-[var(--color-danger-hover)] hover:text-[var(--color-danger-contrast)]',
|
||||
'active:bg-[var(--color-danger-active)] active:border-[var(--color-danger-active)] active:text-[var(--color-danger-contrast)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'warning',
|
||||
appearance: 'filled',
|
||||
class: [
|
||||
'bg-[var(--color-warning)] text-[var(--color-warning-contrast)] border-[var(--color-warning)]',
|
||||
'hover:bg-[var(--color-warning-hover)] hover:border-[var(--color-warning-hover)] hover:text-[var(--color-warning-contrast)]',
|
||||
'active:bg-[var(--color-warning-active)] active:border-[var(--color-warning-active)] active:text-[var(--color-warning-contrast)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'success',
|
||||
appearance: 'filled',
|
||||
class: [
|
||||
'bg-[var(--color-success)] text-[var(--color-success-contrast)] border-[var(--color-success)]',
|
||||
'hover:bg-[var(--color-success-hover)] hover:border-[var(--color-success-hover)] hover:text-[var(--color-success-contrast)]',
|
||||
'active:bg-[var(--color-success-active)] active:border-[var(--color-success-active)] active:text-[var(--color-success-contrast)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'info',
|
||||
appearance: 'filled',
|
||||
class: [
|
||||
'bg-[var(--color-info)] text-[var(--color-info-contrast)] border-[var(--color-info)]',
|
||||
'hover:bg-[var(--color-info-hover)] hover:border-[var(--color-info-hover)] hover:text-[var(--color-info-contrast)]',
|
||||
'active:bg-[var(--color-info-active)] active:border-[var(--color-info-active)] active:text-[var(--color-info-contrast)]',
|
||||
],
|
||||
},
|
||||
// Outline variants
|
||||
{
|
||||
variant: 'default',
|
||||
appearance: 'outline',
|
||||
class: [
|
||||
'border-[var(--color-default-contrast)] text-[var(--color-default-contrast)]',
|
||||
'hover:bg-[var(--color-default-hover)]',
|
||||
'active:bg-[var(--color-default-active)] active:text-[var(--color-default-contrast-inverse)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'primary',
|
||||
appearance: 'outline',
|
||||
class: [
|
||||
'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)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'secondary',
|
||||
appearance: 'outline',
|
||||
class: [
|
||||
'border-[var(--color-secondary)] text-[var(--color-secondary)]',
|
||||
'hover:bg-[var(--color-secondary)] hover:text-[var(--color-secondary-contrast)]',
|
||||
'active:bg-[var(--color-secondary-active)] active:border-[var(--color-secondary-active)] active:text-[var(--color-secondary-contrast)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'danger',
|
||||
appearance: 'outline',
|
||||
class: [
|
||||
'border-[var(--color-danger)] text-[var(--color-danger)]',
|
||||
'hover:bg-[var(--color-danger)] hover:text-[var(--color-danger-contrast)]',
|
||||
'active:bg-[var(--color-danger-active)] active:border-[var(--color-danger-active)] active:text-[var(--color-danger-contrast)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'warning',
|
||||
appearance: 'outline',
|
||||
class: [
|
||||
'border-[var(--color-warning)] text-[var(--color-warning)]',
|
||||
'hover:bg-[var(--color-warning)] hover:text-[var(--color-warning-contrast)]',
|
||||
'active:bg-[var(--color-warning-active)] active:border-[var(--color-warning-active)] active:text-[var(--color-warning-contrast)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'success',
|
||||
appearance: 'outline',
|
||||
class: [
|
||||
'border-[var(--color-success)] text-[var(--color-success)]',
|
||||
'hover:bg-[var(--color-success)] hover:text-[var(--color-success-contrast)]',
|
||||
'active:bg-[var(--color-success-active)] active:border-[var(--color-success-active)] active:text-[var(--color-success-contrast)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'info',
|
||||
appearance: 'outline',
|
||||
class: [
|
||||
'border-[var(--color-info)] text-[var(--color-info)]',
|
||||
'hover:bg-[var(--color-info)] hover:text-[var(--color-info-contrast)]',
|
||||
'active:bg-[var(--color-info-active)] active:border-[var(--color-info-active)] active:text-[var(--color-info-contrast)]',
|
||||
],
|
||||
},
|
||||
// Ghost variants - transparent background, colored text, hover fills, active darkens
|
||||
{
|
||||
variant: 'default',
|
||||
appearance: 'ghost',
|
||||
class: [
|
||||
'text-[var(--color-default-contrast)]',
|
||||
'hover:bg-[var(--color-default-hover)]',
|
||||
'active:bg-[var(--color-default-active)] active:text-[var(--color-default-contrast-inverse)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'primary',
|
||||
appearance: 'ghost',
|
||||
class: [
|
||||
'text-[var(--color-primary)]',
|
||||
'hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)]',
|
||||
'active:bg-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'secondary',
|
||||
appearance: 'ghost',
|
||||
class: [
|
||||
'text-[var(--color-secondary)]',
|
||||
'hover:bg-[var(--color-secondary)] hover:text-[var(--color-secondary-contrast)]',
|
||||
'active:bg-[var(--color-secondary-active)] active:text-[var(--color-secondary-contrast)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'danger',
|
||||
appearance: 'ghost',
|
||||
class: [
|
||||
'text-[var(--color-danger)]',
|
||||
'hover:bg-[var(--color-danger)] hover:text-[var(--color-danger-contrast)]',
|
||||
'active:bg-[var(--color-danger-active)] active:text-[var(--color-danger-contrast)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'warning',
|
||||
appearance: 'ghost',
|
||||
class: [
|
||||
'text-[var(--color-warning)]',
|
||||
'hover:bg-[var(--color-warning)] hover:text-[var(--color-warning-contrast)]',
|
||||
'active:bg-[var(--color-warning-active)] active:text-[var(--color-warning-contrast)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'success',
|
||||
appearance: 'ghost',
|
||||
class: [
|
||||
'text-[var(--color-success)]',
|
||||
'hover:bg-[var(--color-success)] hover:text-[var(--color-success-contrast)]',
|
||||
'active:bg-[var(--color-success-active)] active:text-[var(--color-success-contrast)]',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'info',
|
||||
appearance: 'ghost',
|
||||
class: [
|
||||
'text-[var(--color-info)]',
|
||||
'hover:bg-[var(--color-info)] hover:text-[var(--color-info-contrast)]',
|
||||
'active:bg-[var(--color-info-active)] active:text-[var(--color-info-contrast)]',
|
||||
],
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
variant: 'default',
|
||||
appearance: 'filled',
|
||||
size: 'md',
|
||||
fullWidth: false,
|
||||
},
|
||||
|
||||
@ -1,5 +1,37 @@
|
||||
@import "tailwindcss";
|
||||
/*
|
||||
* Tailwind CSS ohne Preflight (Reset)
|
||||
* Preflight überschreibt globale Styles und kollidiert mit bestehenden Apps.
|
||||
* Stattdessen importieren wir nur Theme und Utilities.
|
||||
*
|
||||
* WICHTIG: Kein globaler button-Reset hier!
|
||||
* Das würde alle Buttons in der Webapp überschreiben (BaseButton, ds-button, etc.)
|
||||
* OsButton verwendet stattdessen Tailwind-Klassen direkt.
|
||||
*/
|
||||
@import "tailwindcss/theme";
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
/* Scan component files for utility classes */
|
||||
@source "../components/**/*.vue";
|
||||
@source "../**/*.ts";
|
||||
|
||||
/*
|
||||
* Disabled state overrides
|
||||
* These rules have higher specificity than :active/:hover to ensure
|
||||
* disabled buttons immediately show disabled styling, even when clicked.
|
||||
*/
|
||||
|
||||
/* Filled buttons: gray background when disabled, regardless of hover/active */
|
||||
button[data-appearance="filled"]:disabled:hover,
|
||||
button[data-appearance="filled"]:disabled:active {
|
||||
background-color: var(--color-disabled) !important;
|
||||
border-color: var(--color-disabled) !important;
|
||||
color: var(--color-disabled-contrast) !important;
|
||||
}
|
||||
|
||||
/* Outline buttons: transparent background when disabled, regardless of hover/active */
|
||||
button[data-appearance="outline"]:disabled:hover,
|
||||
button[data-appearance="outline"]:disabled:active {
|
||||
background-color: transparent !important;
|
||||
border-color: var(--color-disabled) !important;
|
||||
color: var(--color-disabled) !important;
|
||||
}
|
||||
|
||||
2
packages/ui/src/vite-env.d.ts
vendored
@ -1,5 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '@fontsource-variable/inter'
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue-demi'
|
||||
|
||||
|
||||
@ -12,6 +12,9 @@ import { defineConfig } from 'vitest/config'
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
export default defineConfig({
|
||||
optimizeDeps: {
|
||||
exclude: ['vue-demi'],
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
tailwindcss(),
|
||||
|
||||
@ -6,6 +6,14 @@ COPY styleguide .
|
||||
RUN yarn install --production=false --frozen-lockfile --non-interactive
|
||||
RUN yarn run build:lib
|
||||
|
||||
FROM node:25.6.0-alpine AS ui-library
|
||||
RUN apk --no-cache add git python3 make g++
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
COPY packages/ui .
|
||||
RUN yarn install --production=false --frozen-lockfile --non-interactive
|
||||
RUN yarn run build
|
||||
|
||||
FROM node:25.6.0-alpine AS base
|
||||
LABEL org.label-schema.name="ocelot.social:webapp"
|
||||
LABEL org.label-schema.description="Web Frontend of the Social Network Software ocelot.social"
|
||||
@ -22,6 +30,7 @@ RUN apk --no-cache add git python3 make g++ bash jq
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
COPY --from=styleguide ./app/ /styleguide/
|
||||
COPY --from=ui-library ./app/ /packages/ui/
|
||||
CMD ["/bin/bash", "-c", "yarn run start"]
|
||||
|
||||
FROM base AS development
|
||||
|
||||
@ -16,10 +16,19 @@ COPY styleguide .
|
||||
RUN yarn install --production=false --frozen-lockfile --non-interactive
|
||||
RUN yarn run build:lib
|
||||
|
||||
FROM node:25.6.0-alpine AS ui-library
|
||||
RUN apk --no-cache add git python3 make g++
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
COPY packages/ui .
|
||||
RUN yarn install --production=false --frozen-lockfile --non-interactive
|
||||
RUN yarn run build
|
||||
|
||||
FROM node:25.6.0-alpine AS build
|
||||
ENV NODE_ENV="production"
|
||||
RUN apk --no-cache add git python3 make g++ bash jq
|
||||
COPY --from=styleguide ./app/ /styleguide/
|
||||
COPY --from=ui-library ./app/ /packages/ui/
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
COPY webapp/ .
|
||||
|
||||
57
webapp/assets/_new/styles/ocelot-ui-variables.scss
Normal file
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* CSS Custom Properties für @ocelot-social/ui
|
||||
*
|
||||
* Diese Datei mappt die bestehenden SCSS-Variablen auf die CSS Custom Properties,
|
||||
* die von der UI-Library erwartet werden.
|
||||
*
|
||||
* Hinweis: Die SCSS-Variablen werden über styleResources global geladen (nuxt.config.js).
|
||||
*/
|
||||
|
||||
:root {
|
||||
// Default (grau) - entspricht ds-button ohne Modifier
|
||||
--color-default: #{$background-color-softer}; // neutral-90: rgb(245, 244, 246)
|
||||
--color-default-hover: #{$color-neutral-70}; // rgb(203, 199, 209) - deutlich dunkler
|
||||
--color-default-active: #{$color-neutral-60}; // rgb(177, 171, 186) - noch dunkler
|
||||
--color-default-contrast: #{$text-color-base};
|
||||
--color-default-contrast-inverse: #{$color-neutral-100}; // weiß für dunkle Hintergründe
|
||||
|
||||
// Primary (grün) - verwendet $text-color-primary-inverse wie ds-button
|
||||
--color-primary: #{$color-primary};
|
||||
--color-primary-hover: #{$color-primary-light}; // filled buttons hover to lighter color
|
||||
--color-primary-active: #{$color-primary-dark}; // active state uses darker color
|
||||
--color-primary-contrast: #{$text-color-primary-inverse};
|
||||
|
||||
// Secondary (blau)
|
||||
--color-secondary: #{$color-secondary};
|
||||
--color-secondary-hover: #{$color-secondary-active};
|
||||
--color-secondary-active: #{darken($color-secondary, 15%)};
|
||||
--color-secondary-contrast: #{$text-color-secondary-inverse};
|
||||
|
||||
// Danger (rot)
|
||||
--color-danger: #{$color-danger};
|
||||
--color-danger-hover: #{$color-danger-light}; // filled buttons hover to lighter color
|
||||
--color-danger-active: #{$color-danger-dark}; // active state uses darker color
|
||||
--color-danger-contrast: #{$text-color-danger-inverse};
|
||||
|
||||
// Warning (orange)
|
||||
--color-warning: #{$color-warning};
|
||||
--color-warning-hover: #{$color-warning-active};
|
||||
--color-warning-active: #{darken($color-warning, 15%)};
|
||||
--color-warning-contrast: #{$text-color-primary-inverse};
|
||||
|
||||
// Success (grün, wie primary)
|
||||
--color-success: #{$color-success};
|
||||
--color-success-hover: #{$color-success-active};
|
||||
--color-success-active: #{darken($color-success, 15%)};
|
||||
--color-success-contrast: #{$text-color-primary-inverse};
|
||||
|
||||
// Info (blau, wie secondary)
|
||||
--color-info: #{$color-secondary};
|
||||
--color-info-hover: #{$color-secondary-active};
|
||||
--color-info-active: #{darken($color-secondary, 15%)};
|
||||
--color-info-contrast: #{$text-color-secondary-inverse};
|
||||
|
||||
// Disabled state
|
||||
--color-disabled: #{$color-neutral-60}; // rgb(177, 171, 186)
|
||||
--color-disabled-contrast: #{$color-neutral-100}; // weiß
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
<base-card v-if="isUnavailable" class="comment-card">
|
||||
<p>
|
||||
<base-icon name="ban" />
|
||||
{{ this.$t('comment.content.unavailable-placeholder') }}
|
||||
{{ $t('comment.content.unavailable-placeholder') }}
|
||||
</p>
|
||||
</base-card>
|
||||
<base-card v-else :class="commentClass" :id="anchor">
|
||||
@ -35,9 +35,15 @@
|
||||
/>
|
||||
<template v-else>
|
||||
<content-viewer :content="commentContent" class="content" />
|
||||
<base-button v-if="hasLongContent" size="small" ghost @click="isCollapsed = !isCollapsed">
|
||||
<os-button
|
||||
v-if="hasLongContent"
|
||||
size="sm"
|
||||
appearance="ghost"
|
||||
variant="primary"
|
||||
@click="isCollapsed = !isCollapsed"
|
||||
>
|
||||
{{ isCollapsed ? $t('comment.show.more') : $t('comment.show.less') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</template>
|
||||
<div class="actions">
|
||||
<shout-button
|
||||
@ -49,7 +55,7 @@
|
||||
node-type="Comment"
|
||||
/>
|
||||
<base-button
|
||||
:title="this.$t('post.comment.reply')"
|
||||
:title="$t('post.comment.reply')"
|
||||
icon="level-down"
|
||||
class="reply-button"
|
||||
circle
|
||||
@ -62,6 +68,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { COMMENT_MAX_UNTRUNCATED_LENGTH, COMMENT_TRUNCATE_TO_LENGTH } from '~/constants/comment'
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
@ -74,6 +81,7 @@ import scrollToAnchor from '~/mixins/scrollToAnchor.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OsButton,
|
||||
UserTeaser,
|
||||
ContentMenu,
|
||||
ContentViewer,
|
||||
@ -208,13 +216,6 @@ export default {
|
||||
justify-content: space-between;
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes highlight {
|
||||
|
||||
@ -173,9 +173,15 @@
|
||||
</ds-flex-item>
|
||||
<ds-flex-item width="0.15" />
|
||||
<ds-flex-item class="action-buttons-group" width="2">
|
||||
<base-button data-test="cancel-button" :disabled="loading" @click="$router.back()">
|
||||
<os-button
|
||||
data-test="cancel-button"
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
:disabled="loading"
|
||||
@click="$router.back()"
|
||||
>
|
||||
{{ $t('actions.cancel') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
<base-button type="submit" icon="check" :loading="loading" :disabled="errors" filled>
|
||||
{{ $t('actions.save') }}
|
||||
</base-button>
|
||||
@ -187,6 +193,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import gql from 'graphql-tag'
|
||||
import { mapGetters } from 'vuex'
|
||||
import Editor from '~/components/Editor/Editor'
|
||||
@ -202,11 +209,12 @@ import GetCategories from '~/mixins/getCategoriesMixin.js'
|
||||
export default {
|
||||
mixins: [GetCategories],
|
||||
components: {
|
||||
Editor,
|
||||
ImageUploader,
|
||||
PageParamsLink,
|
||||
CategoriesSelect,
|
||||
DatePicker,
|
||||
Editor,
|
||||
ImageUploader,
|
||||
OsButton,
|
||||
PageParamsLink,
|
||||
},
|
||||
props: {
|
||||
contribution: {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import flushPromises from 'flush-promises'
|
||||
import DeleteData from './DeleteData.vue'
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
@ -36,9 +37,7 @@ describe('DeleteData.vue', () => {
|
||||
success: jest.fn(),
|
||||
},
|
||||
$router: {
|
||||
history: {
|
||||
push: jest.fn(),
|
||||
},
|
||||
push: jest.fn(),
|
||||
},
|
||||
}
|
||||
getters = {
|
||||
@ -86,9 +85,9 @@ describe('DeleteData.vue', () => {
|
||||
expect(wrapper.vm.deleteEnabled).toEqual(false)
|
||||
})
|
||||
|
||||
it('does not call the delete user mutation if deleteEnabled is false', () => {
|
||||
it('does not call the delete user mutation if deleteEnabled is false', async () => {
|
||||
deleteAccountBtn = wrapper.find('[data-test="delete-button"]')
|
||||
deleteAccountBtn.trigger('click')
|
||||
await deleteAccountBtn.trigger('click')
|
||||
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -99,8 +98,8 @@ describe('DeleteData.vue', () => {
|
||||
deleteAccountBtn = wrapper.find('[data-test="delete-button"]')
|
||||
})
|
||||
|
||||
it('if deleteEnabled is true and only deletes user ', () => {
|
||||
deleteAccountBtn.trigger('click')
|
||||
it('if deleteEnabled is true and only deletes user', async () => {
|
||||
await deleteAccountBtn.trigger('click')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
@ -111,14 +110,14 @@ describe('DeleteData.vue', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("deletes user's posts and comments if requested by default ", () => {
|
||||
it("deletes user's posts and comments if requested by default", async () => {
|
||||
enableContributionDeletionCheckbox = wrapper.find(
|
||||
'[data-test="contributions-deletion-checkbox"]',
|
||||
)
|
||||
enableContributionDeletionCheckbox.setChecked(true)
|
||||
enableCommentDeletionCheckbox = wrapper.find('[data-test="comments-deletion-checkbox"]')
|
||||
enableCommentDeletionCheckbox.setChecked(true)
|
||||
deleteAccountBtn.trigger('click')
|
||||
await deleteAccountBtn.trigger('click')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
@ -129,12 +128,12 @@ describe('DeleteData.vue', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("deletes a user's posts if requested", () => {
|
||||
it("deletes a user's posts if requested", async () => {
|
||||
enableContributionDeletionCheckbox = wrapper.find(
|
||||
'[data-test="contributions-deletion-checkbox"]',
|
||||
)
|
||||
enableContributionDeletionCheckbox.setChecked(true)
|
||||
deleteAccountBtn.trigger('click')
|
||||
await deleteAccountBtn.trigger('click')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
@ -145,10 +144,10 @@ describe('DeleteData.vue', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("deletes a user's comments if requested", () => {
|
||||
it("deletes a user's comments if requested", async () => {
|
||||
enableCommentDeletionCheckbox = wrapper.find('[data-test="comments-deletion-checkbox"]')
|
||||
enableCommentDeletionCheckbox.setChecked(true)
|
||||
deleteAccountBtn.trigger('click')
|
||||
await deleteAccountBtn.trigger('click')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
@ -166,7 +165,8 @@ describe('DeleteData.vue', () => {
|
||||
|
||||
it('redirect the user to the homepage', async () => {
|
||||
await deleteAccountBtn.trigger('click')
|
||||
expect(mocks.$router.history.push).toHaveBeenCalledWith('/')
|
||||
await flushPromises()
|
||||
expect(mocks.$router.push).toHaveBeenCalledWith('/')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -111,10 +111,14 @@ export default {
|
||||
`,
|
||||
variables: { id: this.currentUser.id, resource: resourceArgs },
|
||||
})
|
||||
.then(() => {
|
||||
.then(async () => {
|
||||
this.$toast.success(this.$t('settings.deleteUserAccount.success'))
|
||||
this.logout()
|
||||
this.$router.history.push('/')
|
||||
try {
|
||||
await this.logout()
|
||||
} catch {
|
||||
// Logout-Fehler ignorieren — Account ist bereits gelöscht
|
||||
}
|
||||
this.$router.push('/')
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toast.error(error.message)
|
||||
|
||||
@ -4,10 +4,18 @@ import DonationInfo from './DonationInfo.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const mockDate = new Date(2019, 11, 6)
|
||||
global.Date = jest.fn(() => mockDate)
|
||||
const OriginalDate = global.Date
|
||||
const mockDate = new OriginalDate(2019, 11, 6)
|
||||
|
||||
describe('DonationInfo.vue', () => {
|
||||
beforeAll(() => {
|
||||
global.Date = jest.fn(() => mockDate)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
global.Date = OriginalDate
|
||||
})
|
||||
|
||||
let mocks, wrapper, propsData
|
||||
|
||||
beforeEach(() => {
|
||||
@ -35,7 +43,7 @@ describe('DonationInfo.vue', () => {
|
||||
})
|
||||
|
||||
it('displays the action button', () => {
|
||||
expect(wrapper.find('.base-button').text()).toBe('donations.donate-now')
|
||||
expect(wrapper.find('button').text()).toBe('donations.donate-now')
|
||||
})
|
||||
|
||||
describe('mount with data', () => {
|
||||
|
||||
@ -1,19 +1,21 @@
|
||||
<template>
|
||||
<div class="donation-info">
|
||||
<progress-bar :label="label" :goal="goal" :progress="progress">
|
||||
<base-button size="small" filled @click="redirectToPage(links.DONATE)">
|
||||
<os-button size="sm" variant="primary" @click="redirectToPage(links.DONATE)">
|
||||
{{ $t('donations.donate-now') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</progress-bar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import links from '~/constants/links.js'
|
||||
import ProgressBar from '~/components/ProgressBar/ProgressBar.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OsButton,
|
||||
ProgressBar,
|
||||
},
|
||||
props: {
|
||||
|
||||
@ -24,12 +24,23 @@
|
||||
<h3>{{ $t('editor.embed.data_privacy_warning') }}</h3>
|
||||
<ds-text>{{ $t('editor.embed.data_privacy_info') }} {{ embedPublisher }}</ds-text>
|
||||
<div class="buttons">
|
||||
<base-button @click="closeOverlay()" data-test="cancel-button" danger>
|
||||
<os-button
|
||||
@click="closeOverlay()"
|
||||
data-test="cancel-button"
|
||||
appearance="outline"
|
||||
variant="danger"
|
||||
class="embed-button"
|
||||
>
|
||||
{{ $t('actions.cancel') }}
|
||||
</base-button>
|
||||
<base-button @click="allowEmbed()" data-test="play-now-button" filled>
|
||||
</os-button>
|
||||
<os-button
|
||||
@click="allowEmbed()"
|
||||
data-test="play-now-button"
|
||||
variant="primary"
|
||||
class="embed-button"
|
||||
>
|
||||
{{ $t('editor.embed.play_now') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</div>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="checkedAlwaysAllowEmbeds" />
|
||||
@ -47,11 +58,15 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { updateUserMutation } from '~/graphql/User.js'
|
||||
|
||||
export default {
|
||||
name: 'embed-component',
|
||||
components: {
|
||||
OsButton,
|
||||
},
|
||||
props: {
|
||||
dataEmbedUrl: {
|
||||
type: String,
|
||||
@ -222,9 +237,8 @@ export default {
|
||||
background-color: $color-neutral-100;
|
||||
|
||||
> .buttons {
|
||||
.base-button {
|
||||
button {
|
||||
margin-right: $space-small;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,17 +20,20 @@
|
||||
<ds-text>
|
||||
{{ $t('components.registration.email-nonce.form.click-next') }}
|
||||
</ds-text>
|
||||
<base-button :disabled="disabled" filled name="submit" type="submit">
|
||||
<os-button variant="primary" :disabled="disabled" name="submit" type="submit">
|
||||
{{ $t('components.registration.email-nonce.form.next') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
<slot></slot>
|
||||
</ds-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import registrationConstants from '~/constants/registration'
|
||||
|
||||
export default {
|
||||
name: 'EnterNonce',
|
||||
components: { OsButton },
|
||||
props: {
|
||||
email: { type: String, required: true },
|
||||
},
|
||||
|
||||
@ -133,7 +133,7 @@
|
||||
<!-- submit -->
|
||||
<ds-space margin-top="large">
|
||||
<nuxt-link to="/groups">
|
||||
<ds-button>{{ $t('actions.cancel') }}</ds-button>
|
||||
<os-button>{{ $t('actions.cancel') }}</os-button>
|
||||
</nuxt-link>
|
||||
<ds-button type="submit" icon="save" primary :disabled="checkFormError(errors)" fill>
|
||||
{{ update ? $t('group.update') : $t('group.save') }}
|
||||
@ -145,6 +145,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
|
||||
import { CATEGORIES_MIN, CATEGORIES_MAX } from '~/constants/categories.js'
|
||||
import {
|
||||
@ -165,6 +166,7 @@ export default {
|
||||
Editor,
|
||||
ActionRadiusSelect,
|
||||
LocationSelect,
|
||||
OsButton,
|
||||
},
|
||||
props: {
|
||||
update: {
|
||||
|
||||
@ -53,17 +53,18 @@
|
||||
</ds-chip>
|
||||
</template>
|
||||
<template #edit="scope">
|
||||
<base-button
|
||||
<os-button
|
||||
v-if="scope.row.membership.role !== 'owner'"
|
||||
size="small"
|
||||
primary
|
||||
appearance="outline"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
@click="
|
||||
isOpen = true
|
||||
userId = scope.row.user.id
|
||||
"
|
||||
>
|
||||
{{ $t('group.removeMemberButton') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</template>
|
||||
</ds-table>
|
||||
<ds-modal
|
||||
@ -79,12 +80,14 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { changeGroupMemberRoleMutation, removeUserFromGroupMutation } from '~/graphql/groups.js'
|
||||
import ProfileAvatar from '~/components/_new/generic/ProfileAvatar/ProfileAvatar'
|
||||
|
||||
export default {
|
||||
name: 'GroupMember',
|
||||
components: {
|
||||
OsButton,
|
||||
ProfileAvatar,
|
||||
},
|
||||
props: {
|
||||
|
||||
@ -27,7 +27,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scope>
|
||||
<style lang="scss" scoped>
|
||||
.login-button {
|
||||
color: $color-secondary;
|
||||
}
|
||||
|
||||
@ -1,20 +1,24 @@
|
||||
<template>
|
||||
<div>
|
||||
<base-button
|
||||
:class="['map-style-button', actualStyle === style.url ? '' : '--deactivated']"
|
||||
<os-button
|
||||
v-for="style in styles"
|
||||
:key="style.title"
|
||||
filled
|
||||
size="small"
|
||||
:appearance="actualStyle === style.url ? 'filled' : 'outline'"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="map-style-button"
|
||||
@click="setStyle(style.url)"
|
||||
>
|
||||
{{ style.title }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
|
||||
export default {
|
||||
components: { OsButton },
|
||||
name: 'MapStylesButtons',
|
||||
props: {
|
||||
styles: { type: Array, required: true },
|
||||
@ -25,15 +29,28 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// Outline (nicht ausgewählt) button styles
|
||||
button.map-style-button.bg-transparent {
|
||||
background-color: $background-color-softer !important;
|
||||
color: $text-color-base !important;
|
||||
}
|
||||
|
||||
button.map-style-button.bg-transparent:hover {
|
||||
background-color: $color-primary-light !important;
|
||||
border-color: $color-primary-light !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
button.map-style-button.bg-transparent:active {
|
||||
background-color: $color-primary-dark !important;
|
||||
border-color: $color-primary-dark !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.map-style-button {
|
||||
position: relative;
|
||||
margin-left: 6px;
|
||||
margin-bottom: 6px;
|
||||
margin-top: 6px;
|
||||
|
||||
&.--deactivated {
|
||||
color: $text-color-base;
|
||||
background-color: $background-color-softer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<p v-html="message" />
|
||||
|
||||
<template slot="footer">
|
||||
<template #footer>
|
||||
<base-button
|
||||
class="cancel"
|
||||
:danger="!modalData.buttons.confirm.danger"
|
||||
@ -42,6 +42,7 @@ export default {
|
||||
components: {
|
||||
SweetalertIcon,
|
||||
},
|
||||
emits: ['close'],
|
||||
props: {
|
||||
name: { type: String, default: '' }, // only used for compatibility with the other modals in 'Modal.vue'
|
||||
type: { type: String, required: true }, // only used for compatibility with the other modals in 'Modal.vue'
|
||||
@ -85,6 +86,7 @@ export default {
|
||||
}, 1500)
|
||||
} catch (err) {
|
||||
this.isOpen = false
|
||||
this.$emit('close')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
@ -36,8 +36,10 @@
|
||||
</ds-section>
|
||||
</div>
|
||||
|
||||
<template slot="footer">
|
||||
<base-button class="cancel" @click="cancel">{{ $t('actions.cancel') }}</base-button>
|
||||
<template #footer>
|
||||
<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">
|
||||
{{ $t('settings.deleteUserAccount.name') }}
|
||||
</base-button>
|
||||
@ -46,6 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import gql from 'graphql-tag'
|
||||
import { mapMutations } from 'vuex'
|
||||
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||
@ -55,9 +58,10 @@ import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
export default {
|
||||
name: 'DeleteUserModal',
|
||||
components: {
|
||||
UserTeaser,
|
||||
SweetalertIcon,
|
||||
DateTime,
|
||||
OsButton,
|
||||
SweetalertIcon,
|
||||
UserTeaser,
|
||||
},
|
||||
props: {
|
||||
userdata: { type: Object, required: true },
|
||||
@ -67,8 +71,6 @@ export default {
|
||||
isOpen: true,
|
||||
success: false,
|
||||
loading: false,
|
||||
// isAdmin: this.$store.getters['auth/isAdmin'],
|
||||
isAdmin: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -124,8 +126,9 @@ export default {
|
||||
}, 1000)
|
||||
},
|
||||
async confirm() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
this.loading = true
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($id: ID!, $resource: [Deletable]) {
|
||||
DeleteUser(id: $id, resource: $resource) {
|
||||
@ -135,26 +138,24 @@ export default {
|
||||
`,
|
||||
variables: { id: this.userdata.id, resource: ['Post', 'Comment'] },
|
||||
})
|
||||
.then(({ _data }) => {
|
||||
this.success = true
|
||||
this.$toast.success(this.$t('settings.deleteUserAccount.success'))
|
||||
setTimeout(() => {
|
||||
this.isOpen = false
|
||||
setTimeout(() => {
|
||||
this.success = false
|
||||
this.$emit('close')
|
||||
this.$router.history.replace('/')
|
||||
}, 500)
|
||||
}, 1500)
|
||||
this.loading = false
|
||||
})
|
||||
.catch((err) => {
|
||||
this.$emit('close')
|
||||
this.success = false
|
||||
this.$toast.error(err.message)
|
||||
this.success = true
|
||||
this.$toast.success(this.$t('settings.deleteUserAccount.success'))
|
||||
setTimeout(() => {
|
||||
this.isOpen = false
|
||||
this.loading = false
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.success = false
|
||||
this.$emit('close')
|
||||
this.$router.replace('/')
|
||||
}, 500)
|
||||
}, 1500)
|
||||
} catch (err) {
|
||||
this.success = false
|
||||
this.$toast.error(err.message)
|
||||
this.isOpen = false
|
||||
this.$emit('close')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -3,8 +3,10 @@
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<p v-html="message" />
|
||||
|
||||
<template slot="footer">
|
||||
<base-button class="cancel" @click="cancel">{{ $t('disable.cancel') }}</base-button>
|
||||
<template #footer>
|
||||
<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">
|
||||
{{ $t('disable.submit') }}
|
||||
</base-button>
|
||||
@ -13,10 +15,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'DisableModal',
|
||||
components: { OsButton },
|
||||
props: {
|
||||
name: { type: String, default: '' },
|
||||
type: { type: String, required: true },
|
||||
|
||||
@ -51,18 +51,18 @@
|
||||
<ds-flex class="notifications-link-container">
|
||||
<ds-flex-item>
|
||||
<nuxt-link :to="{ name: 'notifications' }">
|
||||
<base-button ghost primary>
|
||||
<os-button appearance="ghost" variant="primary">
|
||||
{{ $t('notifications.pageLink') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</nuxt-link>
|
||||
<base-button
|
||||
ghost
|
||||
primary
|
||||
<os-button
|
||||
appearance="ghost"
|
||||
variant="primary"
|
||||
@click="markAllAsRead(closeMenu)"
|
||||
data-test="markAllAsRead-button"
|
||||
>
|
||||
{{ $t('notifications.markAllAsRead') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
<div class="notifications-menu-popover">
|
||||
@ -79,6 +79,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import unionBy from 'lodash/unionBy'
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import {
|
||||
notificationQuery,
|
||||
markAsReadMutation,
|
||||
@ -95,6 +96,7 @@ export default {
|
||||
NotificationsTable,
|
||||
CounterIcon,
|
||||
Dropdown,
|
||||
OsButton,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -200,6 +202,7 @@ export default {
|
||||
background-color: $background-color-softer-active;
|
||||
text-align: right;
|
||||
padding: $space-x-small 0;
|
||||
padding-top: $space-x-small;
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -24,7 +24,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
onNextClick() {
|
||||
this.$router.history.push('/login')
|
||||
this.$router.push('/login')
|
||||
return true
|
||||
},
|
||||
},
|
||||
|
||||
@ -3,8 +3,10 @@
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<p v-html="message" />
|
||||
|
||||
<template slot="footer">
|
||||
<base-button class="cancel" @click="cancel">{{ $t('release.cancel') }}</base-button>
|
||||
<template #footer>
|
||||
<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">
|
||||
{{ $t('release.submit') }}
|
||||
</base-button>
|
||||
@ -13,9 +15,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'ReleaseModal',
|
||||
components: { OsButton },
|
||||
props: {
|
||||
name: { type: String, default: '' },
|
||||
type: { type: String, required: true },
|
||||
|
||||
@ -26,15 +26,15 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-show="!showCropper && imageCanBeCropped" class="crop-overlay">
|
||||
<base-button class="crop-confirm" filled @click="initCropper">
|
||||
<os-button class="crop-confirm" variant="primary" @click="initCropper">
|
||||
{{ $t('contribution.teaserImage.cropImage') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</div>
|
||||
<div v-show="showCropper" class="crop-overlay">
|
||||
<img id="cropping-image" />
|
||||
<base-button class="crop-confirm" filled @click="cropImage">
|
||||
<os-button class="crop-confirm" variant="primary" @click="cropImage">
|
||||
{{ $t('contribution.teaserImage.cropperConfirm') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
<base-button
|
||||
class="crop-cancel"
|
||||
icon="close"
|
||||
@ -49,8 +49,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueDropzone from 'nuxt-dropzone'
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import Cropper from 'cropperjs'
|
||||
import VueDropzone from 'nuxt-dropzone'
|
||||
import LoadingSpinner from '~/components/_new/generic/LoadingSpinner/LoadingSpinner'
|
||||
import 'cropperjs/dist/cropper.css'
|
||||
|
||||
@ -59,6 +60,7 @@ const minAspectRatio = 0.3
|
||||
export default {
|
||||
components: {
|
||||
LoadingSpinner,
|
||||
OsButton,
|
||||
VueDropzone,
|
||||
},
|
||||
props: {
|
||||
@ -197,7 +199,7 @@ export default {
|
||||
}
|
||||
|
||||
> .crop-confirm {
|
||||
position: absolute;
|
||||
position: absolute !important;
|
||||
left: $space-x-small;
|
||||
top: $space-x-small;
|
||||
z-index: $z-index-surface;
|
||||
|
||||
@ -28,12 +28,13 @@
|
||||
</li>
|
||||
</ul>
|
||||
<nuxt-link v-if="isTouchDevice && userLink" :to="userLink" class="link">
|
||||
<ds-button primary>{{ $t('user-teaser.popover.open-profile') }}</ds-button>
|
||||
<os-button variant="primary">{{ $t('user-teaser.popover.open-profile') }}</os-button>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import Badges from '~/components/Badges.vue'
|
||||
import LocationInfo from '~/components/LocationInfo/LocationInfo.vue'
|
||||
import { isTouchDevice } from '~/components/utils/isTouchDevice'
|
||||
@ -44,6 +45,7 @@ export default {
|
||||
components: {
|
||||
Badges,
|
||||
LocationInfo,
|
||||
OsButton,
|
||||
},
|
||||
props: {
|
||||
userId: { type: String },
|
||||
|
||||
@ -56,19 +56,27 @@
|
||||
>
|
||||
{{ isEditing ? $t('actions.save') : texts.addButton }}
|
||||
</base-button>
|
||||
<base-button v-if="isEditing" id="cancel" @click="handleCancel()">
|
||||
<os-button
|
||||
v-if="isEditing"
|
||||
id="cancel"
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
@click="handleCancel()"
|
||||
>
|
||||
{{ $t('actions.cancel') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</ds-space>
|
||||
</ds-space>
|
||||
</ds-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { mapMutations } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'MySomethingList',
|
||||
components: { OsButton },
|
||||
props: {
|
||||
useFormData: { type: Object, default: () => ({}) },
|
||||
useFormSchema: { type: Object, default: () => ({}) },
|
||||
@ -186,7 +194,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scope>
|
||||
<style lang="scss" scoped>
|
||||
.divider {
|
||||
opacity: 0.4;
|
||||
padding: 0 $space-small;
|
||||
|
||||
@ -11,9 +11,14 @@
|
||||
<span class="user-count">
|
||||
{{ $t('moderation.reports.numberOfUsers', { count: report.filed.length }) }}
|
||||
</span>
|
||||
<base-button size="small" @click="showFiledReports = !showFiledReports">
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
size="sm"
|
||||
@click="showFiledReports = !showFiledReports"
|
||||
>
|
||||
{{ $t('moderation.reports.moreDetails') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</td>
|
||||
|
||||
<!-- Content Column -->
|
||||
@ -22,7 +27,7 @@
|
||||
<user-teaser :user="report.resource" :showAvatar="false" :showPopover="false" />
|
||||
</client-only>
|
||||
<nuxt-link v-else class="title" :to="linkTarget">
|
||||
{{ linkText | truncate(50) }}
|
||||
{{ $filters.truncate(linkText, 50) }}
|
||||
</nuxt-link>
|
||||
</td>
|
||||
|
||||
@ -78,14 +83,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import FiledReportsTable from '~/components/features/FiledReportsTable/FiledReportsTable'
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OsButton,
|
||||
FiledReportsTable,
|
||||
UserTeaser,
|
||||
},
|
||||
emits: ['confirm-report'],
|
||||
props: {
|
||||
report: {
|
||||
type: Object,
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
verbose: true,
|
||||
collectCoverage: true,
|
||||
@ -21,14 +23,32 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
coverageProvider: 'v8',
|
||||
setupFiles: ['<rootDir>/test/registerContext.js', '<rootDir>/test/testSetup.js'],
|
||||
// IMPORTANT: vueDemiSetup must be FIRST to ensure vue-demi is loaded from webapp's node_modules
|
||||
setupFiles: [
|
||||
'<rootDir>/test/vueDemiSetup.js',
|
||||
'<rootDir>/test/registerContext.js',
|
||||
'<rootDir>/test/testSetup.js',
|
||||
],
|
||||
transform: {
|
||||
'.*\\.(vue)$': '@vue/vue2-jest',
|
||||
'^.+\\.js$': 'babel-jest',
|
||||
'^.+\\.mjs$': 'babel-jest',
|
||||
},
|
||||
// Transform ESM packages that Jest can't handle natively
|
||||
// Note: @ocelot-social/ui is NOT in the exception list because we load from dist/index.cjs
|
||||
// which is already CommonJS and doesn't need transformation
|
||||
transformIgnorePatterns: ['node_modules/(?!(vue-demi)/)', '<rootDir>/../packages/ui/'],
|
||||
testMatch: ['**/?(*.)+(spec|test).js?(x)'],
|
||||
modulePathIgnorePatterns: ['<rootDir>/dist/'],
|
||||
moduleNameMapper: {
|
||||
// IMPORTANT: vue-demi must be mapped BEFORE @ocelot-social/ui
|
||||
// This ensures that when the UI library's dist/index.cjs calls require("vue-demi"),
|
||||
// it gets webapp's Vue 2.7-configured vue-demi instead of UI library's Vue 3 one
|
||||
'^vue-demi$': path.resolve(__dirname, 'node_modules/vue-demi/lib/index.cjs'),
|
||||
// UI library - use mock that loads dist with correct vue-demi
|
||||
'^@ocelot-social/ui$': '<rootDir>/test/__mocks__/@ocelot-social/ui.js',
|
||||
'^@ocelot-social/ui/style.css$': 'identity-obj-proxy',
|
||||
// Other mappings
|
||||
'\\.(svg)$': '<rootDir>/test/fileMock.js',
|
||||
'\\.(scss|css|less)$': 'identity-obj-proxy',
|
||||
'@mapbox/mapbox-gl-geocoder': 'identity-obj-proxy',
|
||||
@ -38,7 +58,7 @@ module.exports = {
|
||||
'^@@/': '<rootDir>/../styleguide/dist/system.umd.min.js',
|
||||
'^~/(.*)$': '<rootDir>/$1',
|
||||
},
|
||||
moduleFileExtensions: ['js', 'json', 'vue'],
|
||||
moduleFileExtensions: ['js', 'mjs', 'json', 'vue'],
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
snapshotSerializers: ['jest-serializer-vue'],
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import manifest from './constants/manifest.js'
|
||||
import metadata from './constants/metadata.js'
|
||||
|
||||
@ -94,6 +95,9 @@ export default {
|
||||
'~assets/_new/styles/resets.scss',
|
||||
'~assets/styles/main.scss',
|
||||
'~assets/styles/imports/_branding.scss',
|
||||
// @ocelot-social/ui CSS variables (loaded before styleguide)
|
||||
'~assets/_new/styles/ocelot-ui-variables.scss',
|
||||
// Note: @ocelot-social/ui/style.css is loaded via plugin after styleguide
|
||||
],
|
||||
|
||||
/*
|
||||
@ -247,6 +251,9 @@ export default {
|
||||
** Build configuration
|
||||
*/
|
||||
build: {
|
||||
// Transpile ESM modules for SSR compatibility
|
||||
// vue-demi and @ocelot-social/ui must be transpiled to ensure isVue2 is consistent
|
||||
transpile: ['vue-demi', '@ocelot-social/ui'],
|
||||
// Invalidate cache between versions
|
||||
// https://www.reddit.com/r/Nuxt/comments/18i8hp2/comment/kdc1wa3/
|
||||
// https://v2.nuxt.com/docs/configuration-glossary/configuration-build/#filenames
|
||||
@ -281,6 +288,15 @@ export default {
|
||||
config.resolve.alias['@@'] = path.resolve(__dirname, `${styleguidePath}/dist`)
|
||||
// Vue 2.7 has built-in Composition API - redirect old imports
|
||||
config.resolve.alias['@vue/composition-api'] = 'vue'
|
||||
// Ensure vue-demi uses webapp's Vue 2.7 (not UI library's Vue 3)
|
||||
config.resolve.alias['vue-demi'] = path.resolve(__dirname, 'node_modules/vue-demi')
|
||||
// UI library alias - point to dist folder
|
||||
// In Docker: /packages/ui, locally: ../packages/ui (via yarn link)
|
||||
const uiLibraryPath = fs.existsSync('/packages/ui/dist')
|
||||
? '/packages/ui/dist'
|
||||
: path.resolve(__dirname, '../packages/ui/dist')
|
||||
config.resolve.alias['@ocelot-social/ui$'] = path.join(uiLibraryPath, 'index.mjs')
|
||||
config.resolve.alias['@ocelot-social/ui/style.css$'] = path.join(uiLibraryPath, 'style.css')
|
||||
config.module.rules.push({
|
||||
resourceQuery: /blockType=docs/,
|
||||
loader: require.resolve(`${styleguidePath}/src/loader/docs-trim-loader.js`),
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
"postinstall": "node scripts/fix-vue2-jest.js && node scripts/fix-v-mapbox.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ocelot-social/ui": "file:../packages/ui",
|
||||
"@mapbox/mapbox-gl-geocoder": "^5.0.2",
|
||||
"@nuxtjs/apollo": "^4.0.0-rc19",
|
||||
"@nuxtjs/axios": "~5.9.7",
|
||||
@ -55,6 +56,7 @@
|
||||
"vue": "~2.7.16",
|
||||
"vue-advanced-chat": "^2.1.2",
|
||||
"vue-count-to": "~1.0.13",
|
||||
"vue-demi": "^0.14.10",
|
||||
"vue-infinite-loading": "^2.4.5",
|
||||
"vue-izitoast": "^1.2.1",
|
||||
"vue-observe-visibility": "^1.0.0",
|
||||
|
||||
@ -33,17 +33,19 @@
|
||||
:disabled="!showDonations"
|
||||
data-test="donations-progress"
|
||||
/>
|
||||
<base-button class="donations-info-button" filled type="submit">
|
||||
<os-button class="donations-info-button" variant="primary" type="submit">
|
||||
{{ $t('actions.save') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</ds-form>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { DonationsQuery, UpdateDonations } from '~/graphql/Donations'
|
||||
|
||||
export default {
|
||||
components: { OsButton },
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
|
||||
@ -58,7 +58,7 @@ export default {
|
||||
},
|
||||
})
|
||||
this.$toast.success(this.$t('group.groupCreated'))
|
||||
this.$router.history.push({
|
||||
this.$router.push({
|
||||
name: 'groups-id-slug',
|
||||
params: { id: responseId, slug: responseSlug },
|
||||
})
|
||||
|
||||
@ -59,7 +59,7 @@ export default {
|
||||
},
|
||||
})
|
||||
this.$toast.success(this.$t('group.updatedGroup'))
|
||||
this.$router.history.push({
|
||||
this.$router.push({
|
||||
name: 'groups-id-slug',
|
||||
params: { id: responseId, slug: responseSlug },
|
||||
})
|
||||
|
||||
@ -12,14 +12,15 @@
|
||||
</ds-flex>
|
||||
<ds-space />
|
||||
<ds-flex-item class="notifications-header-button" :width="{ base: 'auto' }" centered>
|
||||
<base-button
|
||||
primary
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
:disabled="unreadNotificationsCount === 0"
|
||||
@click="markAllAsRead"
|
||||
data-test="markAllAsRead-button"
|
||||
@click="markAllAsRead"
|
||||
>
|
||||
{{ $t('notifications.markAllAsRead') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</ds-flex-item>
|
||||
<ds-space />
|
||||
<notifications-table
|
||||
@ -41,6 +42,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import NotificationsTable from '~/components/NotificationsTable/NotificationsTable'
|
||||
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'
|
||||
import PaginationButtons from '~/components/_new/generic/PaginationButtons/PaginationButtons'
|
||||
@ -48,6 +50,7 @@ import { notificationQuery, markAsReadMutation, markAllAsReadMutation } from '~/
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OsButton,
|
||||
DropdownFilter,
|
||||
NotificationsTable,
|
||||
PaginationButtons,
|
||||
@ -57,7 +60,7 @@ export default {
|
||||
return {
|
||||
offset: 0,
|
||||
notifications: [],
|
||||
nofiticationRead: null,
|
||||
notificationRead: null,
|
||||
pageSize,
|
||||
first: pageSize,
|
||||
hasNext: false,
|
||||
|
||||
@ -65,9 +65,7 @@ describe('PostSlug', () => {
|
||||
},
|
||||
// If you are mocking the router, then don't use VueRouter with localVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html
|
||||
$router: {
|
||||
history: {
|
||||
push: jest.fn(),
|
||||
},
|
||||
push: jest.fn(),
|
||||
},
|
||||
$toast: {
|
||||
success: jest.fn(),
|
||||
@ -142,7 +140,7 @@ describe('PostSlug', () => {
|
||||
})
|
||||
|
||||
it('does go to index (main) page', () => {
|
||||
expect(mocks.$router.history.push).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.$router.push).toHaveBeenCalledWith('/')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -329,7 +329,7 @@ export default {
|
||||
try {
|
||||
await this.$apollo.mutate(deletePostMutation(this.post.id))
|
||||
this.$toast.success(this.$t('delete.contribution.success'))
|
||||
this.$router.history.push('/') // Redirect to index (main) page
|
||||
this.$router.push('/') // Redirect to index (main) page
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
}
|
||||
|
||||
@ -71,12 +71,24 @@
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
<div v-if="!myProfile" class="action-buttons">
|
||||
<base-button v-if="user.isBlocked" @click="unblockUser(user)">
|
||||
<os-button
|
||||
v-if="user.isBlocked"
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
full-width
|
||||
@click="unblockUser(user)"
|
||||
>
|
||||
{{ $t('settings.blocked-users.unblock') }}
|
||||
</base-button>
|
||||
<base-button v-if="user.isMuted" @click="unmuteUser(user)">
|
||||
</os-button>
|
||||
<os-button
|
||||
v-if="user.isMuted"
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
full-width
|
||||
@click="unmuteUser(user)"
|
||||
>
|
||||
{{ $t('settings.muted-users.unmute') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
<hc-follow-button
|
||||
v-if="!user.isMuted && !user.isBlocked"
|
||||
:follow-id="user.id"
|
||||
@ -192,6 +204,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import uniqBy from 'lodash/uniqBy'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import postListActions from '~/mixins/postListActions'
|
||||
@ -225,6 +238,7 @@ const tabToFilterMapping = ({ tab, id }) => {
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OsButton,
|
||||
SocialMedia,
|
||||
PostTeaser,
|
||||
HcFollowButton,
|
||||
@ -363,7 +377,7 @@ export default {
|
||||
},
|
||||
async unmuteUser(user) {
|
||||
try {
|
||||
this.$apollo.mutate({ mutation: unmuteUser(), variables: { id: user.id } })
|
||||
await this.$apollo.mutate({ mutation: unmuteUser(), variables: { id: user.id } })
|
||||
} catch (error) {
|
||||
this.$toast.error(error.message)
|
||||
} finally {
|
||||
@ -383,7 +397,7 @@ export default {
|
||||
},
|
||||
async unblockUser(user) {
|
||||
try {
|
||||
this.$apollo.mutate({ mutation: unblockUser(), variables: { id: user.id } })
|
||||
await this.$apollo.mutate({ mutation: unblockUser(), variables: { id: user.id } })
|
||||
} catch (error) {
|
||||
this.$toast.error(error.message)
|
||||
} finally {
|
||||
@ -489,10 +503,12 @@ export default {
|
||||
.action-buttons {
|
||||
margin: $space-small 0;
|
||||
|
||||
> button {
|
||||
margin-bottom: $space-x-small;
|
||||
}
|
||||
> .base-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: $space-x-small;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -35,17 +35,11 @@ exports[`notifications.vue mount renders 1`] = `
|
||||
</label></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ds-space" style="margin-top: 24px; margin-bottom: 8px;"><button type="button" class="base-button">
|
||||
<!---->
|
||||
<!---->
|
||||
<div class="ds-space" style="margin-top: 24px; margin-bottom: 8px;"><button type="button" data-appearance="outline" 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] px-[16px] py-0 text-[15px] leading-[normal] rounded-[5px] 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)]">
|
||||
settings.notifications.checkAll
|
||||
</button> <button type="button" class="base-button">
|
||||
<!---->
|
||||
<!---->
|
||||
</button> <button type="button" data-appearance="outline" 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] px-[16px] py-0 text-[15px] leading-[normal] rounded-[5px] 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)]">
|
||||
settings.notifications.uncheckAll
|
||||
</button> <button disabled="disabled" type="button" class="save-button base-button --filled">
|
||||
<!---->
|
||||
<!---->
|
||||
</button> <button type="button" disabled="disabled" data-appearance="filled" class="save-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] px-[16px] py-0 text-[15px] leading-[normal] rounded-[5px] 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)] save-button">
|
||||
actions.save
|
||||
</button></div>
|
||||
<!---->
|
||||
|
||||
@ -20,18 +20,23 @@
|
||||
<strong>
|
||||
{{
|
||||
selectedBadgeIndex === null
|
||||
? this.$t('settings.badges.click-to-select')
|
||||
? $t('settings.badges.click-to-select')
|
||||
: isEmptySlotSelected
|
||||
? this.$t('settings.badges.click-to-use')
|
||||
? $t('settings.badges.click-to-use')
|
||||
: ''
|
||||
}}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedBadgeIndex !== null && !isEmptySlotSelected" class="badge-actions">
|
||||
<base-button @click="removeBadgeFromSlot" class="remove-button">
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
class="remove-button"
|
||||
@click="removeBadgeFromSlot"
|
||||
>
|
||||
{{ $t('settings.badges.remove') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@ -49,6 +54,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { setTrophyBadgeSelected } from '~/graphql/User'
|
||||
import scrollToContent from './scroll-to-content.js'
|
||||
@ -56,7 +62,7 @@ import Badges from '../../components/Badges.vue'
|
||||
import BadgeSelection from '../../components/BadgeSelection.vue'
|
||||
|
||||
export default {
|
||||
components: { BadgeSelection, Badges },
|
||||
components: { OsButton, BadgeSelection, Badges },
|
||||
mixins: [scrollToContent],
|
||||
data() {
|
||||
return {
|
||||
|
||||
@ -18,12 +18,22 @@
|
||||
{{ $t('settings.embeds.status.change.question') }}
|
||||
</ds-text>
|
||||
<ds-space margin-top="small" margin-bottom="base">
|
||||
<base-button @click="submit" :filled="!disabled" :disabled="!disabled">
|
||||
<os-button
|
||||
@click="submit"
|
||||
:appearance="!disabled ? 'filled' : 'outline'"
|
||||
variant="primary"
|
||||
:disabled="!disabled"
|
||||
>
|
||||
{{ $t('settings.embeds.status.change.deny') }}
|
||||
</base-button>
|
||||
<base-button @click="submit" :filled="disabled" :disabled="disabled">
|
||||
</os-button>
|
||||
<os-button
|
||||
@click="submit"
|
||||
:appearance="disabled ? 'filled' : 'outline'"
|
||||
variant="primary"
|
||||
:disabled="disabled"
|
||||
>
|
||||
{{ $t('settings.embeds.status.change.allow') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</ds-space>
|
||||
<h3>{{ $t('settings.embeds.info-description') }}</h3>
|
||||
<ds-space margin="small">
|
||||
@ -45,12 +55,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import axios from 'axios'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { updateUserMutation } from '~/graphql/User.js'
|
||||
import scrollToContent from './scroll-to-content.js'
|
||||
|
||||
export default {
|
||||
components: { OsButton },
|
||||
mixins: [scrollToContent],
|
||||
head() {
|
||||
return {
|
||||
|
||||
@ -20,25 +20,37 @@
|
||||
</div>
|
||||
</ds-space>
|
||||
<ds-space margin-top="base" margin-bottom="x-small">
|
||||
<base-button @click="checkAll" :disabled="isCheckAllDisabled">
|
||||
<os-button
|
||||
appearance="outline"
|
||||
variant="primary"
|
||||
@click="checkAll"
|
||||
:disabled="isCheckAllDisabled"
|
||||
>
|
||||
{{ $t('settings.notifications.checkAll') }}
|
||||
</base-button>
|
||||
<base-button @click="uncheckAll" :disabled="isUncheckAllDisabled">
|
||||
</os-button>
|
||||
<os-button
|
||||
appearance="outline"
|
||||
variant="primary"
|
||||
@click="uncheckAll"
|
||||
:disabled="isUncheckAllDisabled"
|
||||
>
|
||||
{{ $t('settings.notifications.uncheckAll') }}
|
||||
</base-button>
|
||||
<base-button class="save-button" filled @click="submit" :disabled="isSubmitDisabled">
|
||||
</os-button>
|
||||
<os-button class="save-button" variant="primary" @click="submit" :disabled="isSubmitDisabled">
|
||||
{{ $t('actions.save') }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</ds-space>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { updateUserMutation } from '~/graphql/User'
|
||||
import scrollToContent from './scroll-to-content.js'
|
||||
|
||||
export default {
|
||||
components: { OsButton },
|
||||
mixins: [scrollToContent],
|
||||
data() {
|
||||
return {
|
||||
|
||||
@ -52,14 +52,14 @@ describe('privacy.vue', () => {
|
||||
|
||||
it('clicking on submit changes shoutsAllowed to false', async () => {
|
||||
await wrapper.find('#allow-shouts').setChecked(false)
|
||||
await wrapper.find('.base-button').trigger('click')
|
||||
await wrapper.find('button').trigger('click')
|
||||
expect(wrapper.vm.shoutsAllowed).toBe(false)
|
||||
})
|
||||
|
||||
it('clicking on submit with a server error shows a toast and shoutsAllowed is still true', async () => {
|
||||
mocks.$apollo.mutate = jest.fn().mockRejectedValue({ message: 'Ouch!' })
|
||||
await wrapper.find('#allow-shouts').setChecked(false)
|
||||
await wrapper.find('.base-button').trigger('click')
|
||||
await wrapper.find('button').trigger('click')
|
||||
expect(mocks.$toast.error).toHaveBeenCalledWith('Ouch!')
|
||||
expect(wrapper.vm.shoutsAllowed).toBe(true)
|
||||
})
|
||||
|
||||
@ -5,16 +5,20 @@
|
||||
<input id="allow-shouts" type="checkbox" v-model="shoutsAllowed" />
|
||||
<label for="allow-shouts">{{ $t('settings.privacy.make-shouts-public') }}</label>
|
||||
</ds-space>
|
||||
<base-button filled @click="submit" :disabled="disabled">{{ $t('actions.save') }}</base-button>
|
||||
<os-button variant="primary" @click="submit" :disabled="disabled">
|
||||
{{ $t('actions.save') }}
|
||||
</os-button>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { updateUserMutation } from '~/graphql/User'
|
||||
import scrollToContent from './scroll-to-content.js'
|
||||
|
||||
export default {
|
||||
components: { OsButton },
|
||||
mixins: [scrollToContent],
|
||||
data() {
|
||||
return {
|
||||
|
||||
@ -4,28 +4,30 @@
|
||||
<base-icon name="balance-scale" />
|
||||
<h2 class="title">{{ $t(`termsAndConditions.newTermsAndConditions`) }}</h2>
|
||||
<nuxt-link :to="{ name: 'terms-and-conditions' }" target="_blank">
|
||||
<base-button>
|
||||
<os-button appearance="outline" variant="primary">
|
||||
{{ $t(`termsAndConditions.termsAndConditionsNewConfirmText`) }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</nuxt-link>
|
||||
<label for="checkbox">
|
||||
<input id="checkbox" type="checkbox" v-model="checked" :checked="checked" />
|
||||
{{ $t('termsAndConditions.termsAndConditionsNewConfirm') }}
|
||||
</label>
|
||||
<base-button filled @click="submit" :disabled="!checked">
|
||||
<os-button variant="primary" @click="submit" :disabled="!checked">
|
||||
{{ $t(`actions.save`) }}
|
||||
</base-button>
|
||||
</os-button>
|
||||
</base-card>
|
||||
</ds-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import { VERSION } from '~/constants/terms-and-conditions-version.js'
|
||||
import { updateUserMutation } from '~/graphql/User.js'
|
||||
|
||||
export default {
|
||||
name: 'TermsAndConditionsConfirm',
|
||||
components: { OsButton },
|
||||
layout: 'default',
|
||||
head() {
|
||||
return {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import Vue from 'vue'
|
||||
import Styleguide from '@@/system.umd.min.js'
|
||||
import '@@/system.css'
|
||||
// Load UI library CSS after styleguide to ensure correct specificity
|
||||
import '@ocelot-social/ui/style.css'
|
||||
|
||||
Vue.use(Styleguide)
|
||||
|
||||
83
webapp/test/__mocks__/@ocelot-social/ui.js
Normal file
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Jest mock for @ocelot-social/ui
|
||||
*
|
||||
* This mock ensures vue-demi is loaded from webapp's node_modules (Vue 2.7)
|
||||
* BEFORE loading the UI library's dist file.
|
||||
*
|
||||
* Without this, Jest would load the UI library through the symlink, which
|
||||
* would use the UI library's vue-demi (configured for Vue 3).
|
||||
*/
|
||||
const path = require('path')
|
||||
const Module = require('module')
|
||||
|
||||
// Load vue-demi from webapp's node_modules
|
||||
// __dirname is test/__mocks__/@ocelot-social, so go up 3 levels to webapp
|
||||
const vueDemiPath = path.resolve(__dirname, '../../../node_modules/vue-demi/lib/index.cjs')
|
||||
const vueDemi = require(vueDemiPath)
|
||||
|
||||
// Verify vue-demi is correctly configured for Vue 2
|
||||
if (!vueDemi.isVue2) {
|
||||
throw new Error('vue-demi is not configured for Vue 2! isVue2=' + vueDemi.isVue2)
|
||||
}
|
||||
|
||||
// Patch missing Composition API functions from Vue.default
|
||||
// This is needed because Jest loads vue.runtime.common.js which exports under default
|
||||
const Vue = require('vue')
|
||||
const VueApi = Vue.default || Vue
|
||||
|
||||
// List of Composition API functions that vue-demi should export
|
||||
const compositionApiFns = [
|
||||
'defineComponent',
|
||||
'computed',
|
||||
'ref',
|
||||
'reactive',
|
||||
'watch',
|
||||
'watchEffect',
|
||||
'onMounted',
|
||||
'onUnmounted',
|
||||
'onBeforeMount',
|
||||
'onBeforeUnmount',
|
||||
'provide',
|
||||
'inject',
|
||||
'toRef',
|
||||
'toRefs',
|
||||
'unref',
|
||||
'isRef',
|
||||
'shallowRef',
|
||||
'triggerRef',
|
||||
'customRef',
|
||||
'shallowReactive',
|
||||
'shallowReadonly',
|
||||
'readonly',
|
||||
'toRaw',
|
||||
'markRaw',
|
||||
'effectScope',
|
||||
'getCurrentScope',
|
||||
'onScopeDispose',
|
||||
'getCurrentInstance',
|
||||
'h',
|
||||
'nextTick',
|
||||
]
|
||||
|
||||
// Patch any missing functions
|
||||
for (const fn of compositionApiFns) {
|
||||
if (!vueDemi[fn] && VueApi[fn]) {
|
||||
vueDemi[fn] = VueApi[fn]
|
||||
}
|
||||
}
|
||||
|
||||
// Patch Module._load to return the correct vue-demi when the UI library loads it
|
||||
const originalLoad = Module._load
|
||||
Module._load = function (request, parent, isMain) {
|
||||
if (request === 'vue-demi') {
|
||||
return vueDemi
|
||||
}
|
||||
return originalLoad.apply(this, arguments)
|
||||
}
|
||||
|
||||
// Load the UI library dist
|
||||
const uiDistPath = path.resolve(__dirname, '../../../node_modules/@ocelot-social/ui/dist/index.cjs')
|
||||
const ui = require(uiDistPath)
|
||||
|
||||
// Export everything from the UI library
|
||||
module.exports = ui
|
||||
25
webapp/test/vueDemiSetup.js
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* This setup file ensures vue-demi is loaded correctly for Vue 2.7.
|
||||
*
|
||||
* The @ocelot-social/ui package is handled by the mock in test/__mocks__/@ocelot-social/ui.js
|
||||
*/
|
||||
const path = require('path')
|
||||
const Module = require('module')
|
||||
|
||||
// Path to webapp's vue-demi (configured for Vue 2.7)
|
||||
const vueDemiPath = path.resolve(__dirname, '../node_modules/vue-demi/lib/index.cjs')
|
||||
|
||||
// Patch Module._resolveFilename to intercept vue-demi requires
|
||||
const originalResolveFilename = Module._resolveFilename
|
||||
Module._resolveFilename = function (request, parent, isMain, options) {
|
||||
if (request === 'vue-demi') {
|
||||
return vueDemiPath
|
||||
}
|
||||
return originalResolveFilename.apply(this, arguments)
|
||||
}
|
||||
|
||||
// Pre-load vue-demi to ensure correct version is cached
|
||||
const vueDemi = require(vueDemiPath)
|
||||
if (!vueDemi.isVue2) {
|
||||
throw new Error('vue-demi setup failed: isVue2 should be true')
|
||||
}
|
||||
@ -3835,6 +3835,14 @@
|
||||
mustache "^2.3.0"
|
||||
stack-trace "0.0.10"
|
||||
|
||||
"@ocelot-social/ui@file:../packages/ui":
|
||||
version "0.0.1"
|
||||
dependencies:
|
||||
class-variance-authority "^0.7.1"
|
||||
clsx "^2.1.1"
|
||||
tailwind-merge "^3.3.0"
|
||||
vue-demi "^0.14.10"
|
||||
|
||||
"@oclif/color@^0.0.0":
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@oclif/color/-/color-0.0.0.tgz#54939bbd16d1387511bf1a48ccda1a417248e6a9"
|
||||
@ -7965,6 +7973,13 @@ class-utils@^0.3.5:
|
||||
isobject "^3.0.0"
|
||||
static-extend "^0.1.1"
|
||||
|
||||
class-variance-authority@^0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz#4008a798a0e4553a781a57ac5177c9fb5d043787"
|
||||
integrity sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==
|
||||
dependencies:
|
||||
clsx "^2.1.1"
|
||||
|
||||
clean-css@^4.2.3:
|
||||
version "4.2.4"
|
||||
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178"
|
||||
@ -8117,6 +8132,11 @@ clone-response@^1.0.2:
|
||||
dependencies:
|
||||
mimic-response "^1.0.0"
|
||||
|
||||
clsx@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
||||
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
||||
|
||||
co@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
@ -20000,6 +20020,11 @@ table@^6.0.9:
|
||||
string-width "^4.2.3"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
tailwind-merge@^3.3.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.4.0.tgz#5a264e131a096879965f1175d11f8c36e6b64eca"
|
||||
integrity sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==
|
||||
|
||||
tapable@^1.0.0, tapable@^1.0.0-beta.5, tapable@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
|
||||
@ -21249,6 +21274,11 @@ vue-count-to@~1.0.13:
|
||||
resolved "https://registry.yarnpkg.com/vue-count-to/-/vue-count-to-1.0.13.tgz#3e7573ea6e64c2b2972f64e0a2ab2e23c7590ff3"
|
||||
integrity sha512-6R4OVBVNtQTlcbXu6SJ8ENR35M2/CdWt3Jmv57jOUM+1ojiFmjVGvZPH8DfHpMDSA+ITs+EW5V6qthADxeyYOQ==
|
||||
|
||||
vue-demi@^0.14.10:
|
||||
version "0.14.10"
|
||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
|
||||
integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
|
||||
|
||||
vue-eslint-parser@^9.4.3:
|
||||
version "9.4.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz#9b04b22c71401f1e8bca9be7c3e3416a4bde76a8"
|
||||
|
||||