diff --git a/.github/workflows/check-codegen.yml b/.github/workflows/check-codegen.yml index 79dae2602..0b3ad7bb2 100644 --- a/.github/workflows/check-codegen.yml +++ b/.github/workflows/check-codegen.yml @@ -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 diff --git a/v2/cli/src/utils/stories.ts b/v2/cli/src/utils/stories.ts index 09deb5f2a..578d8a2a4 100644 --- a/v2/cli/src/utils/stories.ts +++ b/v2/cli/src/utils/stories.ts @@ -21,7 +21,7 @@ const VUE_INDEX_EXPORTS: Record = { }; // 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([ diff --git a/v2/docs/FX_PLAN.md b/v2/docs/FX_PLAN.md index f84e95314..cafbe729c 100644 --- a/v2/docs/FX_PLAN.md +++ b/v2/docs/FX_PLAN.md @@ -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. --- diff --git a/v2/lib/package.json b/v2/lib/package.json index cd40e0ad8..befa26668 100644 --- a/v2/lib/package.json +++ b/v2/lib/package.json @@ -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" diff --git a/v2/lib/src/components/Avatar/core/_Avatar.ts b/v2/lib/src/components/Avatar/core/_Avatar.ts index af81cb768..7cfa005ba 100644 --- a/v2/lib/src/components/Avatar/core/_Avatar.ts +++ b/v2/lib/src/components/Avatar/core/_Avatar.ts @@ -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'; @@ -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; } diff --git a/v2/lib/src/components/AvatarFx/core/AvatarFx.ts b/v2/lib/src/components/AvatarFx/core/AvatarFx.ts new file mode 100644 index 000000000..64f8d716b --- /dev/null +++ b/v2/lib/src/components/AvatarFx/core/AvatarFx.ts @@ -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'; diff --git a/v2/lib/src/components/AvatarFx/core/_AvatarFx.spec.ts b/v2/lib/src/components/AvatarFx/core/_AvatarFx.spec.ts new file mode 100644 index 000000000..42535a71d --- /dev/null +++ b/v2/lib/src/components/AvatarFx/core/_AvatarFx.spec.ts @@ -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(); + }); + }); +}); diff --git a/v2/lib/src/components/AvatarFx/core/_AvatarFx.ts b/v2/lib/src/components/AvatarFx/core/_AvatarFx.ts new file mode 100644 index 000000000..4374732f7 --- /dev/null +++ b/v2/lib/src/components/AvatarFx/core/_AvatarFx.ts @@ -0,0 +1,281 @@ +import { css, type CSSResultGroup } from 'lit'; +import { property } from 'lit/decorators.js'; +import { Avatar, type AvatarProps } from '../../Avatar/core/_Avatar.js'; +import { motionStyles } from '../../../styles/motion.styles.js'; +import type { FxProps } from '../../../types/fx.js'; + +// Combined props interface +export interface AvatarFxProps extends AvatarProps, FxProps { } + +/** + * AvatarFx - Avatar with CSS animation effects + * + * Extends Avatar to add optional CSS-only animation effects. + * Inherits all Avatar functionality and styling. + * + * Features: + * - CSS-only FX effects + * - Automatic reduced-motion support + */ +export class AvatarFx extends Avatar implements AvatarFxProps { + static override styles: CSSResultGroup = [ + motionStyles, + Avatar.styles, + css` + /* ======================================== + FX EFFECT SETUP + Add necessary CSS properties for effects that need them + ======================================== */ + + /* Shimmer needs a gradient mask */ + .avatar.ag-fx-shimmer { + overflow: hidden; + -webkit-mask-image: linear-gradient( + to right, + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 0.9) 40%, + rgba(0, 0, 0, 0.9) 60%, + rgba(0, 0, 0, 0) 100% + ); + mask-image: linear-gradient( + to right, + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 1) 40%, + rgba(0, 0, 0, 1) 60%, + rgba(0, 0, 0, 0) 100% + ); + -webkit-mask-size: 250% 100%; + mask-size: 250% 100%; + -webkit-mask-position: 215% 0; + mask-position: 215% 0; + } + + /* Ripple needs overflow visible to show expanding ring */ + .avatar.ag-fx-ripple { + overflow: visible; + } + + /* Highlight Sweep needs a gradient background overlay */ + .avatar.ag-fx-highlight-sweep { + position: relative; + overflow: hidden; + } + + .avatar.ag-fx-highlight-sweep::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient( + 90deg, + transparent 0%, + transparent 30%, + var(--ag-fx-sweep-color, rgba(255, 255, 255, 0.5)) 50%, + transparent 70%, + transparent 100% + ); + transform: translateX(-100%); + pointer-events: none; + } + + /* ======================================== + FX EFFECT CLASSES + Applied to target element (.avatar) + ======================================== */ + + :host([fx="bounce"]) .avatar, + :host([fx="flip"]) .avatar, + :host([fx="jelly"]) .avatar { + animation-iteration-count: 1; + } + + /* Hover-triggered effects */ + :host([fx="bounce"]:hover) .avatar { + animation: ag-fx-bounce var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="pulse"]:hover) .avatar { + animation: ag-fx-pulse var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="jelly"]:hover) .avatar { + animation: ag-fx-jelly var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="shimmer"]:hover) .avatar { + animation: ag-fx-shimmer var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="glow"]:hover) .avatar { + animation: ag-fx-glow var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="flip"]:hover) .avatar { + animation: ag-fx-flip var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="ripple"]:hover) .avatar { + animation: ag-fx-ripple var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="highlight-sweep"]:hover) .avatar::before { + animation: ag-fx-highlight-sweep var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + /* Active/press effects */ + .avatar.ag-fx-press-pop { + cursor: pointer; + } + + .avatar.ag-fx-press-pop:active { + animation: ag-fx-press-pop var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + /* Entrance effects */ + .avatar.ag-fx-slide-in { + animation: ag-fx-slide-in var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + /* Disabled state */ + .avatar.ag-fx-disabled { + animation: none !important; + } + + .avatar.ag-fx-disabled::before { + animation: none !important; + } + + /* Reduced motion support */ + @media (prefers-reduced-motion: reduce) { + .avatar.ag-fx-bounce, + .avatar.ag-fx-pulse, + .avatar.ag-fx-jelly, + .avatar.ag-fx-shimmer, + .avatar.ag-fx-glow, + .avatar.ag-fx-flip, + .avatar.ag-fx-ripple, + .avatar.ag-fx-highlight-sweep, + .avatar.ag-fx-press-pop, + .avatar.ag-fx-slide-in { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } + } + ` + ]; + + // FX props + @property({ type: String, reflect: true }) + fx?: string; + + @property({ type: String, attribute: 'fx-speed', reflect: true }) + fxSpeed!: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + + @property({ type: String, attribute: 'fx-ease', reflect: true }) + fxEase!: 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce' | 'spring-sm' | 'spring-md' | 'spring-lg'; + + @property({ type: Boolean, attribute: 'fx-disabled', reflect: true }) + fxDisabled!: boolean; + + private _observer: MutationObserver | null = null; + + constructor() { + super(); + this.fxSpeed = 'md'; + this.fxEase = 'ease'; + this.fxDisabled = false; + } + + override connectedCallback() { + super.connectedCallback(); + this._updateTheme(); + + this._observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') { + this._updateTheme(); + } + }); + }); + + this._observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-theme'] + }); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + if (this._observer) { + this._observer.disconnect(); + this._observer = null; + } + } + + private _updateTheme() { + const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; + const targetEl = this.shadowRoot?.querySelector('.avatar'); + if (targetEl instanceof HTMLElement) { + targetEl.style.setProperty('--ag-fx-shadow-opacity', isDark ? '0.8' : '0.4'); + + if (isDark && this.variant === 'monochrome') { + targetEl.style.setProperty('--ag-fx-sweep-color', 'rgba(0, 0, 0, 0.6)'); + } else { + targetEl.style.setProperty('--ag-fx-sweep-color', 'rgba(255, 255, 255, 0.5)'); + } + } + } + + override firstUpdated(changedProperties: Map) { + super.firstUpdated(changedProperties); + this._applyFxClasses(); + this._applyFxCustomProperties(); + this._updateTheme(); + } + + override updated(changedProperties: Map) { + super.updated(changedProperties); + + if (changedProperties.has('fx') || changedProperties.has('fxDisabled')) { + this._applyFxClasses(); + } + + if (changedProperties.has('variant')) { + this._updateTheme(); + } + + if (changedProperties.has('fxSpeed') || changedProperties.has('fxEase')) { + this._applyFxCustomProperties(); + } + } + + private _applyFxClasses() { + const targetEl = this.shadowRoot?.querySelector('.avatar'); + if (targetEl) { + const classesToRemove: string[] = []; + targetEl.classList.forEach((className: string) => { + if (className.startsWith('ag-fx-')) { + classesToRemove.push(className); + } + }); + classesToRemove.forEach(className => targetEl.classList.remove(className)); + + if (this.fx && !this.fxDisabled) { + targetEl.classList.add(`ag-fx-${this.fx}`); + } + + if (this.fxDisabled) { + targetEl.classList.add('ag-fx-disabled'); + } else { + targetEl.classList.remove('ag-fx-disabled'); + } + } + } + + private _applyFxCustomProperties() { + const targetEl = this.shadowRoot?.querySelector('.avatar'); + if (targetEl instanceof HTMLElement) { + targetEl.style.setProperty('--ag-fx-duration', `var(--ag-fx-duration-${this.fxSpeed})`); + targetEl.style.setProperty('--ag-fx-ease', `var(--ag-fx-ease-${this.fxEase})`); + } + } +} diff --git a/v2/lib/src/components/AvatarFx/react/ReactAvatarFx.tsx b/v2/lib/src/components/AvatarFx/react/ReactAvatarFx.tsx new file mode 100644 index 000000000..a5a9ed43f --- /dev/null +++ b/v2/lib/src/components/AvatarFx/react/ReactAvatarFx.tsx @@ -0,0 +1,19 @@ +import * as React from "react"; +import { createComponent } from "@lit/react"; +import { AvatarFx, type AvatarFxProps } from "../core/AvatarFx"; + +export interface ReactAvatarFxProps extends AvatarFxProps { + children?: React.ReactNode; + className?: string; + id?: string; +} + +export const ReactAvatarFx = createComponent({ + tagName: "ag-avatar-fx", + elementClass: AvatarFx, + react: React, + events: {}, +}); + +export type { AvatarFxProps } from "../core/AvatarFx"; +export type { FxProps } from "../../../types/fx"; diff --git a/v2/lib/src/components/AvatarFx/vue/VueAvatarFx.vue b/v2/lib/src/components/AvatarFx/vue/VueAvatarFx.vue new file mode 100644 index 000000000..0ee3c3df7 --- /dev/null +++ b/v2/lib/src/components/AvatarFx/vue/VueAvatarFx.vue @@ -0,0 +1,63 @@ + + + diff --git a/v2/lib/src/components/AvatarFx/vue/index.ts b/v2/lib/src/components/AvatarFx/vue/index.ts new file mode 100644 index 000000000..afdaf91f4 --- /dev/null +++ b/v2/lib/src/components/AvatarFx/vue/index.ts @@ -0,0 +1,19 @@ +import VueAvatarFx from './VueAvatarFx.vue'; +import type { AvatarSize, AvatarShape, AvatarVariant } from '../../Avatar/core/Avatar'; + +export { VueAvatarFx }; + +/** Props for VueAvatarFx */ +export interface VueAvatarFxProps { + text?: string; + imgSrc?: string; + imgAlt?: string; + size?: AvatarSize; + shape?: AvatarShape; + variant?: AvatarVariant; + ariaLabel?: string; + fx?: string; + fxSpeed?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + fxEase?: 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce' | 'spring-sm' | 'spring-md' | 'spring-lg'; + fxDisabled?: boolean; +} diff --git a/v2/lib/src/components/Tag/core/_Tag.ts b/v2/lib/src/components/Tag/core/_Tag.ts index 2231e30fd..c816ab553 100644 --- a/v2/lib/src/components/Tag/core/_Tag.ts +++ b/v2/lib/src/components/Tag/core/_Tag.ts @@ -1,4 +1,4 @@ -import { LitElement, html, css } from 'lit'; +import { LitElement, html, css, type CSSResultGroup } from 'lit'; import { property } from 'lit/decorators.js'; export type TagVariant = 'info' | 'warning' | 'error' | 'success' | 'primary' | 'monochrome' | ''; @@ -28,7 +28,7 @@ export interface TagProps { * @fires tag-remove - Fired when the remove button is clicked */ export class AgTag extends LitElement implements TagProps { - static styles = css` + static styles: CSSResultGroup = css` :host { display: inline-flex; --tag-background-color: var(--ag-background-tertiary); diff --git a/v2/lib/src/components/TagFx/core/TagFx.ts b/v2/lib/src/components/TagFx/core/TagFx.ts new file mode 100644 index 000000000..7cb670858 --- /dev/null +++ b/v2/lib/src/components/TagFx/core/TagFx.ts @@ -0,0 +1,13 @@ +import { TagFx } from './_TagFx.js'; + +if (!customElements.get('ag-tag-fx')) { + customElements.define('ag-tag-fx', TagFx); +} + +declare global { + interface HTMLElementTagNameMap { + 'ag-tag-fx': TagFx; + } +} + +export * from './_TagFx.js'; diff --git a/v2/lib/src/components/TagFx/core/_TagFx.spec.ts b/v2/lib/src/components/TagFx/core/_TagFx.spec.ts new file mode 100644 index 000000000..0fbafc51c --- /dev/null +++ b/v2/lib/src/components/TagFx/core/_TagFx.spec.ts @@ -0,0 +1,243 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { toHaveNoViolations } from 'jest-axe'; +import { TagFx } from './TagFx'; + +expect.extend(toHaveNoViolations); + +describe('TagFx', () => { + let element: TagFx; + + beforeEach(() => { + element = document.createElement('ag-tag-fx') as TagFx; + element.textContent = 'Test Tag FX'; + 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-tag-fx'); + + await element.updateComplete; + const tagWrapper = element.shadowRoot?.querySelector('.tag-wrapper'); + expect(tagWrapper).toBeDefined(); + expect(element.textContent?.trim()).toBe('Test Tag FX'); + }); + + it('should inherit Tag functionality', async () => { + await element.updateComplete; + const tagWrapper = element.shadowRoot?.querySelector('.tag-wrapper'); + expect(tagWrapper?.tagName.toLowerCase()).toBe('div'); + }); + }); + + 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 tag-wrapper element', async () => { + element.fx = 'pulse'; + await element.updateComplete; + + const tagWrapper = element.shadowRoot?.querySelector('.tag-wrapper'); + expect(tagWrapper?.classList.contains('ag-fx-pulse')).toBe(true); + }); + + it('should update fx class when fx prop changes', async () => { + element.fx = 'pulse'; + await element.updateComplete; + + let tagWrapper = element.shadowRoot?.querySelector('.tag-wrapper'); + expect(tagWrapper?.classList.contains('ag-fx-pulse')).toBe(true); + + element.fx = 'bounce'; + await element.updateComplete; + + tagWrapper = element.shadowRoot?.querySelector('.tag-wrapper'); + expect(tagWrapper?.classList.contains('ag-fx-pulse')).toBe(false); + expect(tagWrapper?.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 tagWrapper = element.shadowRoot?.querySelector('.tag-wrapper'); + expect(tagWrapper?.classList.contains('ag-fx-pulse')).toBe(false); + expect(tagWrapper?.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 tagWrapper = element.shadowRoot?.querySelector('.tag-wrapper'); + expect(tagWrapper?.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 tagWrapper = element.shadowRoot?.querySelector('.tag-wrapper'); + expect(tagWrapper?.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 tagWrapper = element.shadowRoot?.querySelector('.tag-wrapper') as HTMLElement; + const durationValue = tagWrapper?.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 tagWrapper = element.shadowRoot?.querySelector('.tag-wrapper') as HTMLElement; + const easeValue = tagWrapper?.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 tagWrapper = element.shadowRoot?.querySelector('.tag-wrapper') as HTMLElement; + const durationValue = tagWrapper?.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 tagWrapper = element.shadowRoot?.querySelector('.tag-wrapper') as HTMLElement; + const easeValue = tagWrapper?.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 tagWrapper = element.shadowRoot?.querySelector('.tag-wrapper'); + expect(tagWrapper?.classList.contains(`ag-fx-${effect}`)).toBe(true); + }); + }); + }); + + describe('Integration with Tag Props', () => { + it('should work with variant prop', async () => { + element.variant = 'primary'; + element.fx = 'pulse'; + await element.updateComplete; + + expect(element.variant).toBe('primary'); + const tagWrapper = element.shadowRoot?.querySelector('.tag-wrapper'); + expect(tagWrapper?.classList.contains('ag-fx-pulse')).toBe(true); + }); + + it('should work with shape prop', async () => { + element.shape = 'pill'; + element.fx = 'bounce'; + await element.updateComplete; + + expect(element.shape).toBe('pill'); + }); + + it('should work with uppercase prop', async () => { + element.uppercase = true; + element.fx = 'pulse'; + await element.updateComplete; + + expect(element.uppercase).toBe(true); + const tagWrapper = element.shadowRoot?.querySelector('.tag-wrapper'); + expect(tagWrapper?.classList.contains('ag-fx-pulse')).toBe(true); + }); + }); + + describe('Events', () => { + it('should fire tag-remove event when removable and remove button is clicked', async () => { + element.removable = true; + await element.updateComplete; + + let removeEventFired = false; + element.addEventListener('tag-remove', () => { + removeEventFired = true; + }); + + const removeButton = element.shadowRoot?.querySelector('.tag-remove-button') as HTMLButtonElement; + removeButton?.click(); + + expect(removeEventFired).toBe(true); + }); + }); +}); diff --git a/v2/lib/src/components/TagFx/core/_TagFx.ts b/v2/lib/src/components/TagFx/core/_TagFx.ts new file mode 100644 index 000000000..7f07bda6f --- /dev/null +++ b/v2/lib/src/components/TagFx/core/_TagFx.ts @@ -0,0 +1,281 @@ +import { css, type CSSResultGroup } from 'lit'; +import { property } from 'lit/decorators.js'; +import { AgTag, type TagProps } from '../../Tag/core/_Tag.js'; +import { motionStyles } from '../../../styles/motion.styles.js'; +import type { FxProps } from '../../../types/fx.js'; + +// Combined props interface +export interface TagFxProps extends TagProps, FxProps { } + +/** + * TagFx - Tag with CSS animation effects + * + * Extends AgTag to add optional CSS-only animation effects. + * Inherits all Tag functionality and styling. + * + * Features: + * - CSS-only FX effects + * - Automatic reduced-motion support + */ +export class TagFx extends AgTag implements TagFxProps { + static override styles: CSSResultGroup = [ + motionStyles, + AgTag.styles, + css` + /* ======================================== + FX EFFECT SETUP + Add necessary CSS properties for effects that need them + ======================================== */ + + /* Shimmer needs a gradient mask */ + .tag-wrapper.ag-fx-shimmer { + overflow: hidden; + -webkit-mask-image: linear-gradient( + to right, + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 0.9) 40%, + rgba(0, 0, 0, 0.9) 60%, + rgba(0, 0, 0, 0) 100% + ); + mask-image: linear-gradient( + to right, + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 1) 40%, + rgba(0, 0, 0, 1) 60%, + rgba(0, 0, 0, 0) 100% + ); + -webkit-mask-size: 250% 100%; + mask-size: 250% 100%; + -webkit-mask-position: 215% 0; + mask-position: 215% 0; + } + + /* Ripple needs overflow visible to show expanding ring */ + .tag-wrapper.ag-fx-ripple { + overflow: visible; + } + + /* Highlight Sweep needs a gradient background overlay */ + .tag-wrapper.ag-fx-highlight-sweep { + position: relative; + overflow: hidden; + } + + .tag-wrapper.ag-fx-highlight-sweep::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient( + 90deg, + transparent 0%, + transparent 30%, + var(--ag-fx-sweep-color, rgba(255, 255, 255, 0.5)) 50%, + transparent 70%, + transparent 100% + ); + transform: translateX(-100%); + pointer-events: none; + } + + /* ======================================== + FX EFFECT CLASSES + Applied to target element (.tag-wrapper) + ======================================== */ + + :host([fx="bounce"]) .tag-wrapper, + :host([fx="flip"]) .tag-wrapper, + :host([fx="jelly"]) .tag-wrapper { + animation-iteration-count: 1; + } + + /* Hover-triggered effects */ + :host([fx="bounce"]:hover) .tag-wrapper { + animation: ag-fx-bounce var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="pulse"]:hover) .tag-wrapper { + animation: ag-fx-pulse var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="jelly"]:hover) .tag-wrapper { + animation: ag-fx-jelly var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="shimmer"]:hover) .tag-wrapper { + animation: ag-fx-shimmer var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="glow"]:hover) .tag-wrapper { + animation: ag-fx-glow var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="flip"]:hover) .tag-wrapper { + animation: ag-fx-flip var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="ripple"]:hover) .tag-wrapper { + animation: ag-fx-ripple var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="highlight-sweep"]:hover) .tag-wrapper::before { + animation: ag-fx-highlight-sweep var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + /* Active/press effects */ + .tag-wrapper.ag-fx-press-pop { + cursor: pointer; + } + + .tag-wrapper.ag-fx-press-pop:active { + animation: ag-fx-press-pop var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + /* Entrance effects */ + .tag-wrapper.ag-fx-slide-in { + animation: ag-fx-slide-in var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + /* Disabled state */ + .tag-wrapper.ag-fx-disabled { + animation: none !important; + } + + .tag-wrapper.ag-fx-disabled::before { + animation: none !important; + } + + /* Reduced motion support */ + @media (prefers-reduced-motion: reduce) { + .tag-wrapper.ag-fx-bounce, + .tag-wrapper.ag-fx-pulse, + .tag-wrapper.ag-fx-jelly, + .tag-wrapper.ag-fx-shimmer, + .tag-wrapper.ag-fx-glow, + .tag-wrapper.ag-fx-flip, + .tag-wrapper.ag-fx-ripple, + .tag-wrapper.ag-fx-highlight-sweep, + .tag-wrapper.ag-fx-press-pop, + .tag-wrapper.ag-fx-slide-in { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } + } + ` + ]; + + // FX props + @property({ type: String, reflect: true }) + fx?: string; + + @property({ type: String, attribute: 'fx-speed', reflect: true }) + fxSpeed!: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + + @property({ type: String, attribute: 'fx-ease', reflect: true }) + fxEase!: 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce' | 'spring-sm' | 'spring-md' | 'spring-lg'; + + @property({ type: Boolean, attribute: 'fx-disabled', reflect: true }) + fxDisabled!: boolean; + + private _observer: MutationObserver | null = null; + + constructor() { + super(); + this.fxSpeed = 'md'; + this.fxEase = 'ease'; + this.fxDisabled = false; + } + + override connectedCallback() { + super.connectedCallback(); + this._updateTheme(); + + this._observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') { + this._updateTheme(); + } + }); + }); + + this._observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-theme'] + }); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + if (this._observer) { + this._observer.disconnect(); + this._observer = null; + } + } + + private _updateTheme() { + const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; + const targetEl = this.shadowRoot?.querySelector('.tag-wrapper'); + if (targetEl instanceof HTMLElement) { + targetEl.style.setProperty('--ag-fx-shadow-opacity', isDark ? '0.8' : '0.4'); + + if (isDark && this.variant === 'monochrome') { + targetEl.style.setProperty('--ag-fx-sweep-color', 'rgba(0, 0, 0, 0.6)'); + } else { + targetEl.style.setProperty('--ag-fx-sweep-color', 'rgba(255, 255, 255, 0.5)'); + } + } + } + + override firstUpdated(changedProperties: Map) { + super.firstUpdated(changedProperties); + this._applyFxClasses(); + this._applyFxCustomProperties(); + this._updateTheme(); + } + + override updated(changedProperties: Map) { + super.updated(changedProperties); + + if (changedProperties.has('fx') || changedProperties.has('fxDisabled')) { + this._applyFxClasses(); + } + + if (changedProperties.has('variant')) { + this._updateTheme(); + } + + if (changedProperties.has('fxSpeed') || changedProperties.has('fxEase')) { + this._applyFxCustomProperties(); + } + } + + private _applyFxClasses() { + const targetEl = this.shadowRoot?.querySelector('.tag-wrapper'); + if (targetEl) { + const classesToRemove: string[] = []; + targetEl.classList.forEach((className: string) => { + if (className.startsWith('ag-fx-')) { + classesToRemove.push(className); + } + }); + classesToRemove.forEach(className => targetEl.classList.remove(className)); + + if (this.fx && !this.fxDisabled) { + targetEl.classList.add(`ag-fx-${this.fx}`); + } + + if (this.fxDisabled) { + targetEl.classList.add('ag-fx-disabled'); + } else { + targetEl.classList.remove('ag-fx-disabled'); + } + } + } + + private _applyFxCustomProperties() { + const targetEl = this.shadowRoot?.querySelector('.tag-wrapper'); + if (targetEl instanceof HTMLElement) { + targetEl.style.setProperty('--ag-fx-duration', `var(--ag-fx-duration-${this.fxSpeed})`); + targetEl.style.setProperty('--ag-fx-ease', `var(--ag-fx-ease-${this.fxEase})`); + } + } +} diff --git a/v2/lib/src/components/TagFx/react/ReactTagFx.tsx b/v2/lib/src/components/TagFx/react/ReactTagFx.tsx new file mode 100644 index 000000000..38dd75654 --- /dev/null +++ b/v2/lib/src/components/TagFx/react/ReactTagFx.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; +import { createComponent, type EventName } from "@lit/react"; +import { TagFx, type TagFxProps } from "../core/TagFx"; +import type { TagRemoveEvent } from "../../Tag/core/Tag"; + +export interface ReactTagFxProps extends TagFxProps { + children?: React.ReactNode; + className?: string; + id?: string; +} + +export const ReactTagFx = createComponent({ + tagName: "ag-tag-fx", + elementClass: TagFx, + react: React, + events: { + onTagRemove: 'tag-remove' as EventName, + }, +}); + +export type { TagFxProps } from "../core/TagFx"; +export type { FxProps } from "../../../types/fx"; diff --git a/v2/lib/src/components/TagFx/vue/VueTagFx.vue b/v2/lib/src/components/TagFx/vue/VueTagFx.vue new file mode 100644 index 000000000..f1f90f514 --- /dev/null +++ b/v2/lib/src/components/TagFx/vue/VueTagFx.vue @@ -0,0 +1,65 @@ + + + diff --git a/v2/lib/src/components/TagFx/vue/index.ts b/v2/lib/src/components/TagFx/vue/index.ts new file mode 100644 index 000000000..a55ac3fb7 --- /dev/null +++ b/v2/lib/src/components/TagFx/vue/index.ts @@ -0,0 +1,16 @@ +import VueTagFx from './VueTagFx.vue'; +import type { TagVariant, TagShape } from '../../Tag/core/Tag'; + +export { VueTagFx }; + +/** Props for VueTagFx */ +export interface VueTagFxProps { + variant?: TagVariant; + shape?: TagShape; + uppercase?: boolean; + removable?: boolean; + fx?: string; + fxSpeed?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + fxEase?: 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce' | 'spring-sm' | 'spring-md' | 'spring-lg'; + fxDisabled?: boolean; +} diff --git a/v2/lib/src/components/Tooltip/core/_Tooltip.ts b/v2/lib/src/components/Tooltip/core/_Tooltip.ts index e235a6a1d..a972a2d1d 100644 --- a/v2/lib/src/components/Tooltip/core/_Tooltip.ts +++ b/v2/lib/src/components/Tooltip/core/_Tooltip.ts @@ -8,7 +8,7 @@ * https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/ */ -import { LitElement, html, css } from 'lit'; +import { LitElement, html, css, type CSSResultGroup } from 'lit'; import { property, query, state } from 'lit/decorators.js'; import { computePosition, autoUpdate, flip, shift, offset, arrow, type Placement } from '@floating-ui/dom'; @@ -64,7 +64,7 @@ export interface TooltipProps { } export class Tooltip extends LitElement implements TooltipProps { - static styles = css` + static styles: CSSResultGroup = css` :host { display: inline-block; } diff --git a/v2/lib/src/components/TooltipFx/core/TooltipFx.ts b/v2/lib/src/components/TooltipFx/core/TooltipFx.ts new file mode 100644 index 000000000..f60e6361a --- /dev/null +++ b/v2/lib/src/components/TooltipFx/core/TooltipFx.ts @@ -0,0 +1,13 @@ +import { TooltipFx } from './_TooltipFx.js'; + +if (!customElements.get('ag-tooltip-fx')) { + customElements.define('ag-tooltip-fx', TooltipFx); +} + +declare global { + interface HTMLElementTagNameMap { + 'ag-tooltip-fx': TooltipFx; + } +} + +export * from './_TooltipFx.js'; diff --git a/v2/lib/src/components/TooltipFx/core/_TooltipFx.spec.ts b/v2/lib/src/components/TooltipFx/core/_TooltipFx.spec.ts new file mode 100644 index 000000000..560e4b574 --- /dev/null +++ b/v2/lib/src/components/TooltipFx/core/_TooltipFx.spec.ts @@ -0,0 +1,263 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { toHaveNoViolations } from 'jest-axe'; +import { TooltipFx } from './TooltipFx'; + +expect.extend(toHaveNoViolations); + +describe('TooltipFx', () => { + let element: TooltipFx; + + beforeEach(() => { + element = document.createElement('ag-tooltip-fx') as TooltipFx; + element.content = 'Test tooltip content'; + const trigger = document.createElement('button'); + trigger.textContent = 'Hover me'; + element.appendChild(trigger); + 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-tooltip-fx'); + + await element.updateComplete; + const tooltip = element.shadowRoot?.querySelector('.tooltip'); + expect(tooltip).toBeDefined(); + }); + + it('should inherit Tooltip functionality', async () => { + await element.updateComplete; + const tooltip = element.shadowRoot?.querySelector('#tooltip'); + expect(tooltip).toBeDefined(); + }); + + it('should have default tooltip props', () => { + expect(element.placement).toBe('top'); + expect(element.disabled).toBe(false); + }); + }); + + 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 tooltip element', async () => { + element.fx = 'pulse'; + await element.updateComplete; + + const tooltip = element.shadowRoot?.querySelector('.tooltip'); + expect(tooltip?.classList.contains('ag-fx-pulse')).toBe(true); + }); + + it('should update fx class when fx prop changes', async () => { + element.fx = 'pulse'; + await element.updateComplete; + + let tooltip = element.shadowRoot?.querySelector('.tooltip'); + expect(tooltip?.classList.contains('ag-fx-pulse')).toBe(true); + + element.fx = 'bounce'; + await element.updateComplete; + + tooltip = element.shadowRoot?.querySelector('.tooltip'); + expect(tooltip?.classList.contains('ag-fx-pulse')).toBe(false); + expect(tooltip?.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 tooltip = element.shadowRoot?.querySelector('.tooltip'); + expect(tooltip?.classList.contains('ag-fx-pulse')).toBe(false); + expect(tooltip?.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 tooltip = element.shadowRoot?.querySelector('.tooltip'); + expect(tooltip?.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 tooltip = element.shadowRoot?.querySelector('.tooltip'); + expect(tooltip?.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 tooltip = element.shadowRoot?.querySelector('.tooltip') as HTMLElement; + const durationValue = tooltip?.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 tooltip = element.shadowRoot?.querySelector('.tooltip') as HTMLElement; + const easeValue = tooltip?.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 tooltip = element.shadowRoot?.querySelector('.tooltip') as HTMLElement; + const durationValue = tooltip?.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 tooltip = element.shadowRoot?.querySelector('.tooltip') as HTMLElement; + const easeValue = tooltip?.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 tooltip = element.shadowRoot?.querySelector('.tooltip'); + expect(tooltip?.classList.contains(`ag-fx-${effect}`)).toBe(true); + }); + }); + }); + + describe('Integration with Tooltip Props', () => { + it('should work with placement prop', async () => { + element.placement = 'bottom'; + element.fx = 'slide-in'; + await element.updateComplete; + + expect(element.placement).toBe('bottom'); + const tooltip = element.shadowRoot?.querySelector('.tooltip'); + expect(tooltip?.classList.contains('ag-fx-slide-in')).toBe(true); + }); + + it('should work with disabled prop', async () => { + element.disabled = true; + element.fx = 'pulse'; + await element.updateComplete; + + expect(element.disabled).toBe(true); + const tooltip = element.shadowRoot?.querySelector('.tooltip'); + expect(tooltip?.classList.contains('ag-fx-pulse')).toBe(true); + }); + + it('should work with content prop', async () => { + element.content = 'Updated content'; + await element.updateComplete; + expect(element.content).toBe('Updated content'); + }); + }); + + describe('Events', () => { + it('should fire show event when tooltip is shown', async () => { + await element.updateComplete; + + let showEventFired = false; + element.addEventListener('show', () => { + showEventFired = true; + }); + + element.show(); + await element.updateComplete; + + expect(showEventFired).toBe(true); + }); + + it('should fire hide event when tooltip is hidden', async () => { + await element.updateComplete; + element.show(); + await element.updateComplete; + + let hideEventFired = false; + element.addEventListener('hide', () => { + hideEventFired = true; + }); + + element.hide(); + await element.updateComplete; + + expect(hideEventFired).toBe(true); + }); + }); +}); diff --git a/v2/lib/src/components/TooltipFx/core/_TooltipFx.ts b/v2/lib/src/components/TooltipFx/core/_TooltipFx.ts new file mode 100644 index 000000000..6976142b4 --- /dev/null +++ b/v2/lib/src/components/TooltipFx/core/_TooltipFx.ts @@ -0,0 +1,270 @@ +import { css, type CSSResultGroup } from 'lit'; +import { property } from 'lit/decorators.js'; +import { Tooltip, type TooltipProps } from '../../Tooltip/core/_Tooltip.js'; +import { motionStyles } from '../../../styles/motion.styles.js'; +import type { FxProps } from '../../../types/fx.js'; + +// Combined props interface +export interface TooltipFxProps extends TooltipProps, FxProps { } + +/** + * TooltipFx - Tooltip with CSS animation effects + * + * Extends Tooltip to add optional CSS-only animation effects. + * Inherits all Tooltip functionality and styling. + * + * Features: + * - CSS-only FX effects applied to the tooltip popup element + * - Entrance effects (e.g., slide-in) complement the tooltip show/hide behavior + * - Automatic reduced-motion support + */ +export class TooltipFx extends Tooltip implements TooltipFxProps { + static override styles: CSSResultGroup = [ + motionStyles, + Tooltip.styles, + css` + /* ======================================== + FX EFFECT SETUP + Add necessary CSS properties for effects that need them + ======================================== */ + + /* Shimmer needs a gradient mask */ + .tooltip.ag-fx-shimmer { + overflow: hidden; + -webkit-mask-image: linear-gradient( + to right, + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 0.9) 40%, + rgba(0, 0, 0, 0.9) 60%, + rgba(0, 0, 0, 0) 100% + ); + mask-image: linear-gradient( + to right, + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 1) 40%, + rgba(0, 0, 0, 1) 60%, + rgba(0, 0, 0, 0) 100% + ); + -webkit-mask-size: 250% 100%; + mask-size: 250% 100%; + -webkit-mask-position: 215% 0; + mask-position: 215% 0; + } + + /* Ripple needs overflow visible to show expanding ring */ + .tooltip.ag-fx-ripple { + overflow: visible; + } + + /* Highlight Sweep needs a gradient background overlay */ + .tooltip.ag-fx-highlight-sweep { + position: relative; + overflow: hidden; + } + + .tooltip.ag-fx-highlight-sweep::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient( + 90deg, + transparent 0%, + transparent 30%, + var(--ag-fx-sweep-color, rgba(255, 255, 255, 0.3)) 50%, + transparent 70%, + transparent 100% + ); + transform: translateX(-100%); + pointer-events: none; + } + + /* ======================================== + FX EFFECT CLASSES + Applied to target element (.tooltip) + ======================================== */ + + :host([fx="bounce"]) .tooltip, + :host([fx="flip"]) .tooltip, + :host([fx="jelly"]) .tooltip { + animation-iteration-count: 1; + } + + /* Hover-triggered effects (triggered when hovering the tooltip host container) */ + :host([fx="bounce"]:hover) .tooltip { + animation: ag-fx-bounce var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="pulse"]:hover) .tooltip { + animation: ag-fx-pulse var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="jelly"]:hover) .tooltip { + animation: ag-fx-jelly var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="shimmer"]:hover) .tooltip { + animation: ag-fx-shimmer var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="glow"]:hover) .tooltip { + animation: ag-fx-glow var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="flip"]:hover) .tooltip { + animation: ag-fx-flip var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="ripple"]:hover) .tooltip { + animation: ag-fx-ripple var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + :host([fx="highlight-sweep"]:hover) .tooltip::before { + animation: ag-fx-highlight-sweep var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + /* Active/press effects */ + .tooltip.ag-fx-press-pop:active { + animation: ag-fx-press-pop var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + /* Entrance effects */ + .tooltip.ag-fx-slide-in { + animation: ag-fx-slide-in var(--ag-fx-duration, 200ms) var(--ag-fx-ease, ease); + } + + /* Disabled state */ + .tooltip.ag-fx-disabled { + animation: none !important; + } + + .tooltip.ag-fx-disabled::before { + animation: none !important; + } + + /* Reduced motion support */ + @media (prefers-reduced-motion: reduce) { + .tooltip.ag-fx-bounce, + .tooltip.ag-fx-pulse, + .tooltip.ag-fx-jelly, + .tooltip.ag-fx-shimmer, + .tooltip.ag-fx-glow, + .tooltip.ag-fx-flip, + .tooltip.ag-fx-ripple, + .tooltip.ag-fx-highlight-sweep, + .tooltip.ag-fx-press-pop, + .tooltip.ag-fx-slide-in { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } + } + ` + ]; + + // FX props + @property({ type: String, reflect: true }) + fx?: string; + + @property({ type: String, attribute: 'fx-speed', reflect: true }) + fxSpeed!: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + + @property({ type: String, attribute: 'fx-ease', reflect: true }) + fxEase!: 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce' | 'spring-sm' | 'spring-md' | 'spring-lg'; + + @property({ type: Boolean, attribute: 'fx-disabled', reflect: true }) + fxDisabled!: boolean; + + private _fxObserver: MutationObserver | null = null; + + constructor() { + super(); + this.fxSpeed = 'md'; + this.fxEase = 'ease'; + this.fxDisabled = false; + } + + override connectedCallback() { + super.connectedCallback(); + this._updateTheme(); + + this._fxObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') { + this._updateTheme(); + } + }); + }); + + this._fxObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-theme'] + }); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + if (this._fxObserver) { + this._fxObserver.disconnect(); + this._fxObserver = null; + } + } + + private _updateTheme() { + const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; + const targetEl = this.shadowRoot?.querySelector('.tooltip'); + if (targetEl instanceof HTMLElement) { + targetEl.style.setProperty('--ag-fx-shadow-opacity', isDark ? '0.8' : '0.4'); + // Tooltip background is dark (ag-neutral-900), use lighter sweep in light mode + targetEl.style.setProperty('--ag-fx-sweep-color', 'rgba(255, 255, 255, 0.3)'); + } + } + + override firstUpdated() { + super.firstUpdated(); + this._applyFxClasses(); + this._applyFxCustomProperties(); + this._updateTheme(); + } + + override updated(changedProperties: Map) { + super.updated(changedProperties); + + if (changedProperties.has('fx') || changedProperties.has('fxDisabled')) { + this._applyFxClasses(); + } + + if (changedProperties.has('fxSpeed') || changedProperties.has('fxEase')) { + this._applyFxCustomProperties(); + } + } + + private _applyFxClasses() { + const targetEl = this.shadowRoot?.querySelector('.tooltip'); + if (targetEl) { + const classesToRemove: string[] = []; + targetEl.classList.forEach((className: string) => { + if (className.startsWith('ag-fx-')) { + classesToRemove.push(className); + } + }); + classesToRemove.forEach(className => targetEl.classList.remove(className)); + + if (this.fx && !this.fxDisabled) { + targetEl.classList.add(`ag-fx-${this.fx}`); + } + + if (this.fxDisabled) { + targetEl.classList.add('ag-fx-disabled'); + } else { + targetEl.classList.remove('ag-fx-disabled'); + } + } + } + + private _applyFxCustomProperties() { + const targetEl = this.shadowRoot?.querySelector('.tooltip'); + if (targetEl instanceof HTMLElement) { + targetEl.style.setProperty('--ag-fx-duration', `var(--ag-fx-duration-${this.fxSpeed})`); + targetEl.style.setProperty('--ag-fx-ease', `var(--ag-fx-ease-${this.fxEase})`); + } + } +} diff --git a/v2/lib/src/components/TooltipFx/react/ReactTooltipFx.tsx b/v2/lib/src/components/TooltipFx/react/ReactTooltipFx.tsx new file mode 100644 index 000000000..9f470a536 --- /dev/null +++ b/v2/lib/src/components/TooltipFx/react/ReactTooltipFx.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; +import { createComponent, type EventName } from "@lit/react"; +import { TooltipFx, type TooltipFxProps } from "../core/TooltipFx"; +import type { TooltipShowEvent, TooltipHideEvent } from "../../Tooltip/core/Tooltip"; + +export interface ReactTooltipFxProps extends TooltipFxProps { + children?: React.ReactNode; + className?: string; + id?: string; +} + +export const ReactTooltipFx = createComponent({ + tagName: "ag-tooltip-fx", + elementClass: TooltipFx, + react: React, + events: { + onShow: 'show' as EventName, + onHide: 'hide' as EventName, + }, +}); + +export type { TooltipFxProps } from "../core/TooltipFx"; +export type { FxProps } from "../../../types/fx"; diff --git a/v2/lib/src/components/TooltipFx/vue/VueTooltipFx.vue b/v2/lib/src/components/TooltipFx/vue/VueTooltipFx.vue new file mode 100644 index 000000000..8979c6b97 --- /dev/null +++ b/v2/lib/src/components/TooltipFx/vue/VueTooltipFx.vue @@ -0,0 +1,95 @@ + + + diff --git a/v2/lib/src/components/TooltipFx/vue/index.ts b/v2/lib/src/components/TooltipFx/vue/index.ts new file mode 100644 index 000000000..870d1adde --- /dev/null +++ b/v2/lib/src/components/TooltipFx/vue/index.ts @@ -0,0 +1,19 @@ +import VueTooltipFx from './VueTooltipFx.vue'; +import type { TooltipProps, TooltipShowEvent, TooltipHideEvent } from '../../Tooltip/core/Tooltip'; + +export { VueTooltipFx }; + +/** Props for VueTooltipFx */ +export interface VueTooltipFxProps extends Omit { + fx?: string; + fxSpeed?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + fxEase?: 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce' | 'spring-sm' | 'spring-md' | 'spring-lg'; + fxDisabled?: boolean; +} + +export interface VueTooltipFxPropsWithEvents extends VueTooltipFxProps { + onShow?: (event: TooltipShowEvent) => void; + onHide?: (event: TooltipHideEvent) => void; +} + +export type { TooltipShowEvent, TooltipHideEvent } from '../../Tooltip/core/Tooltip'; diff --git a/v2/sdui/renderers/lit/src/AgDynamicRenderer.ts b/v2/sdui/renderers/lit/src/AgDynamicRenderer.ts index 6931ea16e..82a7f6c65 100644 --- a/v2/sdui/renderers/lit/src/AgDynamicRenderer.ts +++ b/v2/sdui/renderers/lit/src/AgDynamicRenderer.ts @@ -11,6 +11,7 @@ import 'agnosticui-core/accordion'; import 'agnosticui-core/alert'; import 'agnosticui-core/aspect-ratio'; import 'agnosticui-core/avatar'; +import 'agnosticui-core/avatar-fx'; import 'agnosticui-core/badge'; import 'agnosticui-core/badge-fx'; import 'agnosticui-core/breadcrumb'; @@ -45,8 +46,10 @@ import 'agnosticui-core/selection-card-group'; import 'agnosticui-core/spinner'; import 'agnosticui-core/tabs'; import 'agnosticui-core/tag'; +import 'agnosticui-core/tag-fx'; import 'agnosticui-core/toggle'; import 'agnosticui-core/tooltip'; +import 'agnosticui-core/tooltip-fx'; type Actions = Record void>; @@ -109,6 +112,21 @@ function renderNode( .ariaLabel=${node.ariaLabel ?? nothing} >`; + case 'AgAvatarFx': + return html``; + case 'AgBadge': return html` doDispatch(node.on_remove, actions)} >${renderChildren(node.children)}`; + case 'AgTagFx': + return html` doDispatch(node.on_remove, actions)} + >`; + case 'AgToggle': return html` doDispatch(node.on_hide, actions)} >`; + case 'AgTooltipFx': + return html` doDispatch(node.on_show, actions)} + @hide=${() => doDispatch(node.on_hide, actions)} + >`; + case 'AgText': { const _text = node.text ?? ''; switch (node.el ?? 'span') { diff --git a/v2/sdui/renderers/react/src/AgDynamicRenderer.tsx b/v2/sdui/renderers/react/src/AgDynamicRenderer.tsx index de9056f39..dc78debfe 100644 --- a/v2/sdui/renderers/react/src/AgDynamicRenderer.tsx +++ b/v2/sdui/renderers/react/src/AgDynamicRenderer.tsx @@ -8,6 +8,7 @@ import { ReactAccordion } from 'agnosticui-core/accordion/react'; import { ReactAlert } from 'agnosticui-core/alert/react'; import { ReactAspectRatio } from 'agnosticui-core/aspect-ratio/react'; import { ReactAvatar } from 'agnosticui-core/avatar/react'; +import { ReactAvatarFx } from 'agnosticui-core/avatar-fx/react'; import { ReactBadge } from 'agnosticui-core/badge/react'; import { ReactBadgeFx } from 'agnosticui-core/badge-fx/react'; import { ReactBreadcrumb } from 'agnosticui-core/breadcrumb/react'; @@ -42,8 +43,10 @@ import { ReactSelectionCardGroup } from 'agnosticui-core/selection-card-group/re import { ReactSpinner } from 'agnosticui-core/spinner/react'; import { ReactTabs } from 'agnosticui-core/tabs/react'; import { ReactTag } from 'agnosticui-core/tag/react'; +import { ReactTagFx } from 'agnosticui-core/tag-fx/react'; import { ReactToggle } from 'agnosticui-core/toggle/react'; import { ReactTooltip } from 'agnosticui-core/tooltip/react'; +import { ReactTooltipFx } from 'agnosticui-core/tooltip-fx/react'; type Actions = Record void>; @@ -128,6 +131,23 @@ function renderNode( aria-label={node.ariaLabel} /> ); + case 'AgAvatarFx': + return ( + + ); + case 'AgBadge': return ( ); + case 'AgTagFx': + return ( + dispatch(node.on_remove, actions)} /> + ); + case 'AgToggle': return ( dispatch(node.on_hide, actions)} /> ); + case 'AgTooltipFx': + return ( + dispatch(node.on_show, actions)} + onHide={() => dispatch(node.on_hide, actions)} /> + ); + case 'AgText': { const Tag = node.el ?? 'span'; return {node.text}; diff --git a/v2/sdui/renderers/vue/src/AgDynamicRenderer.ts b/v2/sdui/renderers/vue/src/AgDynamicRenderer.ts index b3e0023ac..994026de0 100644 --- a/v2/sdui/renderers/vue/src/AgDynamicRenderer.ts +++ b/v2/sdui/renderers/vue/src/AgDynamicRenderer.ts @@ -8,6 +8,7 @@ import { VueAccordion } from 'agnosticui-core/accordion/vue'; import { VueAlert } from 'agnosticui-core/alert/vue'; import { VueAspectRatio } from 'agnosticui-core/aspect-ratio/vue'; import { VueAvatar } from 'agnosticui-core/avatar/vue'; +import { VueAvatarFx } from 'agnosticui-core/avatar-fx/vue'; import { VueBadge } from 'agnosticui-core/badge/vue'; import { VueBadgeFx } from 'agnosticui-core/badge-fx/vue'; import { VueBreadcrumb } from 'agnosticui-core/breadcrumb/vue'; @@ -42,8 +43,10 @@ import { VueSelectionCardGroup } from 'agnosticui-core/selection-card-group/vue' import { VueSpinner } from 'agnosticui-core/spinner/vue'; import { VueTabs } from 'agnosticui-core/tabs/vue'; import { VueTag } from 'agnosticui-core/tag/vue'; +import { VueTagFx } from 'agnosticui-core/tag-fx/vue'; import { VueToggle } from 'agnosticui-core/toggle/vue'; import { VueTooltip } from 'agnosticui-core/tooltip/vue'; +import { VueTooltipFx } from 'agnosticui-core/tooltip-fx/vue'; type Actions = Record void>; @@ -121,6 +124,24 @@ function renderNode( }, ); + case 'AgAvatarFx': + return h( + VueAvatarFx, + { + text: node.text, + imgSrc: node.imgSrc, + imgAlt: node.imgAlt, + size: node.size, + shape: node.shape, + variant: node.variant, + ariaLabel: node.ariaLabel, + fx: node.fx, + fxSpeed: node.fxSpeed, + fxEase: node.fxEase, + fxDisabled: node.fxDisabled, + }, + ); + case 'AgBadge': return h( VueBadge, @@ -708,6 +729,22 @@ function renderNode( { default: () => renderChildren(node.children) }, ); + case 'AgTagFx': + return h( + VueTagFx, + { + variant: node.variant, + shape: node.shape, + uppercase: node.uppercase, + removable: node.removable, + fx: node.fx, + fxSpeed: node.fxSpeed, + fxEase: node.fxEase, + fxDisabled: node.fxDisabled, + onTagRemove: () => doDispatch(node.on_remove, actions), + }, + ); + case 'AgToggle': return h( VueToggle, @@ -747,6 +784,25 @@ function renderNode( }, ); + case 'AgTooltipFx': + return h( + VueTooltipFx, + { + content: node.content, + placement: node.placement, + distance: node.distance, + skidding: node.skidding, + trigger: node.trigger, + disabled: node.disabled, + fx: node.fx, + fxSpeed: node.fxSpeed, + fxEase: node.fxEase, + fxDisabled: node.fxDisabled, + onShow: () => doDispatch(node.on_show, actions), + onHide: () => doDispatch(node.on_hide, actions), + }, + ); + case 'AgText': return h(node.el ?? 'span', { key: node.id }, node.text ?? ''); diff --git a/v2/sdui/schema/agnosticui-schema.json b/v2/sdui/schema/agnosticui-schema.json index 31c81aacc..f6b684d55 100644 --- a/v2/sdui/schema/agnosticui-schema.json +++ b/v2/sdui/schema/agnosticui-schema.json @@ -171,6 +171,100 @@ ], "additionalProperties": false }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "component": { + "type": "string", + "const": "AgAvatarFx" + }, + "text": { + "type": "string" + }, + "imgSrc": { + "type": "string" + }, + "imgAlt": { + "type": "string" + }, + "size": { + "type": "string", + "enum": [ + "xs", + "sm", + "md", + "lg", + "xl" + ] + }, + "shape": { + "type": "string", + "enum": [ + "circle", + "square", + "rounded" + ] + }, + "variant": { + "type": "string", + "enum": [ + "default", + "success", + "monochrome", + "warning", + "info", + "error", + "transparent" + ] + }, + "ariaLabel": { + "type": "string" + }, + "fx": { + "type": "string" + }, + "fxSpeed": { + "type": "string", + "enum": [ + "xs", + "sm", + "md", + "lg", + "xl" + ] + }, + "fxEase": { + "type": "string", + "enum": [ + "ease", + "ease-in", + "ease-out", + "ease-in-out", + "bounce", + "spring-sm", + "spring-md", + "spring-lg" + ] + }, + "fxDisabled": { + "type": "boolean" + }, + "children": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "component" + ], + "additionalProperties": false + }, { "type": "object", "properties": { @@ -2483,6 +2577,86 @@ ], "additionalProperties": false }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "component": { + "type": "string", + "const": "AgTagFx" + }, + "variant": { + "type": "string", + "enum": [ + "primary", + "success", + "monochrome", + "warning", + "info", + "error" + ] + }, + "shape": { + "type": "string", + "enum": [ + "circle", + "pill", + "round" + ] + }, + "uppercase": { + "type": "boolean" + }, + "removable": { + "type": "boolean" + }, + "on_remove": { + "type": "string" + }, + "fx": { + "type": "string" + }, + "fxSpeed": { + "type": "string", + "enum": [ + "xs", + "sm", + "md", + "lg", + "xl" + ] + }, + "fxEase": { + "type": "string", + "enum": [ + "ease", + "ease-in", + "ease-out", + "ease-in-out", + "bounce", + "spring-sm", + "spring-md", + "spring-lg" + ] + }, + "fxDisabled": { + "type": "boolean" + }, + "children": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "component" + ], + "additionalProperties": false + }, { "type": "object", "properties": { @@ -2638,6 +2812,96 @@ ], "additionalProperties": false }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "component": { + "type": "string", + "const": "AgTooltipFx" + }, + "content": { + "type": "string" + }, + "placement": { + "type": "string", + "enum": [ + "top", + "bottom", + "right", + "left", + "top-end", + "top-start", + "bottom-end", + "bottom-start", + "right-end", + "right-start", + "left-end", + "left-start" + ] + }, + "distance": { + "type": "number" + }, + "skidding": { + "type": "number" + }, + "trigger": { + "type": "string" + }, + "disabled": { + "type": "boolean" + }, + "on_show": { + "type": "string" + }, + "on_hide": { + "type": "string" + }, + "fx": { + "type": "string" + }, + "fxSpeed": { + "type": "string", + "enum": [ + "xs", + "sm", + "md", + "lg", + "xl" + ] + }, + "fxEase": { + "type": "string", + "enum": [ + "ease", + "ease-in", + "ease-out", + "ease-in-out", + "bounce", + "spring-sm", + "spring-md", + "spring-lg" + ] + }, + "fxDisabled": { + "type": "boolean" + }, + "children": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "component" + ], + "additionalProperties": false + }, { "type": "object", "properties": { diff --git a/v2/sdui/schema/src/index.ts b/v2/sdui/schema/src/index.ts index afd40ae07..a12484d50 100644 --- a/v2/sdui/schema/src/index.ts +++ b/v2/sdui/schema/src/index.ts @@ -8,6 +8,7 @@ export type { AgAlertNode, AgAspectRatioNode, AgAvatarNode, + AgAvatarFxNode, AgBadgeNode, AgBadgeFxNode, AgBreadcrumbNode, @@ -42,8 +43,10 @@ export type { AgSpinnerNode, AgTabsNode, AgTagNode, + AgTagFxNode, AgToggleNode, AgTooltipNode, + AgTooltipFxNode, AgTextNode, AgComponentName, } from './types.js'; @@ -54,6 +57,7 @@ export { AgAlertSchema, AgAspectRatioSchema, AgAvatarSchema, + AgAvatarFxSchema, AgBadgeSchema, AgBadgeFxSchema, AgBreadcrumbSchema, @@ -88,8 +92,10 @@ export { AgSpinnerSchema, AgTabsSchema, AgTagSchema, + AgTagFxSchema, AgToggleSchema, AgTooltipSchema, + AgTooltipFxSchema, AgTextSchema, } from './schema.js'; diff --git a/v2/sdui/schema/src/schema.ts b/v2/sdui/schema/src/schema.ts index fc22d8b8c..b2345db78 100644 --- a/v2/sdui/schema/src/schema.ts +++ b/v2/sdui/schema/src/schema.ts @@ -44,6 +44,23 @@ export const AgAvatarSchema = z.object({ children: z.array(z.string()).optional(), }).strict(); +export const AgAvatarFxSchema = z.object({ + id: z.string(), + component: z.literal('AgAvatarFx'), + text: z.string().optional(), + imgSrc: z.string().optional(), + imgAlt: z.string().optional(), + size: z.enum(['xs', 'sm', 'md', 'lg', 'xl']).optional(), + shape: z.enum(['circle', 'square', 'rounded']).optional(), + variant: z.enum(['default', 'success', 'monochrome', 'warning', 'info', 'error', 'transparent']).optional(), + ariaLabel: z.string().optional(), + fx: z.string().optional(), + fxSpeed: z.enum(['xs', 'sm', 'md', 'lg', 'xl']).optional(), + fxEase: z.enum(['ease', 'ease-in', 'ease-out', 'ease-in-out', 'bounce', 'spring-sm', 'spring-md', 'spring-lg']).optional(), + fxDisabled: z.boolean().optional(), + children: z.array(z.string()).optional(), +}).strict(); + export const AgBadgeSchema = z.object({ id: z.string(), component: z.literal('AgBadge'), @@ -566,6 +583,21 @@ export const AgTagSchema = z.object({ children: z.array(z.string()).optional(), }).strict(); +export const AgTagFxSchema = z.object({ + id: z.string(), + component: z.literal('AgTagFx'), + variant: z.enum(['primary', 'success', 'monochrome', 'warning', 'info', 'error']).optional(), + shape: z.enum(['circle', 'pill', 'round']).optional(), + uppercase: z.boolean().optional(), + removable: z.boolean().optional(), + on_remove: z.string().optional(), + fx: z.string().optional(), + fxSpeed: z.enum(['xs', 'sm', 'md', 'lg', 'xl']).optional(), + fxEase: z.enum(['ease', 'ease-in', 'ease-out', 'ease-in-out', 'bounce', 'spring-sm', 'spring-md', 'spring-lg']).optional(), + fxDisabled: z.boolean().optional(), + children: z.array(z.string()).optional(), +}).strict(); + export const AgToggleSchema = z.object({ id: z.string(), component: z.literal('AgToggle'), @@ -603,6 +635,24 @@ export const AgTooltipSchema = z.object({ children: z.array(z.string()).optional(), }).strict(); +export const AgTooltipFxSchema = z.object({ + id: z.string(), + component: z.literal('AgTooltipFx'), + content: z.string().optional(), + placement: z.enum(['top', 'bottom', 'right', 'left', 'top-end', 'top-start', 'bottom-end', 'bottom-start', 'right-end', 'right-start', 'left-end', 'left-start']).optional(), + distance: z.number().optional(), + skidding: z.number().optional(), + trigger: z.string().optional(), + disabled: z.boolean().optional(), + on_show: z.string().optional(), + on_hide: z.string().optional(), + fx: z.string().optional(), + fxSpeed: z.enum(['xs', 'sm', 'md', 'lg', 'xl']).optional(), + fxEase: z.enum(['ease', 'ease-in', 'ease-out', 'ease-in-out', 'bounce', 'spring-sm', 'spring-md', 'spring-lg']).optional(), + fxDisabled: z.boolean().optional(), + children: z.array(z.string()).optional(), +}).strict(); + export const AgTextSchema = z.object({ id: z.string(), component: z.literal('AgText'), @@ -616,6 +666,7 @@ export const AgNodeSchema = z.discriminatedUnion('component', [ AgAlertSchema, AgAspectRatioSchema, AgAvatarSchema, + AgAvatarFxSchema, AgBadgeSchema, AgBadgeFxSchema, AgBreadcrumbSchema, @@ -650,7 +701,9 @@ export const AgNodeSchema = z.discriminatedUnion('component', [ AgSpinnerSchema, AgTabsSchema, AgTagSchema, + AgTagFxSchema, AgToggleSchema, AgTooltipSchema, + AgTooltipFxSchema, AgTextSchema, ]); diff --git a/v2/sdui/schema/src/types.ts b/v2/sdui/schema/src/types.ts index e33b095ae..9e1b94182 100644 --- a/v2/sdui/schema/src/types.ts +++ b/v2/sdui/schema/src/types.ts @@ -42,6 +42,23 @@ export interface AgAvatarNode { children?: string[]; } +export interface AgAvatarFxNode { + id: string; + component: 'AgAvatarFx'; + text?: string; + imgSrc?: string; + imgAlt?: string; + size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + shape?: 'circle' | 'square' | 'rounded'; + variant?: 'default' | 'success' | 'monochrome' | 'warning' | 'info' | 'error' | 'transparent'; + ariaLabel?: string; + fx?: string; + fxSpeed?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + fxEase?: 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce' | 'spring-sm' | 'spring-md' | 'spring-lg'; + fxDisabled?: boolean; + children?: string[]; +} + export interface AgBadgeNode { id: string; component: 'AgBadge'; @@ -564,6 +581,21 @@ export interface AgTagNode { children?: string[]; } +export interface AgTagFxNode { + id: string; + component: 'AgTagFx'; + variant?: 'primary' | 'success' | 'monochrome' | 'warning' | 'info' | 'error'; + shape?: 'circle' | 'pill' | 'round'; + uppercase?: boolean; + removable?: boolean; + on_remove?: string; + fx?: string; + fxSpeed?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + fxEase?: 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce' | 'spring-sm' | 'spring-md' | 'spring-lg'; + fxDisabled?: boolean; + children?: string[]; +} + export interface AgToggleNode { id: string; component: 'AgToggle'; @@ -600,6 +632,24 @@ export interface AgTooltipNode { on_hide?: string; children?: string[]; } + +export interface AgTooltipFxNode { + id: string; + component: 'AgTooltipFx'; + content?: string; + placement?: 'top' | 'bottom' | 'right' | 'left' | 'top-end' | 'top-start' | 'bottom-end' | 'bottom-start' | 'right-end' | 'right-start' | 'left-end' | 'left-start'; + distance?: number; + skidding?: number; + trigger?: string; + disabled?: boolean; + on_show?: string; + on_hide?: string; + fx?: string; + fxSpeed?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + fxEase?: 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce' | 'spring-sm' | 'spring-md' | 'spring-lg'; + fxDisabled?: boolean; + children?: string[]; +} export interface AgTextNode { id: string; component: 'AgText'; @@ -613,6 +663,7 @@ export type AgNode = | AgAlertNode | AgAspectRatioNode | AgAvatarNode + | AgAvatarFxNode | AgBadgeNode | AgBadgeFxNode | AgBreadcrumbNode @@ -647,8 +698,10 @@ export type AgNode = | AgSpinnerNode | AgTabsNode | AgTagNode + | AgTagFxNode | AgToggleNode | AgTooltipNode + | AgTooltipFxNode | AgTextNode; export type AgComponentName = AgNode['component']; diff --git a/v2/site/docs/.vitepress/config.mts b/v2/site/docs/.vitepress/config.mts index 6890d6b04..ecfeb38ad 100644 --- a/v2/site/docs/.vitepress/config.mts +++ b/v2/site/docs/.vitepress/config.mts @@ -126,6 +126,7 @@ function getComponents() { { text: 'Alerts', link: '/components/alert' }, { text: 'AspectRatio', link: '/components/aspect-ratio' }, { text: 'Avatar', link: '/components/avatar' }, + { text: 'AvatarFx (Lab)', link: '/components/avatar-fx' }, { text: 'Badge', link: '/components/badge' }, { text: 'BadgeFx (Lab)', link: '/components/badge-fx' }, { text: 'Breadcrumb', link: '/components/breadcrumb' }, @@ -173,10 +174,12 @@ function getComponents() { { text: 'Table', link: '/components/table' }, { text: 'Tabs', link: '/components/tabs' }, { text: 'Tag', link: '/components/tag' }, + { text: 'TagFx (Lab)', link: '/components/tag-fx' }, { text: 'Timeline', link: '/components/timeline' }, { text: 'Toast', link: '/components/toast' }, { text: 'Toggle', link: '/components/toggle' }, { text: 'Tooltip', link: '/components/tooltip' }, + { text: 'TooltipFx (Lab)', link: '/components/tooltip-fx' }, { text: 'VisuallyHidden', link: '/components/visually-hidden' }, ] } diff --git a/v2/site/docs/components/avatar-fx.md b/v2/site/docs/components/avatar-fx.md new file mode 100644 index 000000000..d49a7d782 --- /dev/null +++ b/v2/site/docs/components/avatar-fx.md @@ -0,0 +1,110 @@ +# AvatarFx + + + +AvatarFx extends the core Avatar component with a handful of CSS-only micro-interaction effects. + +::: info Opt-in Component +AvatarFx adds a few hundred lines of CSS for animation effects. It's ideal for marketing sites, landing pages, or when visual polish is a priority. +::: + +## Examples + + + + + + + + +## Usage + +::: tip +The framework examples below `import` AgnosticUI as an `npm` package. Alternatively, you can use the **CLI for complete control, AI/LLM visibility, and full code ownership**: +```bash +npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc. +npx ag add AvatarFx +``` +The CLI copies source code directly into your project, giving you full visibility and control. After running `npx ag add`, you'll receive exact import instructions. +::: + +::: details Vue + +```vue + + + +``` + +::: + +::: details React + +```tsx +import { ReactAvatarFx } from "agnosticui-core/avatar-fx/react"; + +function App() { + return ; +} +``` + +::: + +::: details Lit + +```html + + + +``` + +::: + +## Props + +Inherits all [Avatar Props](./avatar.md#props) plus: + +| Prop | Type | Default | Description | +| ------------ | ------------------------------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------ | +| `fx` | `string` | `undefined` | Effect name: `bounce`, `pulse`, `jelly`, `shimmer`, `glow`, `flip`, `ripple`, `highlight-sweep`, `press-pop`, `slide-in` | +| `fxSpeed` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Animation duration preset | +| `fxEase` | `'ease' \| 'bounce' \| 'spring-sm' \| ...` | `'ease'` | Easing function preset | +| `fxDisabled` | `boolean` | `false` | Disable effects programmatically | + +## Effects Library + +| FX Name | Trigger | Description | +| ----------------- | ------- | ------------------------------ | +| `bounce` | hover | Vertical pop, light spring | +| `pulse` | hover | Scale grow/shrink animation | +| `jelly` | hover | Squash/stretch elastic wobble | +| `shimmer` | hover | Light sweep across surface | +| `glow` | hover | Box-shadow pulse effect | +| `flip` | hover | Y-axis rotation (3D flip) | +| `ripple` | hover | Center-origin radial expansion | +| `highlight-sweep` | hover | Horizontal highlight sweep | +| `press-pop` | active | Quick press down/up | +| `slide-in` | mount | Entrance animation from below | diff --git a/v2/site/docs/components/tag-fx.md b/v2/site/docs/components/tag-fx.md new file mode 100644 index 000000000..bd3261a92 --- /dev/null +++ b/v2/site/docs/components/tag-fx.md @@ -0,0 +1,114 @@ +# TagFx + + + +TagFx extends the core Tag component with a handful of CSS-only micro-interaction effects. + +::: info Opt-in Component +TagFx adds a few hundred lines of CSS for animation effects. It's ideal for marketing sites, landing pages, or when visual polish is a priority. +::: + +## Examples + + + + + + + + +## Usage + +::: tip +The framework examples below `import` AgnosticUI as an `npm` package. Alternatively, you can use the **CLI for complete control, AI/LLM visibility, and full code ownership**: +```bash +npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc. +npx ag add TagFx +``` +The CLI copies source code directly into your project, giving you full visibility and control. After running `npx ag add`, you'll receive exact import instructions. +::: + +::: details Vue + +```vue + + + +``` + +::: + +::: details React + +```tsx +import { ReactTagFx } from "agnosticui-core/tag-fx/react"; + +function App() { + return ( + + Tag Text + + ); +} +``` + +::: + +::: details Lit + +```html + + + Tag Text +``` + +::: + +## Props + +Inherits all [Tag Props](./tag.md#props) plus: + +| Prop | Type | Default | Description | +| ------------ | ------------------------------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------ | +| `fx` | `string` | `undefined` | Effect name: `bounce`, `pulse`, `jelly`, `shimmer`, `glow`, `flip`, `ripple`, `highlight-sweep`, `press-pop`, `slide-in` | +| `fxSpeed` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Animation duration preset | +| `fxEase` | `'ease' \| 'bounce' \| 'spring-sm' \| ...` | `'ease'` | Easing function preset | +| `fxDisabled` | `boolean` | `false` | Disable effects programmatically | + +## Effects Library + +| FX Name | Trigger | Description | +| ----------------- | ------- | ------------------------------ | +| `bounce` | hover | Vertical pop, light spring | +| `pulse` | hover | Scale grow/shrink animation | +| `jelly` | hover | Squash/stretch elastic wobble | +| `shimmer` | hover | Light sweep across surface | +| `glow` | hover | Box-shadow pulse effect | +| `flip` | hover | Y-axis rotation (3D flip) | +| `ripple` | hover | Center-origin radial expansion | +| `highlight-sweep` | hover | Horizontal highlight sweep | +| `press-pop` | active | Quick press down/up | +| `slide-in` | mount | Entrance animation from below | diff --git a/v2/site/docs/components/tooltip-fx.md b/v2/site/docs/components/tooltip-fx.md new file mode 100644 index 000000000..73539c31b --- /dev/null +++ b/v2/site/docs/components/tooltip-fx.md @@ -0,0 +1,118 @@ +# TooltipFx + + + +TooltipFx extends the core Tooltip component with a handful of CSS-only micro-interaction effects applied to the tooltip popup. + +::: info Opt-in Component +TooltipFx adds a few hundred lines of CSS for animation effects. It's ideal for marketing sites, landing pages, or when visual polish is a priority. +::: + +## Examples + + + + + + + + +## Usage + +::: tip +The framework examples below `import` AgnosticUI as an `npm` package. Alternatively, you can use the **CLI for complete control, AI/LLM visibility, and full code ownership**: +```bash +npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc. +npx ag add TooltipFx +``` +The CLI copies source code directly into your project, giving you full visibility and control. After running `npx ag add`, you'll receive exact import instructions. +::: + +::: details Vue + +```vue + + + +``` + +::: + +::: details React + +```tsx +import { ReactTooltipFx } from "agnosticui-core/tooltip-fx/react"; + +function App() { + return ( + + + + ); +} +``` + +::: + +::: details Lit + +```html + + + + + +``` + +::: + +## Props + +Inherits all [Tooltip Props](./tooltip.md#props) plus: + +| Prop | Type | Default | Description | +| ------------ | ------------------------------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------ | +| `fx` | `string` | `undefined` | Effect name: `bounce`, `pulse`, `jelly`, `shimmer`, `glow`, `flip`, `ripple`, `highlight-sweep`, `press-pop`, `slide-in` | +| `fxSpeed` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Animation duration preset | +| `fxEase` | `'ease' \| 'bounce' \| 'spring-sm' \| ...` | `'ease'` | Easing function preset | +| `fxDisabled` | `boolean` | `false` | Disable effects programmatically | + +## Effects Library + +| FX Name | Trigger | Description | +| ----------------- | ------- | ------------------------------ | +| `bounce` | hover | Vertical pop, light spring | +| `pulse` | hover | Scale grow/shrink animation | +| `jelly` | hover | Squash/stretch elastic wobble | +| `shimmer` | hover | Light sweep across surface | +| `glow` | hover | Box-shadow pulse effect | +| `flip` | hover | Y-axis rotation (3D flip) | +| `ripple` | hover | Center-origin radial expansion | +| `highlight-sweep` | hover | Horizontal highlight sweep | +| `press-pop` | active | Quick press down/up | +| `slide-in` | mount | Entrance animation from below | diff --git a/v2/site/docs/examples/AvatarFxExamples.vue b/v2/site/docs/examples/AvatarFxExamples.vue new file mode 100644 index 000000000..44e590045 --- /dev/null +++ b/v2/site/docs/examples/AvatarFxExamples.vue @@ -0,0 +1,17 @@ + + + diff --git a/v2/site/docs/examples/AvatarFxLitExamples.js b/v2/site/docs/examples/AvatarFxLitExamples.js new file mode 100644 index 000000000..e153e307e --- /dev/null +++ b/v2/site/docs/examples/AvatarFxLitExamples.js @@ -0,0 +1,27 @@ +import { LitElement, html } from 'lit'; +import 'agnosticui-core/avatar-fx'; + +export class AvatarFxLitExamples extends LitElement { + // Render in light DOM to access global utility classes + createRenderRoot() { + return this; + } + + render() { + return html` +
+ + + + + + + + + +
+ `; + } +} + +customElements.define('avatarfx-lit-examples', AvatarFxLitExamples); diff --git a/v2/site/docs/examples/AvatarFxReactExamples.jsx b/v2/site/docs/examples/AvatarFxReactExamples.jsx new file mode 100644 index 000000000..9323067cb --- /dev/null +++ b/v2/site/docs/examples/AvatarFxReactExamples.jsx @@ -0,0 +1,17 @@ +import { ReactAvatarFx } from "agnosticui-core/avatar-fx/react"; + +export default function AvatarFxReactExamples() { + return ( +
+ + + + + + + + + +
+ ); +} diff --git a/v2/site/docs/examples/TagFxExamples.vue b/v2/site/docs/examples/TagFxExamples.vue new file mode 100644 index 000000000..b5dadfcf0 --- /dev/null +++ b/v2/site/docs/examples/TagFxExamples.vue @@ -0,0 +1,17 @@ + + + diff --git a/v2/site/docs/examples/TagFxLitExamples.js b/v2/site/docs/examples/TagFxLitExamples.js new file mode 100644 index 000000000..fbb74a8a1 --- /dev/null +++ b/v2/site/docs/examples/TagFxLitExamples.js @@ -0,0 +1,27 @@ +import { LitElement, html } from 'lit'; +import 'agnosticui-core/tag-fx'; + +export class TagFxLitExamples extends LitElement { + // Render in light DOM to access global utility classes + createRenderRoot() { + return this; + } + + render() { + return html` +
+ Bounce + Pulse + Jelly + Shimmer + Glow + Flip + Ripple + Sweep + Press Pop +
+ `; + } +} + +customElements.define('tagfx-lit-examples', TagFxLitExamples); diff --git a/v2/site/docs/examples/TagFxReactExamples.jsx b/v2/site/docs/examples/TagFxReactExamples.jsx new file mode 100644 index 000000000..8b84fdff2 --- /dev/null +++ b/v2/site/docs/examples/TagFxReactExamples.jsx @@ -0,0 +1,17 @@ +import { ReactTagFx } from "agnosticui-core/tag-fx/react"; + +export default function TagFxReactExamples() { + return ( +
+ Bounce + Pulse + Jelly + Shimmer + Glow + Flip + Ripple + Sweep + Press Pop +
+ ); +} diff --git a/v2/site/docs/examples/TooltipFxExamples.vue b/v2/site/docs/examples/TooltipFxExamples.vue new file mode 100644 index 000000000..9be66b98d --- /dev/null +++ b/v2/site/docs/examples/TooltipFxExamples.vue @@ -0,0 +1,33 @@ + + + diff --git a/v2/site/docs/examples/TooltipFxLitExamples.js b/v2/site/docs/examples/TooltipFxLitExamples.js new file mode 100644 index 000000000..b1e79dcfb --- /dev/null +++ b/v2/site/docs/examples/TooltipFxLitExamples.js @@ -0,0 +1,43 @@ +import { LitElement, html } from 'lit'; +import 'agnosticui-core/tooltip-fx'; +import 'agnosticui-core/button'; + +export class TooltipFxLitExamples extends LitElement { + // Render in light DOM to access global utility classes + createRenderRoot() { + return this; + } + + render() { + return html` +
+ + Bounce + + + Pulse + + + Jelly + + + Shimmer + + + Glow + + + Flip + + + Ripple + + + Sweep + +
+ `; + } +} + +customElements.define('tooltipfx-lit-examples', TooltipFxLitExamples); diff --git a/v2/site/docs/examples/TooltipFxReactExamples.jsx b/v2/site/docs/examples/TooltipFxReactExamples.jsx new file mode 100644 index 000000000..c6ee274ec --- /dev/null +++ b/v2/site/docs/examples/TooltipFxReactExamples.jsx @@ -0,0 +1,33 @@ +import { ReactTooltipFx } from "agnosticui-core/tooltip-fx/react"; +import { ReactButton } from "agnosticui-core/button/react"; + +export default function TooltipFxReactExamples() { + return ( +
+ + Bounce + + + Pulse + + + Jelly + + + Shimmer + + + Glow + + + Flip + + + Ripple + + + Sweep + +
+ ); +} diff --git a/v2/site/package-lock.json b/v2/site/package-lock.json index a42b6c689..61e122ba0 100644 --- a/v2/site/package-lock.json +++ b/v2/site/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@stackblitz/sdk": "^1.11.0", - "agnosticui-core": "file:../lib/agnosticui-core-2.0.0-alpha.24.tgz", + "agnosticui-core": "file:../lib/agnosticui-core-2.0.0-alpha.25.tgz", "lucide-vue-next": "^0.545.0", "markdown-it-table": "^4.1.1", "shiki": "^3.20.0" @@ -610,6 +610,49 @@ } } }, + "node_modules/@docsearch/js/node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@docsearch/js/node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/@docsearch/js/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -1978,9 +2021,9 @@ } }, "node_modules/agnosticui-core": { - "version": "2.0.0-alpha.24", - "resolved": "file:../lib/agnosticui-core-2.0.0-alpha.24.tgz", - "integrity": "sha512-yGJMifITu8jZKBSijWVJQvwlfujZEL5LwiRNGACNC2q+K2Vr6PPXWJNB8prGXGLX0+trdopFl2SpdQxUS0p9LA==", + "version": "2.0.0-alpha.25", + "resolved": "file:../lib/agnosticui-core-2.0.0-alpha.25.tgz", + "integrity": "sha512-X49Oj9Aa3BU/JDjas0qNwTyVVks47RNLrspeCsHIgSiz9ZaO5P2EGI9CNx9Du3NZpzndLi1ZhCrouk7t+Hv2Lw==", "license": "Apache-2.0", "dependencies": { "@floating-ui/dom": "^1.7.4", @@ -2524,6 +2567,21 @@ "license": "MIT", "peer": true }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", diff --git a/v2/site/package.json b/v2/site/package.json index b6f9e648a..77e0c4173 100644 --- a/v2/site/package.json +++ b/v2/site/package.json @@ -8,7 +8,7 @@ "scripts": { "sync-tokens": "npm run --prefix ../theme-registry rebuild && cp ../theme-registry/dist/ag-tokens.css ./docs/public/ag-tokens.css && cp ../theme-registry/dist/ag-tokens-dark.css ./docs/public/ag-tokens-dark.css && cp ../lib/src/styles/table.css ./docs/public/table.css && cp ../skins/skins-bundle.css ./docs/public/skins-bundle.css", "clear:cache": "rm -rf docs/.vitepress/cache", - "reinstall:lib": "npm run clear:cache && npm i ../lib/agnosticui-core-2.0.0-alpha.24.tgz && npm run sync-tokens", + "reinstall:lib": "npm run clear:cache && npm i ../lib/agnosticui-core-2.0.0-alpha.25.tgz && npm run sync-tokens", "docs:dev": "vitepress dev docs --port 8000", "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs" @@ -22,7 +22,7 @@ }, "dependencies": { "@stackblitz/sdk": "^1.11.0", - "agnosticui-core": "file:../lib/agnosticui-core-2.0.0-alpha.24.tgz", + "agnosticui-core": "file:../lib/agnosticui-core-2.0.0-alpha.25.tgz", "lucide-vue-next": "^0.545.0", "markdown-it-table": "^4.1.1", "shiki": "^3.20.0"