mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2026-03-01 12:44:37 +00:00
feat(package/ui): os-number (#9254)
This commit is contained in:
parent
518ed8af89
commit
bbad57bbc7
@ -52,4 +52,7 @@
|
||||
/* Disabled (Payne's Grey - muted watercolor grey) */
|
||||
--color-disabled: #c4bdb5;
|
||||
--color-disabled-contrast: #5a4f45;
|
||||
|
||||
/* Text */
|
||||
--color-text-soft: #6b5e7b;
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ Phase 0: Analyse ██████████ 100% (8/8 Schritte) ✅
|
||||
Phase 3: OsButton ██████████ 100% (133/133 Buttons) ✅
|
||||
Phase 4: Tier 1 ██████████ 100% (OsButton, OsIcon, OsSpinner, OsCard) ✅
|
||||
Phase 4: Tier A → HTML ██████████ 100% (10 ds-* Wrapper → Plain HTML) ✅
|
||||
Phase 4: Tier B ██████░░░░ 60% (ds-chip→OsBadge✅, ds-tag→OsBadge✅, ds-grid✅, ds-number⬜, ds-radio⬜)
|
||||
Phase 4: Tier B ████████░░ 80% (ds-chip→OsBadge✅, ds-tag→OsBadge✅, ds-grid✅, ds-number→OsNumber✅, ds-radio⬜)
|
||||
Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅) | Tier 2-3 ausstehend (OsModal, OsInput, OsMenu, OsSelect)
|
||||
```
|
||||
|
||||
@ -23,10 +23,11 @@ Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅)
|
||||
| Webapp Komponenten | 139 |
|
||||
| Styleguide Komponenten | 38 (23 in Webapp genutzt) |
|
||||
| **Gesamt** | **177** |
|
||||
| ✅ UI-Library | OsButton, OsIcon, OsSpinner, OsCard, OsBadge (5) |
|
||||
| ✅ UI-Library | OsButton, OsIcon, OsSpinner, OsCard, OsBadge, OsNumber (6) |
|
||||
| ✅ → Plain HTML | Section, Placeholder, List, ListItem, Container, Heading, Text, Space, Flex, FlexItem, Grid, GridItem, Table (13) |
|
||||
| ✅ → OsBadge | Chip (20 Nutzungen → OsBadge), Tag (3 → OsBadge shape="square") |
|
||||
| ⬜ → Plain HTML | Number, Radio (2) — Tier B Rest |
|
||||
| ✅ → OsNumber | Number (5 Nutzungen → OsNumber, CountTo.vue gelöscht, vue-count-to entfernt) |
|
||||
| ⬜ → Plain HTML | Radio (1) — Tier B Rest |
|
||||
| ⬜ → UI-Library | Modal, Input, Menu, MenuItem, Select (5) — Tier 2-3 |
|
||||
| ⬜ Offen | Form (18 Dateien — HTML oder OsForm?) |
|
||||
| ⬜ Nicht in Webapp | Code, CopyField, FormItem, InputError, InputLabel, Page, PageTitle, Logo, Avatar, TableCol, TableHeadCol (11) |
|
||||
@ -57,7 +58,7 @@ Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅)
|
||||
| 3 | Chip | ✅ UI-Library | → OsBadge (20 Nutzungen in 5 Dateien) |
|
||||
| 4 | Code | ⬜ Nicht genutzt | Nicht in Webapp verwendet |
|
||||
| 5 | Icon | ✅ UI-Library | → OsIcon (BaseIcon gelöscht, 82 Ocelot-Icons) |
|
||||
| 6 | Number | ⬜ Tier B | 5 Dateien → Plain HTML `<div class="ds-number">` |
|
||||
| 6 | Number | ✅ UI-Library | → OsNumber (5 Dateien, CountTo.vue gelöscht, vue-count-to entfernt) |
|
||||
| 7 | Placeholder | ✅ → HTML | Tier A: `<div class="ds-placeholder">` |
|
||||
| 8 | Spinner | ✅ UI-Library | → OsSpinner (LoadingSpinner gelöscht) |
|
||||
| 9 | Table | ✅ → HTML | 7 Dateien → Plain HTML `<table>` + CSS-Klassen (kein OsTable nötig) |
|
||||
@ -157,7 +158,7 @@ Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅)
|
||||
| 27 | ContextMenu | ⬜ Ausstehend | Navigation | Menu | |
|
||||
| 28 | ContributionForm | ✅ Migriert | Feature | Form | Buttons → OsButton, ds-* → HTML |
|
||||
| 29 | CounterIcon | ⬜ Ausstehend | Display | Icon | |
|
||||
| 30 | CountTo | ⬜ Ausstehend | Display | Number | Animation |
|
||||
| 30 | ~~CountTo~~ | ✅ Gelöscht | Display | Number | → OsNumber (Animation eingebaut, vue-count-to entfernt) |
|
||||
| 31 | CreateInvitation | ⬜ Ausstehend | Feature | | |
|
||||
| 32 | CtaJoinLeaveGroup | ✅ Migriert | Button | Button | 🔄 Button-Familie, nutzt OsButton |
|
||||
| 33 | CtaUnblockAuthor | ✅ Migriert | Button | Button | Nutzt OsButton (icon, as="nuxt-link") |
|
||||
|
||||
@ -81,10 +81,10 @@ Phase 0: ██████████ 100% (6/6 Aufgaben) ✅
|
||||
Phase 1: ██████████ 100% (6/6 Aufgaben) ✅
|
||||
Phase 2: ██████████ 100% (26/26 Aufgaben) ✅
|
||||
Phase 3: ██████████ 100% (24/24 Aufgaben) ✅ - Webapp-Integration komplett
|
||||
Phase 4: ██████░░░░ 59% (16/27 Aufgaben) - Tier 1 ✅, Tier A ✅, Infra ✅, OsBadge ✅, ds-grid ✅, ds-table→HTML ✅ | Tier B (rest), Tier 2-3 ausstehend
|
||||
Phase 4: ██████░░░░ 63% (17/27 Aufgaben) - Tier 1 ✅, Tier A ✅, Infra ✅, OsBadge ✅, ds-grid ✅, ds-table→HTML ✅, OsNumber ✅ | Tier B (rest), Tier 2-3 ausstehend
|
||||
Phase 5: ░░░░░░░░░░ 0% (0/7 Aufgaben)
|
||||
───────────────────────────────────────
|
||||
Gesamt: ████████░░ 81% (78/96 Aufgaben)
|
||||
Gesamt: ████████░░ 82% (79/96 Aufgaben)
|
||||
```
|
||||
|
||||
### Katalogisierung (Details in KATALOG.md)
|
||||
@ -186,9 +186,20 @@ Tier A ds-* → Plain HTML + CSS: ✅
|
||||
├─ ds-flex/ds-flex-item: JavaScript window.innerWidth → CSS @media Queries
|
||||
│ (kein Layout-Shift bei SSR, bessere Performance)
|
||||
├─ system.css bleibt geladen — bestehende CSS-Klassen funktionieren weiter
|
||||
├─ Verbleibend: 9 ds-* Komponenten (Tier B Rest: 2 einfache, Tier C: 6 komplexe → UI-Library)
|
||||
├─ Verbleibend: 8 ds-* Komponenten (Tier B Rest: 1 einfache, Tier C: 6 komplexe → UI-Library)
|
||||
└─ 0 Tier-A ds-* Komponenten-Tags verbleibend
|
||||
|
||||
ds-number → OsNumber (UI-Library): ✅
|
||||
├─ OsNumber Komponente: h() Render-Function, requestAnimationFrame Animation, inheritAttrs: false
|
||||
├─ Props: count (required), label (optional), animated (optional)
|
||||
├─ Animation: 1500ms ease-out, watch(count) re-animiert, SSR-safe (onMounted)
|
||||
├─ Styling: tabular-nums + min-width für stabile Breite, --color-text-soft Label-Farbe
|
||||
├─ ds-number + CountTo: 5 Dateien → <os-number> (UserTeaserPopover, TabNavigation, admin, profile, groups)
|
||||
├─ vue-count-to Dependency entfernt, CountTo.vue gelöscht
|
||||
├─ CSS-Variable: --color-text-soft in requiredCssVariables + ocelot-ui-variables.scss
|
||||
├─ 11 Unit-Tests, 5 Stories, 5 Visual Tests + 1 Keyboard Test
|
||||
└─ 0 ds-number/CountTo Nutzungen verbleibend
|
||||
|
||||
ds-chip + ds-tag → OsBadge (UI-Library): ✅
|
||||
├─ OsBadge Komponente: CVA-Varianten, h() Render-Function, inheritAttrs: false
|
||||
├─ Props: variant (default/primary/danger), size (sm/md/lg), shape (pill/square)
|
||||
@ -205,20 +216,27 @@ ds-chip + ds-tag → OsBadge (UI-Library): ✅
|
||||
|
||||
## Aktueller Stand
|
||||
|
||||
**Letzte Aktualisierung:** 2026-02-20 (Session 31)
|
||||
**Letzte Aktualisierung:** 2026-02-20 (Session 32)
|
||||
|
||||
**Aktuelle Phase:** Phase 4 - Tier 1 ✅, Tier A ✅, OsBadge ✅, ds-grid ✅, ds-table→HTML ✅ | Tier B (rest), Tier 2-3 ausstehend
|
||||
**Aktuelle Phase:** Phase 4 - Tier 1 ✅, Tier A ✅, OsBadge ✅, ds-grid ✅, ds-table→HTML ✅, OsNumber ✅ | Tier B (rest), Tier 2-3 ausstehend
|
||||
|
||||
**Zuletzt abgeschlossen (Session 31 - ds-table → Plain HTML):**
|
||||
**Zuletzt abgeschlossen (Session 32 - OsNumber: ds-number + CountTo → OsNumber):**
|
||||
- [x] OsNumber Komponente in packages/ui erstellt (h() Render-Function, requestAnimationFrame Animation)
|
||||
- [x] Props: count (required), label (optional), animated (optional, 1500ms ease-out)
|
||||
- [x] Animation: requestAnimationFrame-Loop, watch(count) re-animiert von oldVal→newVal
|
||||
- [x] Stabile Breite: `tabular-nums` + `min-width: Nch` basierend auf Zielwert-Ziffernanzahl
|
||||
- [x] CSS-Variable `--color-text-soft` in tailwind.preset.ts (requiredCssVariables), Storybook-Theme, ocelot-ui-variables.scss
|
||||
- [x] 5 Webapp-Dateien migriert: UserTeaserPopover (statisch), TabNavigation (animated), admin/index (animated), profile/_slug (animated), groups/_slug (animated)
|
||||
- [x] CountTo.vue gelöscht, `vue-count-to` Dependency aus package.json entfernt
|
||||
- [x] `followedByCountStartValue` / `membersCountStartValue` Pattern entfernt (OsNumber watch-basiert)
|
||||
- [x] ds-number CSS aus `_ds-compat.scss` entfernt
|
||||
- [x] Admin-Dashboard: `.os-number-label { text-transform: uppercase }` per CSS (kein neuer Prop)
|
||||
- [x] Test-Fixes: Fehlende Count-Properties in Mock-Daten (followedByCount, contributionsCount, membersCount etc.)
|
||||
- [x] 11 Unit-Tests, 5 Stories, 5 Visual + A11y Tests
|
||||
|
||||
**Zuvor abgeschlossen (Session 31 - ds-table → Plain HTML):**
|
||||
- [x] ds-table (7 Nutzungen) → native `<table>` + CSS-Klassen (kein OsTable nötig)
|
||||
- [x] Table-CSS in `_ds-compat.scss`: .ds-table-wrap, .ds-table, .ds-table-col, .ds-table-head-col, .ds-table-bordered, .ds-table-condensed, Alignment-Klassen
|
||||
- [x] `pages/admin/hashtags.vue`: 4 Spalten (index, id-Link, taggedCountUnique, taggedCount)
|
||||
- [x] `pages/admin/categories.vue`: 3 Spalten (icon, name, postCount)
|
||||
- [x] `pages/admin/users/index.vue`: 9-10 Spalten (conditional badges), komplexeste Tabelle
|
||||
- [x] `pages/settings/blocked-users.vue`: 4 Spalten, unblockUser() auf direktes User-Objekt umgestellt
|
||||
- [x] `pages/settings/muted-users.vue`: 4 Spalten, unmuteUser() auf direktes User-Objekt umgestellt
|
||||
- [x] `components/Group/GroupMember.vue`: 5 Spalten (avatar, name, slug, roleInGroup, edit)
|
||||
- [x] `components/features/FiledReportsTable/FiledReportsTable.vue`: 4 Spalten mit colgroup widths
|
||||
- [x] `fields()` / `tableFields()` Computed Properties aus allen 7 Dateien entfernt (Labels direkt in `<th>`)
|
||||
- [x] Alle 16 Tests bestanden (3 Test-Suites: admin/users Snapshots aktualisiert, FiledReportsTable ✅, ReportsTable ✅)
|
||||
|
||||
@ -261,8 +279,8 @@ ds-chip + ds-tag → OsBadge (UI-Library): ✅
|
||||
- [x] Test-Fix: Empty.spec.js `attributes().margin` → `classes().toContain('ds-my-xxx-small')`
|
||||
- [x] 0 Tier-A `ds-*` Komponenten-Tags verbleibend
|
||||
|
||||
**Verbleibende ds-* Komponenten (8 Typen):**
|
||||
- Tier B Rest (→ Plain HTML): ds-number (5), ds-radio (1)
|
||||
**Verbleibende ds-* Komponenten (7 Typen):**
|
||||
- Tier B Rest (→ Plain HTML): ds-radio (1)
|
||||
- Tier C (→ UI-Library): ds-input (23), ds-form (18), ds-modal (7), ds-menu/ds-menu-item (17), ds-select (3)
|
||||
|
||||
**Zuvor abgeschlossen (Session 26 - CodeRabbit Review Fixes):**
|
||||
@ -385,11 +403,12 @@ ds-chip + ds-tag → OsBadge (UI-Library): ✅
|
||||
- [x] OsCard Komponente + BaseCard → OsCard Webapp-Migration ✅
|
||||
- [x] Tier A: 10 triviale ds-* Wrapper → Plain HTML + CSS ✅
|
||||
- [x] OsBadge Komponente + ds-chip/ds-tag → OsBadge Webapp-Migration ✅
|
||||
- [ ] Tier B (Rest): ds-number, ds-grid/ds-grid-item, ds-radio → Plain HTML
|
||||
- [x] OsNumber Komponente + ds-number/CountTo → OsNumber Webapp-Migration ✅
|
||||
- [ ] Tier B (Rest): ds-radio → Plain HTML
|
||||
- [ ] Weitere Tier 2 Komponenten (OsModal, OsDropdown, OsAvatar, OsInput)
|
||||
- [ ] ds-form + ds-input → OsForm + OsInput (stark gekoppelt, 18+23 Dateien)
|
||||
- [ ] ds-menu / ds-menu-item → OsMenu / OsMenuItem
|
||||
- [ ] ds-table → OsTable, ds-select → OsSelect
|
||||
- [ ] ds-select → OsSelect
|
||||
- [ ] Browser-Fehler untersuchen: `TypeError: Cannot read properties of undefined (reading 'heartO')` (ocelotIcons undefined im Browser trotz korrekter Webpack-Aliase)
|
||||
|
||||
**Manuelle Setup-Aufgaben (außerhalb Code):**
|
||||
@ -656,7 +675,7 @@ Jeder migrierte Button muss manuell geprüft werden: Normal, Hover, Focus, Activ
|
||||
**Tier B: Einfache ds-* → Plain HTML / UI-Library**
|
||||
- [x] ds-chip (5 Dateien) → OsBadge (UI-Library) ✅
|
||||
- [x] ds-tag (3 Dateien) → OsBadge shape="square" (UI-Library) ✅
|
||||
- [ ] ds-number (5 Dateien) → `<div class="ds-number">`
|
||||
- [x] ds-number (5 Dateien) → OsNumber (UI-Library) ✅ + CountTo.vue gelöscht, vue-count-to entfernt
|
||||
- [x] ds-grid / ds-grid-item (10 Dateien) → CSS Grid ✅
|
||||
- [ ] ds-radio (1 Datei) → native `<input type="radio">`
|
||||
|
||||
@ -1806,6 +1825,12 @@ Bei der Migration werden:
|
||||
| 2026-02-20 | **Table-CSS** | _ds-compat.scss erweitert: .ds-table-wrap, .ds-table, .ds-table-col, .ds-table-head-col, bordered, condensed, alignment |
|
||||
| 2026-02-20 | **fields() entfernt** | Computed Properties `fields()`/`tableFields()` aus 7 Dateien entfernt — Labels direkt in `<th>` |
|
||||
| 2026-02-20 | **Scope-Objekte entfernt** | `scope.row` Zugriffe → direkte Iteration-Variable (user, tag, member, report) |
|
||||
| 2026-02-20 | **OsNumber Komponente (Session 32)** | Neue Komponente: h() Render-Function, requestAnimationFrame Animation (1500ms ease-out), count (required), label, animated Props |
|
||||
| 2026-02-20 | **ds-number + CountTo → OsNumber** | 5 Dateien: UserTeaserPopover, TabNavigation, admin/index, profile/_slug, groups/_slug |
|
||||
| 2026-02-20 | **Animation-Stabilität** | `tabular-nums` + `min-width: Nch` für stabile Breite während Count-up Animation |
|
||||
| 2026-02-20 | **CountTo.vue gelöscht** | vue-count-to Dependency entfernt, followedByCountStartValue/membersCountStartValue Pattern entfernt |
|
||||
| 2026-02-20 | **CSS-Variable --color-text-soft** | Neuer Contract-Eintrag in tailwind.preset.ts + ocelot-ui-variables.scss (Label-Farbe) |
|
||||
| 2026-02-20 | **Admin-Label uppercase** | `.admin-stats__item .os-number-label { text-transform: uppercase }` per CSS statt neuem Prop |
|
||||
|
||||
---
|
||||
|
||||
@ -1823,10 +1848,10 @@ Bei der Migration werden:
|
||||
**Styleguide-Migration:**
|
||||
| Status | Komponenten |
|
||||
|--------|------------|
|
||||
| ✅ UI-Library | OsButton, OsIcon, OsSpinner, OsCard, OsBadge (5) |
|
||||
| ✅ UI-Library | OsButton, OsIcon, OsSpinner, OsCard, OsBadge, OsNumber (6) |
|
||||
| ✅ → Plain HTML | Section, Placeholder, List, ListItem, Container, Heading, Text, Space, Flex, FlexItem, Grid, GridItem, Table (13) — Tier A/B |
|
||||
| ✅ → UI-Library | Chip, Tag → OsBadge (2) — Tier B |
|
||||
| ⬜ → Plain HTML | Number, Radio (2) — Tier B |
|
||||
| ✅ → UI-Library | Chip, Tag → OsBadge (2), Number → OsNumber (1) — Tier B |
|
||||
| ⬜ → Plain HTML | Radio (1) — Tier B |
|
||||
| ⬜ → UI-Library | Modal, Input, Menu, MenuItem, Select (5) — Tier 2-3 |
|
||||
| ⬜ Nicht genutzt | Code, CopyField, FormItem, InputError, InputLabel, Page, PageTitle, Logo, Avatar, TableCol, TableHeadCol (11) |
|
||||
| ⬜ Offen | Form (18 Dateien — HTML `<form>` oder OsForm?) |
|
||||
|
||||
268
packages/ui/src/components/OsNumber/OsNumber.spec.ts
Normal file
268
packages/ui/src/components/OsNumber/OsNumber.spec.ts
Normal file
@ -0,0 +1,268 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import OsNumber from './OsNumber.vue'
|
||||
|
||||
describe('osNumber', () => {
|
||||
let rafCallbacks: ((time: number) => void)[]
|
||||
let mockTime: number
|
||||
|
||||
beforeEach(() => {
|
||||
rafCallbacks = []
|
||||
mockTime = 0
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(
|
||||
// eslint-disable-next-line promise/prefer-await-to-callbacks
|
||||
(cb) => {
|
||||
rafCallbacks.push(cb)
|
||||
return rafCallbacks.length
|
||||
},
|
||||
)
|
||||
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {})
|
||||
vi.spyOn(performance, 'now').mockImplementation(() => mockTime)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
function flushAnimation() {
|
||||
mockTime += 1500
|
||||
let safety = 0
|
||||
while (rafCallbacks.length > 0 && safety++ < 100) {
|
||||
const fn = rafCallbacks.shift() as (time: number) => void
|
||||
fn(mockTime)
|
||||
}
|
||||
}
|
||||
|
||||
describe('rendering', () => {
|
||||
it('renders as div element', () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 0 },
|
||||
})
|
||||
|
||||
expect((wrapper.element as HTMLElement).tagName).toBe('DIV')
|
||||
})
|
||||
|
||||
it('has os-number class', () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 0 },
|
||||
})
|
||||
|
||||
expect(wrapper.classes()).toContain('os-number')
|
||||
})
|
||||
|
||||
it('displays count as text', () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 42 },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-number-count').text()).toBe('42')
|
||||
})
|
||||
|
||||
it('displays 0 when count is 0', () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 0 },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-number-count').text()).toBe('0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('label', () => {
|
||||
it('shows label when set', () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 0, label: 'Followers' },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-number-label').exists()).toBe(true)
|
||||
expect(wrapper.find('.os-number-label').text()).toBe('Followers')
|
||||
})
|
||||
|
||||
it('hides label when not set', () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 0 },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-number-label').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('css', () => {
|
||||
it('merges custom classes', () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 0 },
|
||||
attrs: { class: 'my-custom-class' },
|
||||
})
|
||||
|
||||
expect(wrapper.classes()).toContain('os-number')
|
||||
expect(wrapper.classes()).toContain('my-custom-class')
|
||||
})
|
||||
|
||||
it('passes through attributes', () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 0 },
|
||||
attrs: { 'data-testid': 'my-number' },
|
||||
})
|
||||
|
||||
expect(wrapper.attributes('data-testid')).toBe('my-number')
|
||||
})
|
||||
|
||||
it('applies count styling classes', () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 42 },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-number-count').classes()).toContain('font-bold')
|
||||
expect(wrapper.find('.os-number-count').classes()).toContain('text-[1.5rem]')
|
||||
})
|
||||
|
||||
it('applies label styling classes', () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 0, label: 'Test' },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-number-label').classes()).toContain('text-[12px]')
|
||||
expect(wrapper.find('.os-number-label').classes()).toContain('text-[var(--color-text-soft)]')
|
||||
})
|
||||
})
|
||||
|
||||
describe('animation', () => {
|
||||
it('starts at 0 when animated is true', () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 100, animated: true },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-number-count').text()).toBe('0')
|
||||
})
|
||||
|
||||
it('hides animated count from screen readers and provides live region', () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 100, animated: true },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-number-count').attributes('aria-hidden')).toBe('true')
|
||||
|
||||
const liveRegion = wrapper.find('[aria-live="polite"]')
|
||||
|
||||
expect(liveRegion.exists()).toBe(true)
|
||||
expect(liveRegion.text()).toBe('100')
|
||||
expect(liveRegion.classes()).toContain('sr-only')
|
||||
})
|
||||
|
||||
it('does not add aria attributes when not animated', () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 42 },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-number-count').attributes('aria-hidden')).toBeUndefined()
|
||||
expect(wrapper.find('[aria-live]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('animates to target value after mount', async () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 100, animated: true },
|
||||
})
|
||||
|
||||
flushAnimation()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.find('.os-number-count').text()).toBe('100')
|
||||
})
|
||||
|
||||
it('re-animates when count changes', async () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 50, animated: true },
|
||||
})
|
||||
|
||||
flushAnimation()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.find('.os-number-count').text()).toBe('50')
|
||||
|
||||
await wrapper.setProps({ count: 100 })
|
||||
flushAnimation()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.find('.os-number-count').text()).toBe('100')
|
||||
})
|
||||
|
||||
it('shows intermediate value during animation', async () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 100, animated: true },
|
||||
})
|
||||
|
||||
mockTime += 750
|
||||
const fn = rafCallbacks.shift() as (time: number) => void
|
||||
fn(mockTime)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const intermediate = Number(wrapper.find('.os-number-count').text())
|
||||
|
||||
expect(intermediate).toBeGreaterThan(0)
|
||||
expect(intermediate).toBeLessThan(100)
|
||||
})
|
||||
|
||||
it('updates value directly when not animated', async () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 50, animated: false },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-number-count').text()).toBe('50')
|
||||
|
||||
await wrapper.setProps({ count: 100 })
|
||||
|
||||
expect(wrapper.find('.os-number-count').text()).toBe('100')
|
||||
})
|
||||
|
||||
it('cancels previous animation when count changes rapidly', async () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 50, animated: true },
|
||||
})
|
||||
|
||||
// First animation starts on mount
|
||||
expect(window.requestAnimationFrame).toHaveBeenCalledWith(expect.any(Function))
|
||||
|
||||
await wrapper.setProps({ count: 200 })
|
||||
|
||||
expect(window.cancelAnimationFrame).toHaveBeenCalledWith(expect.any(Number))
|
||||
|
||||
flushAnimation()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.find('.os-number-count').text()).toBe('200')
|
||||
})
|
||||
|
||||
it('cancels animation on unmount', () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 100, animated: true },
|
||||
})
|
||||
|
||||
// Animation is running
|
||||
expect(window.requestAnimationFrame).toHaveBeenCalledWith(expect.any(Function))
|
||||
|
||||
wrapper.unmount()
|
||||
|
||||
expect(window.cancelAnimationFrame).toHaveBeenCalledWith(expect.any(Number))
|
||||
})
|
||||
|
||||
it('does not cancel animation on unmount when not animated', () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 100 },
|
||||
})
|
||||
|
||||
wrapper.unmount()
|
||||
|
||||
expect(window.cancelAnimationFrame).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('keyboard accessibility', () => {
|
||||
it('is not focusable (non-interactive element)', () => {
|
||||
const wrapper = mount(OsNumber, {
|
||||
props: { count: 0 },
|
||||
})
|
||||
|
||||
expect(wrapper.attributes('tabindex')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
111
packages/ui/src/components/OsNumber/OsNumber.stories.ts
Normal file
111
packages/ui/src/components/OsNumber/OsNumber.stories.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import OsNumber from './OsNumber.vue'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
const meta: Meta<typeof OsNumber> = {
|
||||
title: 'Components/OsNumber',
|
||||
component: OsNumber,
|
||||
tags: ['autodocs'],
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof OsNumber>
|
||||
|
||||
interface PlaygroundArgs {
|
||||
count: number
|
||||
label: string
|
||||
animated: boolean
|
||||
}
|
||||
|
||||
export const Playground: StoryObj<PlaygroundArgs> = {
|
||||
argTypes: {
|
||||
count: {
|
||||
control: 'number',
|
||||
},
|
||||
label: {
|
||||
control: 'text',
|
||||
},
|
||||
animated: {
|
||||
control: 'boolean',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
count: 42,
|
||||
label: 'Followers',
|
||||
animated: false,
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { OsNumber },
|
||||
setup() {
|
||||
const numberProps = computed(() => ({
|
||||
count: args.count,
|
||||
label: args.label,
|
||||
animated: args.animated,
|
||||
}))
|
||||
const remountKey = computed(() => (args.animated ? Date.now() : 'static'))
|
||||
return { numberProps, remountKey }
|
||||
},
|
||||
template: `<OsNumber :key="remountKey" v-bind="numberProps" />`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const StaticCount: Story = {
|
||||
render: () => ({
|
||||
components: { OsNumber },
|
||||
template: `
|
||||
<div data-testid="static-count" class="flex items-center gap-8">
|
||||
<OsNumber :count="0" />
|
||||
<OsNumber :count="42" />
|
||||
<OsNumber :count="1337" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithLabel: Story = {
|
||||
render: () => ({
|
||||
components: { OsNumber },
|
||||
template: `
|
||||
<div data-testid="with-label" class="flex items-center gap-8">
|
||||
<OsNumber :count="12" label="Posts" />
|
||||
<OsNumber :count="42" label="Followers" />
|
||||
<OsNumber :count="7" label="Following" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const Animated: Story = {
|
||||
render: () => ({
|
||||
components: { OsNumber },
|
||||
template: `
|
||||
<div data-testid="animated" class="flex items-center gap-8">
|
||||
<OsNumber :count="128" label="Posts" :animated="true" />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const MultipleCounters: Story = {
|
||||
render: () => ({
|
||||
components: { OsNumber },
|
||||
template: `
|
||||
<div data-testid="multiple-counters" class="flex flex-col gap-6">
|
||||
<div class="flex items-center gap-8">
|
||||
<OsNumber :count="156" label="Users" />
|
||||
<OsNumber :count="42" label="Posts" />
|
||||
<OsNumber :count="7" label="Comments" />
|
||||
<OsNumber :count="3" label="Groups" />
|
||||
</div>
|
||||
<div class="flex items-center gap-8">
|
||||
<OsNumber :count="156" label="Users" :animated="true" />
|
||||
<OsNumber :count="42" label="Posts" :animated="true" />
|
||||
<OsNumber :count="7" label="Comments" :animated="true" />
|
||||
<OsNumber :count="3" label="Groups" :animated="true" />
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
95
packages/ui/src/components/OsNumber/OsNumber.visual.spec.ts
Normal file
95
packages/ui/src/components/OsNumber/OsNumber.visual.spec.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { AxeBuilder } from '@axe-core/playwright'
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
const STORY_URL = '/iframe.html?id=components-osnumber'
|
||||
const STORY_ROOT = '#storybook-root'
|
||||
|
||||
async function waitForReady(page: Page) {
|
||||
await page.evaluate(async () => document.fonts.ready)
|
||||
}
|
||||
|
||||
async function checkA11y(page: Page) {
|
||||
const results = await new AxeBuilder({ page }).include(STORY_ROOT).analyze()
|
||||
|
||||
expect(results.violations).toEqual([])
|
||||
}
|
||||
|
||||
test.describe('OsNumber keyboard accessibility', () => {
|
||||
test('number is not focusable (non-interactive element)', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--static-count&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
|
||||
const numbers = root.locator('.os-number')
|
||||
const count = await numbers.count()
|
||||
|
||||
expect(count).toBeGreaterThan(0)
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
await expect(numbers.nth(i)).not.toHaveAttribute('tabindex')
|
||||
await expect(numbers.nth(i)).not.toHaveAttribute('role')
|
||||
}
|
||||
|
||||
await page.keyboard.press('Tab')
|
||||
for (let i = 0; i < count; i++) {
|
||||
await expect(numbers.nth(i)).not.toBeFocused()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('OsNumber visual regression', () => {
|
||||
test('static count', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--static-count&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForReady(page)
|
||||
|
||||
await expect(root.locator('[data-testid="static-count"]')).toHaveScreenshot('static-count.png')
|
||||
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('with label', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--with-label&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForReady(page)
|
||||
|
||||
await expect(root.locator('[data-testid="with-label"]')).toHaveScreenshot('with-label.png')
|
||||
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('animated', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--animated&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForReady(page)
|
||||
|
||||
// Wait for animation to complete (first counter animates to 128)
|
||||
await expect(root.locator('.os-number-count').first()).toHaveText('128', { timeout: 3000 })
|
||||
|
||||
await expect(root.locator('[data-testid="animated"]')).toHaveScreenshot('animated.png')
|
||||
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('multiple counters', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--multiple-counters&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForReady(page)
|
||||
|
||||
// Wait for animated counters to finish (second row has animated OsNumber instances)
|
||||
const animatedCounters = root.locator('.os-number-count')
|
||||
await expect(animatedCounters.nth(4)).toHaveText('156', { timeout: 3000 })
|
||||
|
||||
await expect(root.locator('[data-testid="multiple-counters"]')).toHaveScreenshot(
|
||||
'multiple-counters.png',
|
||||
)
|
||||
|
||||
await checkA11y(page)
|
||||
})
|
||||
})
|
||||
181
packages/ui/src/components/OsNumber/OsNumber.vue
Normal file
181
packages/ui/src/components/OsNumber/OsNumber.vue
Normal file
@ -0,0 +1,181 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
isVue2,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue-demi'
|
||||
|
||||
import { cn } from '#src/utils'
|
||||
|
||||
import { numberVariants } from './number.variants'
|
||||
|
||||
import type { ClassValue } from 'clsx'
|
||||
|
||||
const ANIMATION_DURATION = 1500
|
||||
|
||||
function easeOut(t: number): number {
|
||||
return 1 - (1 - t) * (1 - t)
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-interactive numeric display with optional label and count-up animation.
|
||||
*
|
||||
* @slot default - Not used. Content is derived from the `count` prop.
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'OsNumber',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
/**
|
||||
* The number to display.
|
||||
*/
|
||||
count: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* Optional label displayed below the count.
|
||||
*/
|
||||
label: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
/**
|
||||
* Whether to animate from 0 to the count value on mount.
|
||||
* Re-animates when count changes.
|
||||
*/
|
||||
animated: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { attrs }) {
|
||||
/* v8 ignore start -- Vue 2 only */
|
||||
const instance = isVue2 ? getCurrentInstance() : null
|
||||
/* v8 ignore stop */
|
||||
|
||||
const displayValue = ref(props.animated ? 0 : props.count)
|
||||
let animationFrame: number | undefined
|
||||
|
||||
function animateTo(from: number, to: number) {
|
||||
if (animationFrame !== undefined) {
|
||||
cancelAnimationFrame(animationFrame)
|
||||
}
|
||||
|
||||
const startTime = performance.now()
|
||||
|
||||
function step(currentTime: number) {
|
||||
const elapsed = currentTime - startTime
|
||||
const progress = Math.min(elapsed / ANIMATION_DURATION, 1)
|
||||
const easedProgress = easeOut(progress)
|
||||
|
||||
displayValue.value = Math.round(from + (to - from) * easedProgress)
|
||||
|
||||
if (progress < 1) {
|
||||
animationFrame = requestAnimationFrame(step)
|
||||
}
|
||||
}
|
||||
|
||||
animationFrame = requestAnimationFrame(step)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.animated) {
|
||||
animateTo(0, props.count)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationFrame !== undefined) {
|
||||
cancelAnimationFrame(animationFrame)
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.count,
|
||||
(newVal, oldVal) => {
|
||||
if (props.animated) {
|
||||
/* v8 ignore start -- oldVal is always numeric from Vue's watch */
|
||||
animateTo(oldVal ?? 0, newVal)
|
||||
/* v8 ignore stop */
|
||||
} else {
|
||||
displayValue.value = newVal
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return () => {
|
||||
const rootClass = cn('os-number', numberVariants())
|
||||
|
||||
const countAttrs: Record<string, unknown> = {
|
||||
class: 'os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block',
|
||||
style: { minWidth: `${String(props.count).length}ch` },
|
||||
}
|
||||
|
||||
if (props.animated) {
|
||||
countAttrs['aria-hidden'] = 'true'
|
||||
}
|
||||
|
||||
const countChild = h('span', countAttrs, [String(displayValue.value)])
|
||||
|
||||
const children = [countChild]
|
||||
|
||||
if (props.animated) {
|
||||
children.push(
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
class: 'sr-only',
|
||||
'aria-live': 'polite',
|
||||
},
|
||||
[String(props.count)],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (props.label) {
|
||||
children.push(
|
||||
h('span', { class: 'os-number-label text-[12px] text-[var(--color-text-soft)]' }, [
|
||||
props.label,
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
/* v8 ignore start -- Vue 2 branch tested in webapp Jest tests */
|
||||
if (isVue2) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const proxy = instance?.proxy as any
|
||||
const parentClass = proxy?.$vnode?.data?.staticClass || ''
|
||||
const parentDynClass = proxy?.$vnode?.data?.class
|
||||
const parentAttrs = proxy?.$vnode?.data?.attrs || {}
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: cn(rootClass, parentClass, parentDynClass),
|
||||
attrs: { ...parentAttrs, ...attrs },
|
||||
},
|
||||
children,
|
||||
)
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
|
||||
const { class: attrClass, ...restAttrs } = attrs as Record<string, unknown>
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: cn(rootClass, attrClass as ClassValue),
|
||||
...restAttrs,
|
||||
},
|
||||
children,
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
2
packages/ui/src/components/OsNumber/index.ts
Normal file
2
packages/ui/src/components/OsNumber/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as OsNumber } from './OsNumber.vue'
|
||||
export { numberVariants, type NumberVariants } from './number.variants'
|
||||
15
packages/ui/src/components/OsNumber/number.variants.ts
Normal file
15
packages/ui/src/components/OsNumber/number.variants.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
|
||||
/**
|
||||
* Number display variants using CVA (Class Variance Authority)
|
||||
*
|
||||
* Non-interactive numeric display with optional label and count-up animation.
|
||||
*/
|
||||
export const numberVariants = cva(['flex flex-col items-center text-center'], {
|
||||
variants: {},
|
||||
defaultVariants: {},
|
||||
})
|
||||
|
||||
export type NumberVariants = VariantProps<typeof numberVariants>
|
||||
@ -27,3 +27,4 @@ export {
|
||||
type BadgeVariant,
|
||||
type BadgeVariants,
|
||||
} from './OsBadge'
|
||||
export { OsNumber, numberVariants, type NumberVariants } from './OsNumber'
|
||||
|
||||
@ -62,6 +62,8 @@ export const requiredCssVariables: string[] = [
|
||||
// Disabled
|
||||
'--color-disabled',
|
||||
'--color-disabled-contrast',
|
||||
// Text
|
||||
'--color-text-soft',
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@ -100,12 +100,6 @@
|
||||
.ds-text-warning { color: $text-color-warning; }
|
||||
.ds-text-danger { color: $text-color-danger; }
|
||||
|
||||
// ds-number Ersatz — Statistic display (count + label)
|
||||
// Grep: os-number
|
||||
.os-number { text-align: center; }
|
||||
.os-number-count { font-weight: $font-weight-bold; font-size: $font-size-x-large; display: block; }
|
||||
.os-number-label { font-size: $font-size-small; color: $text-color-soft; display: block; }
|
||||
|
||||
// ds-table Ersatz
|
||||
.ds-table-wrap { width: 100%; overflow: auto; }
|
||||
.ds-table { width: 100%; border-collapse: collapse; }
|
||||
|
||||
@ -54,4 +54,7 @@
|
||||
// Disabled state
|
||||
--color-disabled: #{$color-neutral-60}; // rgb(177, 171, 186)
|
||||
--color-disabled-contrast: #{$color-neutral-100}; // weiß
|
||||
|
||||
// Text
|
||||
--color-text-soft: #{$text-color-soft}; // rgb(112, 103, 126)
|
||||
}
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<span>
|
||||
<client-only placeholder="0" tag="span">
|
||||
<count-to
|
||||
:start-val="startVal"
|
||||
:end-val="endVal"
|
||||
:duration="duration"
|
||||
:autoplay="autoplay"
|
||||
:separator="separator"
|
||||
/>
|
||||
</client-only>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CountTo from 'vue-count-to'
|
||||
export default {
|
||||
components: {
|
||||
CountTo,
|
||||
},
|
||||
props: {
|
||||
startVal: { type: Number, default: 0 },
|
||||
endVal: { type: Number, default: 0 },
|
||||
duration: { type: Number, default: 3000 },
|
||||
autoplay: { type: Boolean, default: true },
|
||||
separator: { type: String, default: '.' },
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -8,6 +8,9 @@ const user = {
|
||||
id: 'id',
|
||||
name: 'Tilda Swinton',
|
||||
slug: 'tilda-swinton',
|
||||
followedByCount: 42,
|
||||
contributionsCount: 7,
|
||||
commentedCount: 13,
|
||||
badgeVerification: {
|
||||
id: 'bv1',
|
||||
icon: '/icons/verified',
|
||||
@ -42,7 +45,12 @@ const userLink = {
|
||||
}
|
||||
|
||||
describe('UserTeaserPopover', () => {
|
||||
const Wrapper = ({ badgesEnabled = true, withUserLink = true, onTouchScreen = false }) => {
|
||||
const Wrapper = ({
|
||||
badgesEnabled = true,
|
||||
withUserLink = true,
|
||||
onTouchScreen = false,
|
||||
userData = user,
|
||||
}) => {
|
||||
const mockIsTouchDevice = onTouchScreen
|
||||
jest.mock('../utils/isTouchDevice', () => ({
|
||||
isTouchDevice: jest.fn(() => mockIsTouchDevice),
|
||||
@ -54,7 +62,7 @@ describe('UserTeaserPopover', () => {
|
||||
userLink: withUserLink ? userLink : null,
|
||||
},
|
||||
data: () => ({
|
||||
User: [user],
|
||||
User: [userData],
|
||||
}),
|
||||
stubs: {
|
||||
NuxtLink: RouterLinkStub,
|
||||
@ -96,4 +104,15 @@ describe('UserTeaserPopover', () => {
|
||||
const wrapper = Wrapper({ badgesEnabled: false })
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders correctly for a fresh user with zero counts', () => {
|
||||
const freshUser = {
|
||||
...user,
|
||||
followedByCount: 0,
|
||||
contributionsCount: 0,
|
||||
commentedCount: 0,
|
||||
}
|
||||
const wrapper = Wrapper({ userData: freshUser })
|
||||
expect(wrapper.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
@ -10,18 +10,19 @@
|
||||
:is-owner="userId === $store.getters['auth/user'].id"
|
||||
class="location-info"
|
||||
/>
|
||||
<!-- No :animated on OsNumber — popover appears on hover, animation would be distracting -->
|
||||
<ul class="statistics">
|
||||
<li>
|
||||
<ds-number :count="user.followedByCount" :label="$t('profile.followers')" />
|
||||
<os-number :count="user.followedByCount" :label="$t('profile.followers')" />
|
||||
</li>
|
||||
<li>
|
||||
<ds-number
|
||||
<os-number
|
||||
:count="user.contributionsCount"
|
||||
:label="$t('common.post', null, user.contributionsCount)"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ds-number
|
||||
<os-number
|
||||
:count="user.commentedCount"
|
||||
:label="$t('common.comment', null, user.commentedCount)"
|
||||
/>
|
||||
@ -40,7 +41,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { OsButton, OsNumber } from '@ocelot-social/ui'
|
||||
import Badges from '~/components/Badges.vue'
|
||||
import LocationInfo from '~/components/LocationInfo/LocationInfo.vue'
|
||||
import { isTouchDevice } from '~/components/utils/isTouchDevice'
|
||||
@ -52,6 +53,7 @@ export default {
|
||||
Badges,
|
||||
LocationInfo,
|
||||
OsButton,
|
||||
OsNumber,
|
||||
},
|
||||
props: {
|
||||
userId: { type: String },
|
||||
|
||||
@ -14,61 +14,55 @@ exports[`UserTeaserPopover does not show badges when disabled 1`] = `
|
||||
>
|
||||
<li>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 2ch;"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
42
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
|
||||
profile.followers
|
||||
|
||||
</p>
|
||||
profile.followers
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 1ch;"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
7
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
|
||||
common.post
|
||||
|
||||
</p>
|
||||
common.post
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 2ch;"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
13
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
|
||||
common.comment
|
||||
|
||||
</p>
|
||||
common.comment
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@ -131,61 +125,55 @@ exports[`UserTeaserPopover given a non-touch device does not show button when us
|
||||
>
|
||||
<li>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 2ch;"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
42
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
|
||||
profile.followers
|
||||
|
||||
</p>
|
||||
profile.followers
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 1ch;"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
7
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
|
||||
common.post
|
||||
|
||||
</p>
|
||||
common.post
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 2ch;"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
13
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
|
||||
common.comment
|
||||
|
||||
</p>
|
||||
common.comment
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@ -248,61 +236,55 @@ exports[`UserTeaserPopover given a touch device does not show button when userLi
|
||||
>
|
||||
<li>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 2ch;"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
42
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
|
||||
profile.followers
|
||||
|
||||
</p>
|
||||
profile.followers
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 1ch;"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
7
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
|
||||
common.post
|
||||
|
||||
</p>
|
||||
common.post
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 2ch;"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
13
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
|
||||
common.comment
|
||||
|
||||
</p>
|
||||
common.comment
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@ -365,61 +347,166 @@ exports[`UserTeaserPopover given a touch device shows button when userLink is pr
|
||||
>
|
||||
<li>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 2ch;"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
42
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
|
||||
profile.followers
|
||||
|
||||
</p>
|
||||
profile.followers
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 1ch;"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
7
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
|
||||
common.post
|
||||
|
||||
</p>
|
||||
common.post
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 2ch;"
|
||||
>
|
||||
13
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
common.comment
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`UserTeaserPopover renders correctly for a fresh user with zero counts 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="user-teaser-popover"
|
||||
>
|
||||
<div
|
||||
class="hc-badges"
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/icons/verified"
|
||||
title="Verified"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/icons/trophy1"
|
||||
title="Trophy 1"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/icons/trophy2"
|
||||
title="Trophy 2"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
src="/api/icons/empty"
|
||||
title="Empty"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
|
||||
<ul
|
||||
class="statistics"
|
||||
>
|
||||
<li>
|
||||
<div
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 1ch;"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
|
||||
common.comment
|
||||
|
||||
</p>
|
||||
profile.followers
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 1ch;"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
common.post
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 1ch;"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
common.comment
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@ -482,61 +569,55 @@ exports[`UserTeaserPopover shows badges when enabled 1`] = `
|
||||
>
|
||||
<li>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 2ch;"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
42
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
|
||||
profile.followers
|
||||
|
||||
</p>
|
||||
profile.followers
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 1ch;"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
7
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
|
||||
common.post
|
||||
|
||||
</p>
|
||||
common.post
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 2ch;"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
13
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
|
||||
common.comment
|
||||
|
||||
</p>
|
||||
common.comment
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@ -60,7 +60,7 @@ describe('SearchResults', () => {
|
||||
|
||||
describe('result contains 25 posts, 8 users and 0 hashtags', () => {
|
||||
// we couldn't get it running with "jest.runAllTimers()" and so we used "setTimeout"
|
||||
// time is a bit more then 3000 milisec see "webapp/components/CountTo.vue"
|
||||
// OsNumber animation runs for 1500ms
|
||||
const counterTimeout = 3000 + 10
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@ -49,32 +49,27 @@ describe('TabNavigation', () => {
|
||||
})
|
||||
|
||||
describe('displays', () => {
|
||||
// we couldn't get it running with "jest.runAllTimers()" and so we used "setTimeout"
|
||||
// time is a bit more then 3000 milisec see "webapp/components/CountTo.vue"
|
||||
const counterTimeout = 3000 + 10
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
// Re-mount with fake timers so requestAnimationFrame is captured
|
||||
wrapper = Wrapper()
|
||||
jest.advanceTimersByTime(1600)
|
||||
})
|
||||
|
||||
it('shows a total of 17 results', () => {
|
||||
setTimeout(() => {
|
||||
expect(wrapper.find('.total-search-results').text()).toContain('17')
|
||||
}, counterTimeout)
|
||||
afterEach(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('shows tab with 12 posts', () => {
|
||||
setTimeout(() => {
|
||||
expect(wrapper.find('[data-test="Post-tab"]').text()).toContain('12')
|
||||
}, counterTimeout)
|
||||
expect(wrapper.find('[data-test="Post-tab"]').text()).toContain('12')
|
||||
})
|
||||
|
||||
it('shows tab with 9 users', () => {
|
||||
setTimeout(() => {
|
||||
expect(wrapper.find('[data-test="User-tab"]').text()).toContain('9')
|
||||
}, counterTimeout)
|
||||
expect(wrapper.find('[data-test="User-tab"]').text()).toContain('9')
|
||||
})
|
||||
|
||||
it('shows tab with 0 hashtags', () => {
|
||||
setTimeout(() => {
|
||||
expect(wrapper.find('[data-test="Hashtag-tab"]').text()).toContain('0')
|
||||
}, counterTimeout)
|
||||
expect(wrapper.find('[data-test="Hashtag-tab"]').text()).toContain('0')
|
||||
})
|
||||
|
||||
describe('basic props setting', () => {
|
||||
|
||||
@ -16,9 +16,7 @@
|
||||
<a :data-test="tab.type + '-tab-click'" @click="switchTab(tab)">
|
||||
<div class="ds-my-small">
|
||||
<client-only :placeholder="$t('client-only.loading')">
|
||||
<ds-number :label="tab.title">
|
||||
<hc-count-to slot="count" :end-val="tab.count" />
|
||||
</ds-number>
|
||||
<os-number :count="tab.count" :label="tab.title" :animated="true" />
|
||||
</client-only>
|
||||
</div>
|
||||
</a>
|
||||
@ -29,13 +27,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsCard } from '@ocelot-social/ui'
|
||||
import HcCountTo from '~/components/CountTo.vue'
|
||||
import { OsCard, OsNumber } from '@ocelot-social/ui'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OsCard,
|
||||
HcCountTo,
|
||||
OsNumber,
|
||||
},
|
||||
props: {
|
||||
tabs: {
|
||||
|
||||
@ -55,7 +55,6 @@
|
||||
"validator": "^13.15.26",
|
||||
"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",
|
||||
|
||||
@ -14,11 +14,7 @@
|
||||
class="admin-stats__item"
|
||||
>
|
||||
<div class="ds-my-small">
|
||||
<ds-number :count="0" :label="$t('admin.dashboard.' + name)" size="x-large" uppercase>
|
||||
<client-only slot="count">
|
||||
<hc-count-to :end-val="value" />
|
||||
</client-only>
|
||||
</ds-number>
|
||||
<os-number :count="value" :label="$t('admin.dashboard.' + name)" :animated="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -38,15 +34,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsCard, OsSpinner } from '@ocelot-social/ui'
|
||||
import HcCountTo from '~/components/CountTo.vue'
|
||||
import { OsCard, OsNumber, OsSpinner } from '@ocelot-social/ui'
|
||||
import { Statistics } from '~/graphql/admin/Statistics'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OsCard,
|
||||
OsNumber,
|
||||
OsSpinner,
|
||||
HcCountTo,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -75,6 +70,10 @@ export default {
|
||||
.admin-stats__item {
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
|
||||
.os-number-label {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
@media #{$media-query-small} {
|
||||
.admin-stats__item {
|
||||
|
||||
@ -288,36 +288,26 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a close
|
||||
<div
|
||||
class="ds-flex-item"
|
||||
>
|
||||
<client-only-stub>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
<div
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 1ch;"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
>
|
||||
<span>
|
||||
<client-only-stub
|
||||
placeholder="0"
|
||||
tag="span"
|
||||
>
|
||||
<span>
|
||||
|
||||
0
|
||||
|
||||
</span>
|
||||
</client-only-stub>
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
>
|
||||
|
||||
group.membersCount
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</client-only-stub>
|
||||
0
|
||||
</span>
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
group.membersCount
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2343,36 +2333,26 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a close
|
||||
<div
|
||||
class="ds-flex-item"
|
||||
>
|
||||
<client-only-stub>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
<div
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 1ch;"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
>
|
||||
<span>
|
||||
<client-only-stub
|
||||
placeholder="0"
|
||||
tag="span"
|
||||
>
|
||||
<span>
|
||||
|
||||
0
|
||||
|
||||
</span>
|
||||
</client-only-stub>
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
>
|
||||
|
||||
group.membersCount
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</client-only-stub>
|
||||
0
|
||||
</span>
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
group.membersCount
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3424,36 +3404,26 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a curre
|
||||
<div
|
||||
class="ds-flex-item"
|
||||
>
|
||||
<client-only-stub>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
<div
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 1ch;"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
>
|
||||
<span>
|
||||
<client-only-stub
|
||||
placeholder="0"
|
||||
tag="span"
|
||||
>
|
||||
<span>
|
||||
|
||||
0
|
||||
|
||||
</span>
|
||||
</client-only-stub>
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
>
|
||||
|
||||
group.membersCount
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</client-only-stub>
|
||||
0
|
||||
</span>
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
4
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
group.membersCount
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4287,36 +4257,26 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a curre
|
||||
<div
|
||||
class="ds-flex-item"
|
||||
>
|
||||
<client-only-stub>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
<div
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 1ch;"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
>
|
||||
<span>
|
||||
<client-only-stub
|
||||
placeholder="0"
|
||||
tag="span"
|
||||
>
|
||||
<span>
|
||||
|
||||
0
|
||||
|
||||
</span>
|
||||
</client-only-stub>
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
>
|
||||
|
||||
group.membersCount
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</client-only-stub>
|
||||
0
|
||||
</span>
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
4
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
group.membersCount
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -5096,36 +5056,26 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a curre
|
||||
<div
|
||||
class="ds-flex-item"
|
||||
>
|
||||
<client-only-stub>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
<div
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 1ch;"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
>
|
||||
<span>
|
||||
<client-only-stub
|
||||
placeholder="0"
|
||||
tag="span"
|
||||
>
|
||||
<span>
|
||||
|
||||
0
|
||||
|
||||
</span>
|
||||
</client-only-stub>
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
>
|
||||
|
||||
group.membersCount
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</client-only-stub>
|
||||
0
|
||||
</span>
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
4
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
group.membersCount
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -6019,36 +5969,26 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a curre
|
||||
<div
|
||||
class="ds-flex-item"
|
||||
>
|
||||
<client-only-stub>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
<div
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 1ch;"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
>
|
||||
<span>
|
||||
<client-only-stub
|
||||
placeholder="0"
|
||||
tag="span"
|
||||
>
|
||||
<span>
|
||||
|
||||
0
|
||||
|
||||
</span>
|
||||
</client-only-stub>
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
>
|
||||
|
||||
group.membersCount
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</client-only-stub>
|
||||
0
|
||||
</span>
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
4
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
group.membersCount
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -7096,36 +7036,26 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a hidde
|
||||
<div
|
||||
class="ds-flex-item"
|
||||
>
|
||||
<client-only-stub>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
<div
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 1ch;"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
>
|
||||
<span>
|
||||
<client-only-stub
|
||||
placeholder="0"
|
||||
tag="span"
|
||||
>
|
||||
<span>
|
||||
|
||||
0
|
||||
|
||||
</span>
|
||||
</client-only-stub>
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
>
|
||||
|
||||
group.membersCount
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</client-only-stub>
|
||||
0
|
||||
</span>
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
group.membersCount
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -8150,36 +8080,26 @@ exports[`GroupProfileSlug given a puplic group – "yoga-practice" given a hidde
|
||||
<div
|
||||
class="ds-flex-item"
|
||||
>
|
||||
<client-only-stub>
|
||||
<div
|
||||
class="ds-number ds-number-size-x-large"
|
||||
<div
|
||||
class="os-number flex flex-col items-center text-center"
|
||||
>
|
||||
<span
|
||||
class="os-number-count font-bold text-[1.5rem] tabular-nums text-center inline-block"
|
||||
style="min-width: 1ch;"
|
||||
>
|
||||
<p
|
||||
class="ds-text ds-number-count ds-text-size-x-large"
|
||||
style="margin-bottom: 0px;"
|
||||
>
|
||||
<span>
|
||||
<client-only-stub
|
||||
placeholder="0"
|
||||
tag="span"
|
||||
>
|
||||
<span>
|
||||
|
||||
0
|
||||
|
||||
</span>
|
||||
</client-only-stub>
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
class="ds-text ds-number-label ds-text-size-small ds-text-soft"
|
||||
>
|
||||
|
||||
group.membersCount
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</client-only-stub>
|
||||
0
|
||||
</span>
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
<span
|
||||
class="os-number-label text-[12px] text-[var(--color-text-soft)]"
|
||||
>
|
||||
group.membersCount
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -118,6 +118,7 @@ describe('GroupProfileSlug', () => {
|
||||
locationName: null,
|
||||
location: null,
|
||||
isMutedByMe: false,
|
||||
membersCount: 4,
|
||||
// myRole: 'usual',
|
||||
}
|
||||
schoolForCitizens = {
|
||||
@ -152,6 +153,7 @@ describe('GroupProfileSlug', () => {
|
||||
nameEN: 'Paris',
|
||||
},
|
||||
isMutedByMe: true,
|
||||
membersCount: 0,
|
||||
// myRole: 'usual',
|
||||
}
|
||||
investigativeJournalism = {
|
||||
@ -195,6 +197,7 @@ describe('GroupProfileSlug', () => {
|
||||
nameEN: 'Hamburg',
|
||||
},
|
||||
isMutedByMe: false,
|
||||
membersCount: 0,
|
||||
// myRole: 'usual',
|
||||
}
|
||||
peterLustig = {
|
||||
|
||||
@ -51,15 +51,11 @@
|
||||
<div class="ds-flex" v-if="isAllowedSeeingGroupMembers">
|
||||
<!-- group members count -->
|
||||
<div class="ds-flex-item" v-if="isAllowedSeeingGroupMembers">
|
||||
<client-only>
|
||||
<ds-number :label="$t('group.membersCount', {}, groupMembers.length)">
|
||||
<count-to
|
||||
slot="count"
|
||||
:start-val="membersCountStartValue"
|
||||
:end-val="group.membersCount"
|
||||
/>
|
||||
</ds-number>
|
||||
</client-only>
|
||||
<os-number
|
||||
:count="group.membersCount"
|
||||
:label="$t('group.membersCount', {}, groupMembers.length)"
|
||||
:animated="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
@ -80,7 +76,6 @@
|
||||
:isNonePendingMember="isGroupMemberNonePending"
|
||||
:disabled="isGroupOwner"
|
||||
:loading="$apollo.loading"
|
||||
@prepare="prepareJoinLeave"
|
||||
@update="updateJoinLeave"
|
||||
/>
|
||||
</div>
|
||||
@ -293,7 +288,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsBadge, OsButton, OsCard, OsIcon, OsSpinner } from '@ocelot-social/ui'
|
||||
import { OsBadge, OsButton, OsCard, OsIcon, OsNumber, OsSpinner } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import uniqBy from 'lodash/uniqBy'
|
||||
import { profilePagePosts } from '~/graphql/PostQuery'
|
||||
@ -304,7 +299,6 @@ import postListActions from '~/mixins/postListActions'
|
||||
import AvatarUploader from '~/components/Uploader/AvatarUploader'
|
||||
import Category from '~/components/Category'
|
||||
import ContentViewer from '~/components/Editor/ContentViewer'
|
||||
import CountTo from '~/components/CountTo.vue'
|
||||
import Empty from '~/components/Empty/Empty'
|
||||
import GroupContentMenu from '~/components/ContentMenu/GroupContentMenu'
|
||||
import JoinLeaveButton from '~/components/Button/JoinLeaveButton'
|
||||
@ -334,11 +328,11 @@ export default {
|
||||
OsCard,
|
||||
OsButton,
|
||||
OsIcon,
|
||||
OsNumber,
|
||||
OsSpinner,
|
||||
AvatarUploader,
|
||||
Category,
|
||||
ContentViewer,
|
||||
CountTo,
|
||||
Empty,
|
||||
GroupContentMenu,
|
||||
JoinLeaveButton,
|
||||
@ -372,7 +366,6 @@ export default {
|
||||
pageSize: 6,
|
||||
// tabActive: 'post',
|
||||
filter,
|
||||
membersCountStartValue: 0,
|
||||
membersCountToLoad: 25,
|
||||
updateGroupMutation,
|
||||
isDescriptionCollapsed: true,
|
||||
@ -577,10 +570,6 @@ export default {
|
||||
// this.user.followedByCurrentUser = followedByCurrentUser
|
||||
// this.user.followedBy = followedBy
|
||||
// },
|
||||
prepareJoinLeave() {
|
||||
// "membersCountStartValue" is updated to avoid counting from 0 when join/leave
|
||||
this.membersCountStartValue = (this.GroupMembers && this.GroupMembers.length) || 0
|
||||
},
|
||||
updateJoinLeave() {
|
||||
this.$apollo.queries.Group.refetch()
|
||||
if (this.isAllowedSeeingGroupMembers) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -95,6 +95,8 @@ describe('ProfileSlug', () => {
|
||||
contributionsCount: 6,
|
||||
shoutedCount: 7,
|
||||
commentedCount: 8,
|
||||
followedByCount: 0,
|
||||
followingCount: 0,
|
||||
location: {
|
||||
name: 'Berlin',
|
||||
distanceToMe: '877 km',
|
||||
@ -157,6 +159,8 @@ describe('ProfileSlug', () => {
|
||||
contributionsCount: 6,
|
||||
shoutedCount: 7,
|
||||
commentedCount: 8,
|
||||
followedByCount: 0,
|
||||
followingCount: 0,
|
||||
location: {
|
||||
name: 'Paris',
|
||||
distanceToMe: '0 km',
|
||||
|
||||
@ -51,22 +51,18 @@
|
||||
</div>
|
||||
<div class="ds-flex">
|
||||
<div class="ds-flex-item">
|
||||
<client-only>
|
||||
<ds-number :label="$t('profile.followers')">
|
||||
<hc-count-to
|
||||
slot="count"
|
||||
:start-val="followedByCountStartValue"
|
||||
:end-val="user.followedByCount"
|
||||
/>
|
||||
</ds-number>
|
||||
</client-only>
|
||||
<os-number
|
||||
:count="user.followedByCount"
|
||||
:label="$t('profile.followers')"
|
||||
:animated="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="ds-flex-item">
|
||||
<client-only>
|
||||
<ds-number :label="$t('profile.following')">
|
||||
<hc-count-to slot="count" :end-val="user.followingCount" />
|
||||
</ds-number>
|
||||
</client-only>
|
||||
<os-number
|
||||
:count="user.followingCount"
|
||||
:label="$t('profile.following')"
|
||||
:animated="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!myProfile" class="action-buttons">
|
||||
@ -212,14 +208,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton, OsCard, OsIcon, OsSpinner } from '@ocelot-social/ui'
|
||||
import { OsButton, OsCard, OsIcon, OsNumber, OsSpinner } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import uniqBy from 'lodash/uniqBy'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import postListActions from '~/mixins/postListActions'
|
||||
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
|
||||
import HcFollowButton from '~/components/Button/FollowButton'
|
||||
import HcCountTo from '~/components/CountTo.vue'
|
||||
import HcBadges from '~/components/Badges.vue'
|
||||
import FollowList, { followListVisibleCount } from '~/components/features/ProfileList/FollowList'
|
||||
import HcEmpty from '~/components/Empty/Empty'
|
||||
@ -250,11 +245,11 @@ export default {
|
||||
OsCard,
|
||||
OsButton,
|
||||
OsIcon,
|
||||
OsNumber,
|
||||
OsSpinner,
|
||||
SocialMedia,
|
||||
PostTeaser,
|
||||
HcFollowButton,
|
||||
HcCountTo,
|
||||
HcBadges,
|
||||
HcEmpty,
|
||||
ProfileAvatar,
|
||||
@ -289,7 +284,6 @@ export default {
|
||||
pageSize: 6,
|
||||
tabActive: 'post',
|
||||
filter,
|
||||
followedByCountStartValue: 0,
|
||||
followedByCount: followListVisibleCount,
|
||||
followingCount: followListVisibleCount,
|
||||
updateUserMutation,
|
||||
@ -428,10 +422,6 @@ export default {
|
||||
})
|
||||
},
|
||||
optimisticFollow({ followedByCurrentUser }) {
|
||||
/*
|
||||
* Note: followedByCountStartValue is updated to avoid counting from 0 when follow/unfollow
|
||||
*/
|
||||
this.followedByCountStartValue = this.user.followedByCount
|
||||
const currentUser = this.$store.getters['auth/user']
|
||||
if (followedByCurrentUser) {
|
||||
this.user.followedByCount++
|
||||
@ -443,7 +433,6 @@ export default {
|
||||
this.user.followedByCurrentUser = followedByCurrentUser
|
||||
},
|
||||
updateFollow({ followedByCurrentUser, followedBy, followedByCount }) {
|
||||
this.followedByCountStartValue = this.user.followedByCount
|
||||
this.user.followedByCount = followedByCount
|
||||
this.user.followedByCurrentUser = followedByCurrentUser
|
||||
this.user.followedBy = followedBy
|
||||
|
||||
@ -21269,11 +21269,6 @@ vue-client-only@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/vue-client-only/-/vue-client-only-2.1.0.tgz#1a67a47b8ecacfa86d75830173fffee3bf8a4ee3"
|
||||
integrity sha512-vKl1skEKn8EK9f8P2ZzhRnuaRHLHrlt1sbRmazlvsx6EiC3A8oWF8YCBrMJzoN+W3OnElwIGbVjsx6/xelY1AA==
|
||||
|
||||
vue-count-to@~1.0.13:
|
||||
version "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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user