Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/check-codegen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ jobs:
cache: npm
cache-dependency-path: v2/sdui/schema/package-lock.json

- name: Install dependencies
- name: Install schema dependencies
run: npm ci
working-directory: v2/sdui/schema

- name: Install lib dependencies (needed for TypeScript type resolution)
run: npm ci
working-directory: v2/lib

- name: Check codegen drift
run: npm run check-codegen
working-directory: v2/sdui/schema
2 changes: 1 addition & 1 deletion v2/cli/src/utils/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const VUE_INDEX_EXPORTS: Record<string, string> = {
};

// Components that have Fx animation props
const FX_COMPONENTS = new Set(['BadgeFx', 'ButtonFx', 'IconButtonFx']);
const FX_COMPONENTS = new Set(['BadgeFx', 'ButtonFx', 'IconButtonFx', 'TagFx', 'AvatarFx', 'TooltipFx']);

// Components with an `open` prop that are invisible until triggered
const OPEN_CONTROLLED_COMPONENTS = new Set([
Expand Down
8 changes: 4 additions & 4 deletions v2/docs/FX_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ This document specifies the standardized approach for extending AgnosticUI core
| BadgeFx | ✅ Done (`v2/lib/src/components/BadgeFx/`) |
| ButtonFx | ✅ Done (`v2/lib/src/components/ButtonFx/`) |
| IconButtonFx | ✅ Done (`v2/lib/src/components/IconButtonFx/`) |
| TagFx | ⬜ Remaining |
| AvatarFx | ⬜ Remaining |
| TooltipFx | ⬜ Remaining |
| TagFx | ✅ Done (`v2/lib/src/components/TagFx/`) |
| AvatarFx | ✅ Done (`v2/lib/src/components/AvatarFx/`) |
| TooltipFx | ✅ Done (`v2/lib/src/components/TooltipFx/`) |

The architecture spec below applies to all remaining Fx components.
All Fx components are implemented. The architecture spec below serves as reference.

---

Expand Down
36 changes: 36 additions & 0 deletions v2/lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,42 @@
"types": "./dist/components/IconButtonFx/vue/index.d.ts",
"import": "./dist/components/IconButtonFx/vue/index.js"
},
"./tag-fx": {
"types": "./dist/components/TagFx/core/TagFx.d.ts",
"import": "./dist/components/TagFx/core/TagFx.js"
},
"./tag-fx/react": {
"types": "./dist/components/TagFx/react/ReactTagFx.d.ts",
"import": "./dist/components/TagFx/react/ReactTagFx.js"
},
"./tag-fx/vue": {
"types": "./dist/components/TagFx/vue/index.d.ts",
"import": "./dist/components/TagFx/vue/index.js"
},
"./avatar-fx": {
"types": "./dist/components/AvatarFx/core/AvatarFx.d.ts",
"import": "./dist/components/AvatarFx/core/AvatarFx.js"
},
"./avatar-fx/react": {
"types": "./dist/components/AvatarFx/react/ReactAvatarFx.d.ts",
"import": "./dist/components/AvatarFx/react/ReactAvatarFx.js"
},
"./avatar-fx/vue": {
"types": "./dist/components/AvatarFx/vue/index.d.ts",
"import": "./dist/components/AvatarFx/vue/index.js"
},
"./tooltip-fx": {
"types": "./dist/components/TooltipFx/core/TooltipFx.d.ts",
"import": "./dist/components/TooltipFx/core/TooltipFx.js"
},
"./tooltip-fx/react": {
"types": "./dist/components/TooltipFx/react/ReactTooltipFx.d.ts",
"import": "./dist/components/TooltipFx/react/ReactTooltipFx.js"
},
"./tooltip-fx/vue": {
"types": "./dist/components/TooltipFx/vue/index.d.ts",
"import": "./dist/components/TooltipFx/vue/index.js"
},
"./selection-card": {
"types": "./dist/components/SelectionCard/core/SelectionCard.d.ts",
"import": "./dist/components/SelectionCard/core/SelectionCard.js"
Expand Down
4 changes: 2 additions & 2 deletions v2/lib/src/components/Avatar/core/_Avatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* Supports multiple sizes, shapes, and color variants.
*/

import { LitElement, html, css } from 'lit';
import { LitElement, html, css, type CSSResultGroup } from 'lit';
import { property } from 'lit/decorators.js';

export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
Expand Down Expand Up @@ -39,7 +39,7 @@ export interface AvatarProps {
}

export class Avatar extends LitElement implements AvatarProps {
static styles = css`
static styles: CSSResultGroup = css`
:host {
display: inline-flex;
}
Expand Down
13 changes: 13 additions & 0 deletions v2/lib/src/components/AvatarFx/core/AvatarFx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { AvatarFx } from './_AvatarFx.js';

if (!customElements.get('ag-avatar-fx')) {
customElements.define('ag-avatar-fx', AvatarFx);
}

declare global {
interface HTMLElementTagNameMap {
'ag-avatar-fx': AvatarFx;
}
}

export * from './_AvatarFx.js';
238 changes: 238 additions & 0 deletions v2/lib/src/components/AvatarFx/core/_AvatarFx.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { toHaveNoViolations } from 'jest-axe';
import { AvatarFx } from './AvatarFx';

expect.extend(toHaveNoViolations);

describe('AvatarFx', () => {
let element: AvatarFx;

beforeEach(() => {
element = document.createElement('ag-avatar-fx') as AvatarFx;
element.text = 'AB';
document.body.appendChild(element);
});

afterEach(() => {
if (element && element.parentNode) {
element.parentNode.removeChild(element);
}
});

describe('Basic Functionality', () => {
it('should render with default properties', async () => {
expect(element).toBeDefined();
expect(element.tagName.toLowerCase()).toBe('ag-avatar-fx');

await element.updateComplete;
const avatar = element.shadowRoot?.querySelector('.avatar');
expect(avatar).toBeDefined();
});

it('should inherit Avatar functionality', async () => {
await element.updateComplete;
const avatar = element.shadowRoot?.querySelector('.avatar');
expect(avatar?.tagName.toLowerCase()).toBe('div');
});

it('should render text content', async () => {
await element.updateComplete;
const avatarText = element.shadowRoot?.querySelector('.avatar-text');
expect(avatarText?.textContent).toBe('AB');
});
});

describe('FX Props', () => {
it('should have default fx props', () => {
expect(element.fxSpeed).toBe('md');
expect(element.fxEase).toBe('ease');
expect(element.fxDisabled).toBe(false);
});

it('should accept fx prop', async () => {
element.fx = 'pulse';
await element.updateComplete;
expect(element.fx).toBe('pulse');
});

it('should accept fxSpeed prop', async () => {
element.fxSpeed = 'lg';
await element.updateComplete;
expect(element.fxSpeed).toBe('lg');
});

it('should accept fxEase prop', async () => {
element.fxEase = 'spring-md';
await element.updateComplete;
expect(element.fxEase).toBe('spring-md');
});

it('should accept fxDisabled prop', async () => {
element.fxDisabled = true;
await element.updateComplete;
expect(element.fxDisabled).toBe(true);
});
});

describe('FX Class Application', () => {
it('should apply fx class to avatar element', async () => {
element.fx = 'pulse';
await element.updateComplete;

const avatar = element.shadowRoot?.querySelector('.avatar');
expect(avatar?.classList.contains('ag-fx-pulse')).toBe(true);
});

it('should update fx class when fx prop changes', async () => {
element.fx = 'pulse';
await element.updateComplete;

let avatar = element.shadowRoot?.querySelector('.avatar');
expect(avatar?.classList.contains('ag-fx-pulse')).toBe(true);

element.fx = 'bounce';
await element.updateComplete;

avatar = element.shadowRoot?.querySelector('.avatar');
expect(avatar?.classList.contains('ag-fx-pulse')).toBe(false);
expect(avatar?.classList.contains('ag-fx-bounce')).toBe(true);
});

it('should remove old fx class when changing fx', async () => {
element.fx = 'pulse';
await element.updateComplete;

element.fx = 'wobble';
await element.updateComplete;

const avatar = element.shadowRoot?.querySelector('.avatar');
expect(avatar?.classList.contains('ag-fx-pulse')).toBe(false);
expect(avatar?.classList.contains('ag-fx-wobble')).toBe(true);
});

it('should apply ag-fx-disabled class when fxDisabled is true', async () => {
element.fxDisabled = true;
await element.updateComplete;

const avatar = element.shadowRoot?.querySelector('.avatar');
expect(avatar?.classList.contains('ag-fx-disabled')).toBe(true);
});

it('should remove ag-fx-disabled class when fxDisabled is false', async () => {
element.fxDisabled = true;
await element.updateComplete;

element.fxDisabled = false;
await element.updateComplete;

const avatar = element.shadowRoot?.querySelector('.avatar');
expect(avatar?.classList.contains('ag-fx-disabled')).toBe(false);
});
});

describe('FX Custom Properties', () => {
it('should set --ag-fx-duration custom property based on fxSpeed', async () => {
element.fxSpeed = 'lg';
await element.updateComplete;

const avatar = element.shadowRoot?.querySelector('.avatar') as HTMLElement;
const durationValue = avatar?.style.getPropertyValue('--ag-fx-duration');
expect(durationValue).toBe('var(--ag-fx-duration-lg)');
});

it('should set --ag-fx-ease custom property based on fxEase', async () => {
element.fxEase = 'spring-md';
await element.updateComplete;

const avatar = element.shadowRoot?.querySelector('.avatar') as HTMLElement;
const easeValue = avatar?.style.getPropertyValue('--ag-fx-ease');
expect(easeValue).toBe('var(--ag-fx-ease-spring-md)');
});

it('should update custom properties when fxSpeed changes', async () => {
element.fxSpeed = 'sm';
await element.updateComplete;

element.fxSpeed = 'xl';
await element.updateComplete;

const avatar = element.shadowRoot?.querySelector('.avatar') as HTMLElement;
const durationValue = avatar?.style.getPropertyValue('--ag-fx-duration');
expect(durationValue).toBe('var(--ag-fx-duration-xl)');
});

it('should update custom properties when fxEase changes', async () => {
element.fxEase = 'ease-in';
await element.updateComplete;

element.fxEase = 'bounce';
await element.updateComplete;

const avatar = element.shadowRoot?.querySelector('.avatar') as HTMLElement;
const easeValue = avatar?.style.getPropertyValue('--ag-fx-ease');
expect(easeValue).toBe('var(--ag-fx-ease-bounce)');
});
});

describe('FX Effects Support', () => {
const fxEffects = [
'pulse',
'bounce',
'jelly',
'shimmer',
'glow',
'flip',
'ripple',
'highlight-sweep',
'press-pop',
'slide-in',
];

fxEffects.forEach((effect) => {
it(`should support ${effect} effect`, async () => {
element.fx = effect;
await element.updateComplete;

const avatar = element.shadowRoot?.querySelector('.avatar');
expect(avatar?.classList.contains(`ag-fx-${effect}`)).toBe(true);
});
});
});

describe('Integration with Avatar Props', () => {
it('should work with variant prop', async () => {
element.variant = 'info';
element.fx = 'pulse';
await element.updateComplete;

expect(element.variant).toBe('info');
const avatar = element.shadowRoot?.querySelector('.avatar');
expect(avatar?.classList.contains('ag-fx-pulse')).toBe(true);
});

it('should work with size prop', async () => {
element.size = 'lg';
element.fx = 'bounce';
await element.updateComplete;

expect(element.size).toBe('lg');
});

it('should work with shape prop', async () => {
element.shape = 'rounded';
element.fx = 'pulse';
await element.updateComplete;

expect(element.shape).toBe('rounded');
});

it('should render image when imgSrc is provided', async () => {
element.imgSrc = 'https://example.com/avatar.jpg';
element.imgAlt = 'Test avatar';
await element.updateComplete;

const img = element.shadowRoot?.querySelector('.avatar-image');
expect(img).toBeDefined();
});
});
});
Loading
Loading