Add comprehensive component library and demo application
Added extensive component library with feedback components (empty state, loading spinner, skeleton loader), enhanced form components (autocomplete, date picker, file upload, form field, time picker), navigation components (pagination), and overlay components (backdrop, drawer, modal, overlay container). Updated demo application with comprehensive showcase components and enhanced styling throughout the project. Excluded font files from repository to reduce size. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
11
.gitignore
vendored
11
.gitignore
vendored
@@ -40,3 +40,14 @@ testem.log
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
dist/
|
||||
.angular/
|
||||
|
||||
# Font files and directories
|
||||
*.woff
|
||||
*.woff2
|
||||
*.eot
|
||||
*.ttf
|
||||
*.otf
|
||||
assets/fonts/
|
||||
projects/demo-ui-essentials/public/fonts/
|
||||
|
||||
50
.gitignore copy
Normal file
50
.gitignore copy
Normal file
@@ -0,0 +1,50 @@
|
||||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Font files - exclude large binary font assets
|
||||
/assets/fonts/
|
||||
*.ttf
|
||||
*.otf
|
||||
*.woff
|
||||
*.woff2
|
||||
*.eot
|
||||
56
CLAUDE.md
56
CLAUDE.md
@@ -4,11 +4,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Overview
|
||||
|
||||
SSuite is an Angular workspace containing three libraries designed as a modular component system:
|
||||
SSuite is an Angular workspace containing three libraries and a demo application:
|
||||
|
||||
- **shared-ui**: UI design system with comprehensive SCSS styling, design tokens, and reusable components
|
||||
- **shared-utils**: Common utilities and shared services
|
||||
- **ui-essentials**: Essential UI components and functionality
|
||||
- **shared-utils**: Common utilities and shared services
|
||||
- **ui-essentials**: Essential UI components with comprehensive component library (buttons, forms, data-display, navigation, media, feedback, overlays, layout)
|
||||
- **demo-ui-essentials**: Demo application showcasing all components with live examples
|
||||
|
||||
## Development Commands
|
||||
|
||||
@@ -39,17 +40,29 @@ ng test ui-essentials
|
||||
|
||||
### Development Server
|
||||
```bash
|
||||
# Start development server (if main application exists)
|
||||
ng serve
|
||||
# Start demo application server
|
||||
ng serve demo-ui-essentials
|
||||
|
||||
# Alternative start command
|
||||
npm start
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Workspace Structure
|
||||
```
|
||||
projects/
|
||||
├── shared-ui/ - Design system and SCSS architecture
|
||||
├── shared-utils/ - Common utilities and services
|
||||
├── ui-essentials/ - Component library with 8 major categories
|
||||
└── demo-ui-essentials/ - Demo application with component examples
|
||||
```
|
||||
|
||||
### Library Structure
|
||||
Each library follows Angular's standard structure:
|
||||
- `src/lib/` - Library source code
|
||||
- `src/public-api.ts` - Public exports
|
||||
- `ng-package.json` - Library packaging configuration
|
||||
- `ng-package.json` - Library packaging configuration
|
||||
- `package.json` - Library metadata
|
||||
|
||||
### TypeScript Path Mapping
|
||||
@@ -62,6 +75,17 @@ Libraries are mapped in `tsconfig.json` paths:
|
||||
}
|
||||
```
|
||||
|
||||
### Component Categories (ui-essentials)
|
||||
The component library is organized into these categories:
|
||||
- **buttons**: Button variants and actions
|
||||
- **forms**: Input, checkbox, radio, search, switch, autocomplete, date/time pickers, file upload, form fields
|
||||
- **data-display**: Tables, lists, cards, chips, badges, avatars
|
||||
- **navigation**: App bars, menus, pagination
|
||||
- **media**: Image containers, carousels, video players
|
||||
- **feedback**: Progress indicators, status badges, theme switchers, empty states, loading spinners, skeleton loaders
|
||||
- **overlays**: Modals, drawers, backdrops, overlay containers
|
||||
- **layout**: Containers, spacers, grid systems
|
||||
|
||||
### Design System (shared-ui)
|
||||
Contains a comprehensive SCSS architecture:
|
||||
- **Base tokens**: Colors, typography, spacing, motion, shadows
|
||||
@@ -95,10 +119,22 @@ Entry points:
|
||||
- Tokens: `projects/shared-ui/src/styles/tokens.scss`
|
||||
- Semantic: `projects/shared-ui/src/styles/semantic/index.scss`
|
||||
|
||||
## Development Notes
|
||||
## Development Workflow
|
||||
|
||||
- All libraries use Angular 19.2+ with strict TypeScript configuration
|
||||
### Component Development
|
||||
1. **New components**: Add to appropriate category in `ui-essentials/src/lib/components/`
|
||||
2. **Export new components**: Update category `index.ts` files
|
||||
3. **Demo components**: Create matching demo in `demo-ui-essentials/src/app/demos/`
|
||||
4. **Routing**: Add demo routes to `demos.routes.ts`
|
||||
|
||||
### Build Dependencies
|
||||
- Libraries must be built before being consumed by other projects
|
||||
- Use `ng generate` commands within library contexts for scaffolding
|
||||
- Demo application depends on built library artifacts in `dist/`
|
||||
- Use watch mode (`ng build --watch`) for development iterations
|
||||
|
||||
### Library Development
|
||||
- All libraries use Angular 19.2+ with strict TypeScript configuration
|
||||
- Use `ng generate` commands within library contexts for scaffolding
|
||||
- Libraries are publishable to npm after building to `dist/` directory
|
||||
- Testing uses Karma with Jasmine framework
|
||||
- Testing uses Karma with Jasmine framework
|
||||
- FontAwesome icons integrated via `@fortawesome/angular-fontawesome`
|
||||
@@ -1,15 +1,19 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { ButtonComponent } from '../../../ui-essentials/src/public-api';
|
||||
import { FabComponent } from "../../../ui-essentials/src/lib/components/buttons/fab.component";
|
||||
import { DashboardComponent } from "./features/dashboard/dashboard.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet, ButtonComponent],
|
||||
imports: [RouterOutlet, ButtonComponent, FabComponent, DashboardComponent],
|
||||
template: `
|
||||
|
||||
<ui-button></ui-button>
|
||||
<skyui-dashboard></skyui-dashboard>
|
||||
|
||||
`
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'demo-ui-essentials';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,588 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { AppbarComponent } from '../../../../../ui-essentials/src/lib/components/navigation/appbar';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import {
|
||||
faBars,
|
||||
faSearch,
|
||||
faUser,
|
||||
faHeart,
|
||||
faBell,
|
||||
faCog,
|
||||
faShoppingCart,
|
||||
faHome,
|
||||
faArrowLeft,
|
||||
faPlus,
|
||||
faShare,
|
||||
faEllipsisV,
|
||||
faSun,
|
||||
faMoon
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-appbar-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
AppbarComponent,
|
||||
FontAwesomeModule
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Appbar Component Showcase</h2>
|
||||
|
||||
<!-- Appbar Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Appbar Variants</h3>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Compact</h4>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; margin-bottom: 1rem;">
|
||||
<ui-appbar
|
||||
variant="compact"
|
||||
[slots]="{title: true, leftIcon: true, rightIcon: true}"
|
||||
[elevated]="false">
|
||||
<fa-icon [icon]="faBars" slot="left-icon"></fa-icon>
|
||||
<span slot="title">Compact Appbar</span>
|
||||
<fa-icon [icon]="faSearch" slot="right-icon"></fa-icon>
|
||||
</ui-appbar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Standard</h4>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; margin-bottom: 1rem;">
|
||||
<ui-appbar
|
||||
variant="standard"
|
||||
[slots]="{title: true, leftIcon: true, rightAvatar: true}"
|
||||
[elevated]="false">
|
||||
<fa-icon [icon]="faBars" slot="left-icon"></fa-icon>
|
||||
<span slot="title">Standard Appbar</span>
|
||||
<div slot="right-avatar" style="width: 32px; height: 32px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-weight: 500;">
|
||||
JS
|
||||
</div>
|
||||
</ui-appbar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Large</h4>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; margin-bottom: 1rem;">
|
||||
<ui-appbar
|
||||
variant="large"
|
||||
[slots]="{title: true, leftIcon: true, rightMenu: true}"
|
||||
[elevated]="false">
|
||||
<fa-icon [icon]="faArrowLeft" slot="left-icon"></fa-icon>
|
||||
<span slot="title">Large Appbar with More Space</span>
|
||||
<div slot="right-menu" style="display: flex; gap: 1rem;">
|
||||
<fa-icon [icon]="faHeart" style="cursor: pointer;"></fa-icon>
|
||||
<fa-icon [icon]="faShare" style="cursor: pointer;"></fa-icon>
|
||||
<fa-icon [icon]="faEllipsisV" style="cursor: pointer;"></fa-icon>
|
||||
</div>
|
||||
</ui-appbar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Prominent</h4>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; margin-bottom: 1rem;">
|
||||
<ui-appbar
|
||||
variant="prominent"
|
||||
[slots]="{title: true, leftLogo: true, rightIcon: true}"
|
||||
[elevated]="false">
|
||||
<div slot="left-logo" style="display: flex; align-items: center; gap: 0.5rem; font-weight: 600; color: #007bff;">
|
||||
<div style="width: 24px; height: 24px; background: linear-gradient(135deg, #007bff, #0056b3); border-radius: 4px;"></div>
|
||||
SkyUI
|
||||
</div>
|
||||
<span slot="title">Prominent Design System</span>
|
||||
<fa-icon [icon]="faCog" slot="right-icon"></fa-icon>
|
||||
</ui-appbar>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Elevation and Position -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Elevation & Position</h3>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Elevated</h4>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; margin-bottom: 1rem;">
|
||||
<ui-appbar
|
||||
variant="standard"
|
||||
[slots]="{title: true, leftIcon: true, rightIcon: true}"
|
||||
[elevated]="true">
|
||||
<fa-icon [icon]="faBars" slot="left-icon"></fa-icon>
|
||||
<span slot="title">Elevated Appbar</span>
|
||||
<fa-icon [icon]="faBell" slot="right-icon"></fa-icon>
|
||||
</ui-appbar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Not Elevated</h4>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; margin-bottom: 1rem;">
|
||||
<ui-appbar
|
||||
variant="standard"
|
||||
[slots]="{title: true, leftIcon: true, rightIcon: true}"
|
||||
[elevated]="false">
|
||||
<fa-icon [icon]="faBars" slot="left-icon"></fa-icon>
|
||||
<span slot="title">Flat Appbar</span>
|
||||
<fa-icon [icon]="faBell" slot="right-icon"></fa-icon>
|
||||
</ui-appbar>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Slot Configurations -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Slot Configurations</h3>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Left Icon + Title + Right Avatar</h4>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; margin-bottom: 1rem;">
|
||||
<ui-appbar
|
||||
variant="standard"
|
||||
[slots]="{title: true, leftIcon: true, rightAvatar: true}">
|
||||
<fa-icon [icon]="faHome" slot="left-icon"></fa-icon>
|
||||
<span slot="title">Home</span>
|
||||
<img
|
||||
slot="right-avatar"
|
||||
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=32&h=32&fit=crop&crop=face"
|
||||
style="width: 32px; height: 32px; border-radius: 50%; object-fit: cover;"
|
||||
alt="User avatar"
|
||||
(error)="handleImageError($event)">
|
||||
</ui-appbar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Left Logo + Title + Right Menu</h4>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; margin-bottom: 1rem;">
|
||||
<ui-appbar
|
||||
variant="standard"
|
||||
[slots]="{title: true, leftLogo: true, rightMenu: true}">
|
||||
<div slot="left-logo" style="display: flex; align-items: center; gap: 0.5rem; font-weight: 600;">
|
||||
<div style="width: 20px; height: 20px; background: linear-gradient(45deg, #ff6b6b, #ee5a24); border-radius: 50%;"></div>
|
||||
App
|
||||
</div>
|
||||
<span slot="title">Dashboard</span>
|
||||
<div slot="right-menu" style="display: flex; gap: 1rem; align-items: center;">
|
||||
<fa-icon [icon]="faShoppingCart" style="cursor: pointer;" (click)="handleAction('cart')"></fa-icon>
|
||||
<fa-icon [icon]="faBell" style="cursor: pointer;" (click)="handleAction('notifications')"></fa-icon>
|
||||
<fa-icon [icon]="faUser" style="cursor: pointer;" (click)="handleAction('profile')"></fa-icon>
|
||||
</div>
|
||||
</ui-appbar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Left Avatar + Title + Right Icon</h4>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; margin-bottom: 1rem;">
|
||||
<ui-appbar
|
||||
variant="standard"
|
||||
[slots]="{title: true, leftAvatar: true, rightIcon: true}">
|
||||
<div slot="left-avatar" style="width: 32px; height: 32px; border-radius: 50%; background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); display: flex; align-items: center; justify-content: center; color: #333; font-weight: 500; font-size: 12px;">
|
||||
UI
|
||||
</div>
|
||||
<span slot="title">Profile Settings</span>
|
||||
<button
|
||||
slot="right-icon"
|
||||
style="background: none; border: none; padding: 8px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center;"
|
||||
(click)="toggleTheme()">
|
||||
<fa-icon [icon]="isDarkMode() ? faSun : faMoon"></fa-icon>
|
||||
</button>
|
||||
</ui-appbar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Multiple Right Elements</h4>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; margin-bottom: 1rem;">
|
||||
<ui-appbar
|
||||
variant="standard"
|
||||
[slots]="{title: true, leftIcon: true, rightIcon: true, rightMenu: true}">
|
||||
<fa-icon [icon]="faBars" slot="left-icon"></fa-icon>
|
||||
<span slot="title">Multi-Action Bar</span>
|
||||
<fa-icon [icon]="faPlus" slot="right-icon" style="cursor: pointer;" (click)="handleAction('add')"></fa-icon>
|
||||
<div slot="right-menu" style="display: flex; gap: 0.5rem;">
|
||||
<fa-icon [icon]="faSearch" style="cursor: pointer;" (click)="handleAction('search')"></fa-icon>
|
||||
<fa-icon [icon]="faEllipsisV" style="cursor: pointer;" (click)="handleAction('more')"></fa-icon>
|
||||
</div>
|
||||
</ui-appbar>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Real-world Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Real-world Examples</h3>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>E-commerce App</h4>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; margin-bottom: 1rem;">
|
||||
<ui-appbar
|
||||
variant="standard"
|
||||
[slots]="{title: true, leftIcon: true, rightMenu: true}"
|
||||
[elevated]="true">
|
||||
<fa-icon [icon]="faBars" slot="left-icon"></fa-icon>
|
||||
<span slot="title">ShopMart</span>
|
||||
<div slot="right-menu" style="display: flex; gap: 1rem; align-items: center;">
|
||||
<div style="position: relative;">
|
||||
<fa-icon [icon]="faShoppingCart" style="cursor: pointer;" (click)="handleAction('cart')"></fa-icon>
|
||||
<span style="position: absolute; top: -8px; right: -8px; background: #ff4757; color: white; border-radius: 50%; width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 600;">3</span>
|
||||
</div>
|
||||
<fa-icon [icon]="faSearch" style="cursor: pointer;" (click)="handleAction('search')"></fa-icon>
|
||||
</div>
|
||||
</ui-appbar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Social Media App</h4>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; margin-bottom: 1rem;">
|
||||
<ui-appbar
|
||||
variant="standard"
|
||||
[slots]="{title: true, leftLogo: true, rightMenu: true}"
|
||||
[elevated]="false">
|
||||
<div slot="left-logo" style="font-weight: 700; font-size: 1.25rem; background: linear-gradient(45deg, #667eea, #764ba2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">
|
||||
SocialHub
|
||||
</div>
|
||||
<span slot="title">Feed</span>
|
||||
<div slot="right-menu" style="display: flex; gap: 1rem; align-items: center;">
|
||||
<div style="position: relative;">
|
||||
<fa-icon [icon]="faBell" style="cursor: pointer;" (click)="handleAction('notifications')"></fa-icon>
|
||||
<span style="position: absolute; top: -6px; right: -6px; background: #ff3742; width: 8px; height: 8px; border-radius: 50%;"></span>
|
||||
</div>
|
||||
<fa-icon [icon]="faUser" style="cursor: pointer;" (click)="handleAction('profile')"></fa-icon>
|
||||
</div>
|
||||
</ui-appbar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Settings Page</h4>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; margin-bottom: 1rem;">
|
||||
<ui-appbar
|
||||
variant="large"
|
||||
[slots]="{title: true, leftIcon: true, rightIcon: true}"
|
||||
[elevated]="false">
|
||||
<fa-icon [icon]="faArrowLeft" slot="left-icon" style="cursor: pointer;" (click)="handleAction('back')"></fa-icon>
|
||||
<span slot="title">Account Settings</span>
|
||||
<fa-icon [icon]="faCog" slot="right-icon" style="cursor: pointer;" (click)="handleAction('settings')"></fa-icon>
|
||||
</ui-appbar>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Demo</h3>
|
||||
<p style="margin-bottom: 1rem; color: #6c757d;">
|
||||
Customize the appbar below by toggling different options:
|
||||
</p>
|
||||
|
||||
<div style="display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap;">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="demoConfig().elevated"
|
||||
(change)="updateDemoConfig('elevated', $event)">
|
||||
Elevated
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="demoConfig().slots.leftIcon"
|
||||
(change)="updateDemoSlot('leftIcon', $event)">
|
||||
Left Icon
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="demoConfig().slots.leftLogo"
|
||||
(change)="updateDemoSlot('leftLogo', $event)">
|
||||
Left Logo
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="demoConfig().slots.rightIcon"
|
||||
(change)="updateDemoSlot('rightIcon', $event)">
|
||||
Right Icon
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="demoConfig().slots.rightAvatar"
|
||||
(change)="updateDemoSlot('rightAvatar', $event)">
|
||||
Right Avatar
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="demoConfig().slots.rightMenu"
|
||||
(change)="updateDemoSlot('rightMenu', $event)">
|
||||
Right Menu
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
Variant:
|
||||
<select
|
||||
[value]="demoConfig().variant"
|
||||
(change)="updateDemoVariant($event)"
|
||||
style="padding: 0.25rem 0.5rem; border: 1px solid #ced4da; border-radius: 4px;">
|
||||
<option value="compact">Compact</option>
|
||||
<option value="standard">Standard</option>
|
||||
<option value="large">Large</option>
|
||||
<option value="prominent">Prominent</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; margin-bottom: 1rem;">
|
||||
<ui-appbar
|
||||
[variant]="demoConfig().variant"
|
||||
[slots]="demoConfig().slots"
|
||||
[elevated]="demoConfig().elevated">
|
||||
@if (demoConfig().slots.leftIcon) {
|
||||
<fa-icon [icon]="faBars" slot="left-icon"></fa-icon>
|
||||
}
|
||||
@if (demoConfig().slots.leftLogo) {
|
||||
<div slot="left-logo" style="display: flex; align-items: center; gap: 0.5rem; font-weight: 600; color: #007bff;">
|
||||
<div style="width: 20px; height: 20px; background: linear-gradient(135deg, #007bff, #0056b3); border-radius: 4px;"></div>
|
||||
Demo
|
||||
</div>
|
||||
}
|
||||
<span slot="title">{{ demoConfig().title }}</span>
|
||||
@if (demoConfig().slots.rightIcon) {
|
||||
<fa-icon [icon]="faSearch" slot="right-icon"></fa-icon>
|
||||
}
|
||||
@if (demoConfig().slots.rightAvatar) {
|
||||
<div slot="right-avatar" style="width: 32px; height: 32px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-weight: 500; font-size: 12px;">
|
||||
DU
|
||||
</div>
|
||||
}
|
||||
@if (demoConfig().slots.rightMenu) {
|
||||
<div slot="right-menu" style="display: flex; gap: 1rem;">
|
||||
<fa-icon [icon]="faHeart" style="cursor: pointer;"></fa-icon>
|
||||
<fa-icon [icon]="faCog" style="cursor: pointer;"></fa-icon>
|
||||
</div>
|
||||
}
|
||||
</ui-appbar>
|
||||
</div>
|
||||
|
||||
@if (lastAction()) {
|
||||
<div style="padding: 1rem; background: #e3f2fd; border-radius: 4px;">
|
||||
<strong>Last action:</strong> {{ lastAction() }}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Code Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Basic Appbar:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-appbar
|
||||
variant="standard"
|
||||
[slots]="{title: true, leftIcon: true, rightIcon: true}"
|
||||
[elevated]="false">
|
||||
<fa-icon [icon]="faBars" slot="left-icon"></fa-icon>
|
||||
<span slot="title">My App</span>
|
||||
<fa-icon [icon]="faSearch" slot="right-icon"></fa-icon>
|
||||
</ui-appbar></code></pre>
|
||||
|
||||
<h4>Appbar with Logo and Avatar:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-appbar
|
||||
variant="standard"
|
||||
[slots]="{title: true, leftLogo: true, rightAvatar: true}"
|
||||
[elevated]="true">
|
||||
<div slot="left-logo" style="font-weight: 600;">
|
||||
MyBrand
|
||||
</div>
|
||||
<span slot="title">Dashboard</span>
|
||||
<img slot="right-avatar" src="avatar.jpg"
|
||||
style="width: 32px; height: 32px; border-radius: 50%;">
|
||||
</ui-appbar></code></pre>
|
||||
|
||||
<h4>Appbar with Multiple Actions:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-appbar
|
||||
variant="standard"
|
||||
[slots]="{title: true, leftIcon: true, rightMenu: true}"
|
||||
[elevated]="false">
|
||||
<fa-icon [icon]="faArrowLeft" slot="left-icon"></fa-icon>
|
||||
<span slot="title">Settings</span>
|
||||
<div slot="right-menu" style="display: flex; gap: 1rem;">
|
||||
<fa-icon [icon]="faSearch"></fa-icon>
|
||||
<fa-icon [icon]="faEllipsisV"></fa-icon>
|
||||
</div>
|
||||
</ui-appbar></code></pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(287, 12%, 35%);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid hsl(289, 14%, 90%);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: hsl(286, 20%, 99%);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
fa-icon:hover {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.875rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
select {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class AppbarDemoComponent {
|
||||
// State signals
|
||||
isDarkMode = signal(false);
|
||||
lastAction = signal('');
|
||||
|
||||
// Demo configuration
|
||||
demoConfig = signal({
|
||||
variant: 'standard' as any,
|
||||
elevated: false,
|
||||
title: 'Interactive Demo',
|
||||
slots: {
|
||||
leftIcon: true,
|
||||
leftLogo: false,
|
||||
leftAvatar: false,
|
||||
title: true,
|
||||
rightIcon: true,
|
||||
rightLogo: false,
|
||||
rightAvatar: false,
|
||||
rightMenu: false
|
||||
}
|
||||
});
|
||||
|
||||
// Font Awesome icons
|
||||
faBars = faBars;
|
||||
faSearch = faSearch;
|
||||
faUser = faUser;
|
||||
faHeart = faHeart;
|
||||
faBell = faBell;
|
||||
faCog = faCog;
|
||||
faShoppingCart = faShoppingCart;
|
||||
faHome = faHome;
|
||||
faArrowLeft = faArrowLeft;
|
||||
faPlus = faPlus;
|
||||
faShare = faShare;
|
||||
faEllipsisV = faEllipsisV;
|
||||
faSun = faSun;
|
||||
faMoon = faMoon;
|
||||
|
||||
handleAction(action: string): void {
|
||||
this.lastAction.set(`${action} clicked at ${new Date().toLocaleTimeString()}`);
|
||||
console.log(`Appbar action: ${action}`);
|
||||
}
|
||||
|
||||
toggleTheme(): void {
|
||||
this.isDarkMode.update(current => !current);
|
||||
this.handleAction(`Theme switched to ${this.isDarkMode() ? 'dark' : 'light'} mode`);
|
||||
}
|
||||
|
||||
handleImageError(event: Event): void {
|
||||
const target = event.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
const fallback = document.createElement('div');
|
||||
fallback.style.width = '32px';
|
||||
fallback.style.height = '32px';
|
||||
fallback.style.borderRadius = '50%';
|
||||
fallback.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
|
||||
fallback.style.display = 'flex';
|
||||
fallback.style.alignItems = 'center';
|
||||
fallback.style.justifyContent = 'center';
|
||||
fallback.style.color = 'white';
|
||||
fallback.style.fontWeight = '500';
|
||||
fallback.style.fontSize = '12px';
|
||||
fallback.textContent = 'U';
|
||||
target.parentNode?.insertBefore(fallback, target);
|
||||
}
|
||||
|
||||
updateDemoConfig(key: string, event: Event): void {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const value = target.checked;
|
||||
|
||||
this.demoConfig.update(config => ({
|
||||
...config,
|
||||
[key]: value
|
||||
}));
|
||||
}
|
||||
|
||||
updateDemoSlot(slotName: string, event: Event): void {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const checked = target.checked;
|
||||
|
||||
this.demoConfig.update(config => ({
|
||||
...config,
|
||||
slots: {
|
||||
...config.slots,
|
||||
[slotName]: checked
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
updateDemoVariant(event: Event): void {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
const variant = target.value as any;
|
||||
|
||||
this.demoConfig.update(config => ({
|
||||
...config,
|
||||
variant,
|
||||
title: variant.charAt(0).toUpperCase() + variant.slice(1) + ' Demo'
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: 32px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: 32px;
|
||||
margin-bottom: 48px;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 64px;
|
||||
padding: 32px;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 32px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
> label {
|
||||
font-weight: 500;
|
||||
color: $semantic-color-text-secondary;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-form {
|
||||
.form-values {
|
||||
margin-top: 32px;
|
||||
padding: 16px;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
h4 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: $semantic-color-background;
|
||||
padding: 12px;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: $semantic-color-text-primary;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: $semantic-color-text-secondary;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.event-log {
|
||||
margin-top: 32px;
|
||||
|
||||
.log-container {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: $semantic-color-background;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr auto;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
font-size: 12px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-event {
|
||||
font-weight: 500;
|
||||
color: $semantic-color-brand-primary;
|
||||
}
|
||||
|
||||
.log-data {
|
||||
color: $semantic-color-text-primary;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: $semantic-color-text-tertiary;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-log-btn {
|
||||
padding: 8px 12px;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
color: $semantic-color-text-secondary;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-color: $semantic-color-border-focus;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-brand-primary;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 1024px - 1) {
|
||||
.demo-row {
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px - 1) {
|
||||
.demo-container {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.event-log {
|
||||
.log-entry {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
|
||||
.log-time {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms';
|
||||
import { AutocompleteComponent, AutocompleteOption } from '../../../../../ui-essentials/src/lib/components/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-autocomplete-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, AutocompleteComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Autocomplete Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<label>Small</label>
|
||||
<ui-autocomplete
|
||||
size="sm"
|
||||
placeholder="Small autocomplete..."
|
||||
[options]="countries"
|
||||
(valueChange)="onValueChange('small', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<label>Medium (default)</label>
|
||||
<ui-autocomplete
|
||||
size="md"
|
||||
placeholder="Medium autocomplete..."
|
||||
[options]="countries"
|
||||
(valueChange)="onValueChange('medium', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<label>Large</label>
|
||||
<ui-autocomplete
|
||||
size="lg"
|
||||
placeholder="Large autocomplete..."
|
||||
[options]="countries"
|
||||
(valueChange)="onValueChange('large', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Styles -->
|
||||
<section class="demo-section">
|
||||
<h3>Variants</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<label>Outlined (default)</label>
|
||||
<ui-autocomplete
|
||||
variant="outlined"
|
||||
placeholder="Outlined autocomplete..."
|
||||
[options]="countries"
|
||||
(valueChange)="onValueChange('outlined', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<label>Filled</label>
|
||||
<ui-autocomplete
|
||||
variant="filled"
|
||||
placeholder="Filled autocomplete..."
|
||||
[options]="countries"
|
||||
(valueChange)="onValueChange('filled', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- With Labels and Helper Text -->
|
||||
<section class="demo-section">
|
||||
<h3>With Labels & Helper Text</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<ui-autocomplete
|
||||
label="Country"
|
||||
placeholder="Select a country..."
|
||||
helperText="Choose your country of residence"
|
||||
[options]="countries"
|
||||
[required]="true"
|
||||
(valueChange)="onValueChange('labeled', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<ui-autocomplete
|
||||
label="Programming Language"
|
||||
placeholder="Search languages..."
|
||||
helperText="Find your favorite programming language"
|
||||
[options]="programmingLanguages"
|
||||
(valueChange)="onValueChange('languages', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<label>Disabled</label>
|
||||
<ui-autocomplete
|
||||
placeholder="Disabled autocomplete..."
|
||||
[options]="countries"
|
||||
[disabled]="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<label>Read-only</label>
|
||||
<ui-autocomplete
|
||||
placeholder="Read-only autocomplete..."
|
||||
[options]="countries"
|
||||
[readonly]="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<label>Loading</label>
|
||||
<ui-autocomplete
|
||||
placeholder="Loading autocomplete..."
|
||||
[options]="countries"
|
||||
[loading]="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Error State -->
|
||||
<section class="demo-section">
|
||||
<h3>Error State</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<ui-autocomplete
|
||||
label="Country (Required)"
|
||||
placeholder="Select a country..."
|
||||
errorText="Please select a valid country"
|
||||
[options]="countries"
|
||||
[required]="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Advanced Features -->
|
||||
<section class="demo-section">
|
||||
<h3>Advanced Features</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<label>With Secondary Text</label>
|
||||
<ui-autocomplete
|
||||
placeholder="Search users..."
|
||||
[options]="users"
|
||||
(valueChange)="onValueChange('users', $event)"
|
||||
(optionSelected)="onOptionSelected($event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<label>Custom Filter (starts with)</label>
|
||||
<ui-autocomplete
|
||||
placeholder="Type first letters..."
|
||||
[options]="countries"
|
||||
[filterFn]="startsWithFilter"
|
||||
(valueChange)="onValueChange('custom-filter', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<label>Limited Options (max 3)</label>
|
||||
<ui-autocomplete
|
||||
placeholder="Search countries (max 3)..."
|
||||
[options]="countries"
|
||||
[maxOptions]="3"
|
||||
(valueChange)="onValueChange('limited', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Behavior Options -->
|
||||
<section class="demo-section">
|
||||
<h3>Behavior Options</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<label>Open on Focus</label>
|
||||
<ui-autocomplete
|
||||
placeholder="Focus to see options..."
|
||||
[options]="fruits"
|
||||
[openOnFocus]="true"
|
||||
[minQueryLength]="0"
|
||||
(valueChange)="onValueChange('open-focus', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<label>Not Clearable</label>
|
||||
<ui-autocomplete
|
||||
placeholder="No clear button..."
|
||||
[options]="fruits"
|
||||
[clearable]="false"
|
||||
(valueChange)="onValueChange('not-clearable', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<label>Min Query Length (3)</label>
|
||||
<ui-autocomplete
|
||||
placeholder="Type at least 3 characters..."
|
||||
[options]="countries"
|
||||
[minQueryLength]="3"
|
||||
helperText="Start typing to see suggestions"
|
||||
(valueChange)="onValueChange('min-query', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Reactive Forms Integration -->
|
||||
<section class="demo-section">
|
||||
<h3>Reactive Forms Integration</h3>
|
||||
<form [formGroup]="demoForm" class="demo-form">
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<ui-autocomplete
|
||||
label="Favorite Country"
|
||||
placeholder="Select your favorite country..."
|
||||
helperText="This is part of a reactive form"
|
||||
[options]="countries"
|
||||
formControlName="favoriteCountry"
|
||||
[required]="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<ui-autocomplete
|
||||
label="Preferred Language"
|
||||
placeholder="Choose a programming language..."
|
||||
[options]="programmingLanguages"
|
||||
formControlName="preferredLanguage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-values">
|
||||
<h4>Form Values:</h4>
|
||||
<pre>{{ demoForm.value | json }}</pre>
|
||||
<p><strong>Form Valid:</strong> {{ demoForm.valid }}</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Event Logging -->
|
||||
<section class="demo-section">
|
||||
<h3>Event Monitoring</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<ui-autocomplete
|
||||
label="Event Test"
|
||||
placeholder="Type and select to see events..."
|
||||
[options]="fruits"
|
||||
(valueChange)="logEvent('valueChange', $event)"
|
||||
(queryChange)="logEvent('queryChange', $event)"
|
||||
(optionSelected)="logEvent('optionSelected', $event)"
|
||||
(focused)="logEvent('focused', $event)"
|
||||
(blurred)="logEvent('blurred', $event)"
|
||||
(dropdownOpen)="logEvent('dropdownOpen', $event)"
|
||||
(dropdownClose)="logEvent('dropdownClose', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="event-log">
|
||||
<h4>Event Log:</h4>
|
||||
<div class="log-container">
|
||||
@for (event of eventLog(); track event.id) {
|
||||
<div class="log-entry">
|
||||
<span class="log-event">{{ event.type }}</span>
|
||||
<span class="log-data">{{ event.data }}</span>
|
||||
<span class="log-time">{{ event.time }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<button type="button" (click)="clearEventLog()" class="clear-log-btn">
|
||||
Clear Log
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './autocomplete-demo.component.scss'
|
||||
})
|
||||
export class AutocompleteDemoComponent {
|
||||
// Form for reactive forms demo
|
||||
demoForm = new FormGroup({
|
||||
favoriteCountry: new FormControl(''),
|
||||
preferredLanguage: new FormControl('')
|
||||
});
|
||||
|
||||
// Event logging
|
||||
private _eventLog = signal<Array<{id: number, type: string, data: string, time: string}>>([]);
|
||||
eventLog = this._eventLog.asReadonly();
|
||||
private eventCounter = 0;
|
||||
|
||||
// Sample data
|
||||
countries: AutocompleteOption[] = [
|
||||
{ value: 'us', label: 'United States' },
|
||||
{ value: 'ca', label: 'Canada' },
|
||||
{ value: 'gb', label: 'United Kingdom' },
|
||||
{ value: 'de', label: 'Germany' },
|
||||
{ value: 'fr', label: 'France' },
|
||||
{ value: 'it', label: 'Italy' },
|
||||
{ value: 'es', label: 'Spain' },
|
||||
{ value: 'jp', label: 'Japan' },
|
||||
{ value: 'kr', label: 'South Korea' },
|
||||
{ value: 'cn', label: 'China' },
|
||||
{ value: 'in', label: 'India' },
|
||||
{ value: 'au', label: 'Australia' },
|
||||
{ value: 'br', label: 'Brazil' },
|
||||
{ value: 'mx', label: 'Mexico' },
|
||||
{ value: 'ru', label: 'Russia' },
|
||||
{ value: 'za', label: 'South Africa' },
|
||||
{ value: 'ng', label: 'Nigeria' },
|
||||
{ value: 'eg', label: 'Egypt' }
|
||||
];
|
||||
|
||||
programmingLanguages: AutocompleteOption[] = [
|
||||
{ value: 'typescript', label: 'TypeScript' },
|
||||
{ value: 'javascript', label: 'JavaScript' },
|
||||
{ value: 'python', label: 'Python' },
|
||||
{ value: 'java', label: 'Java' },
|
||||
{ value: 'csharp', label: 'C#' },
|
||||
{ value: 'cpp', label: 'C++' },
|
||||
{ value: 'go', label: 'Go' },
|
||||
{ value: 'rust', label: 'Rust' },
|
||||
{ value: 'swift', label: 'Swift' },
|
||||
{ value: 'kotlin', label: 'Kotlin' },
|
||||
{ value: 'php', label: 'PHP' },
|
||||
{ value: 'ruby', label: 'Ruby' },
|
||||
{ value: 'scala', label: 'Scala' },
|
||||
{ value: 'dart', label: 'Dart' }
|
||||
];
|
||||
|
||||
users: AutocompleteOption[] = [
|
||||
{ value: '1', label: 'John Doe', secondaryText: 'john.doe@example.com' },
|
||||
{ value: '2', label: 'Jane Smith', secondaryText: 'jane.smith@example.com' },
|
||||
{ value: '3', label: 'Bob Johnson', secondaryText: 'bob.johnson@example.com' },
|
||||
{ value: '4', label: 'Alice Brown', secondaryText: 'alice.brown@example.com' },
|
||||
{ value: '5', label: 'Charlie Wilson', secondaryText: 'charlie.wilson@example.com' },
|
||||
{ value: '6', label: 'Diana Davis', secondaryText: 'diana.davis@example.com' },
|
||||
{ value: '7', label: 'Eve Miller', secondaryText: 'eve.miller@example.com' },
|
||||
{ value: '8', label: 'Frank Garcia', secondaryText: 'frank.garcia@example.com' }
|
||||
];
|
||||
|
||||
fruits: AutocompleteOption[] = [
|
||||
{ value: 'apple', label: 'Apple' },
|
||||
{ value: 'banana', label: 'Banana' },
|
||||
{ value: 'orange', label: 'Orange' },
|
||||
{ value: 'grape', label: 'Grape' },
|
||||
{ value: 'strawberry', label: 'Strawberry' },
|
||||
{ value: 'blueberry', label: 'Blueberry' },
|
||||
{ value: 'pineapple', label: 'Pineapple' },
|
||||
{ value: 'mango', label: 'Mango' },
|
||||
{ value: 'kiwi', label: 'Kiwi' },
|
||||
{ value: 'peach', label: 'Peach' },
|
||||
{ value: 'cherry', label: 'Cherry' },
|
||||
{ value: 'watermelon', label: 'Watermelon' }
|
||||
];
|
||||
|
||||
// Custom filter function
|
||||
startsWithFilter = (option: AutocompleteOption, query: string): boolean => {
|
||||
return option.label.toLowerCase().startsWith(query.toLowerCase());
|
||||
};
|
||||
|
||||
onValueChange(source: string, value: any): void {
|
||||
console.log(`Value changed (${source}):`, value);
|
||||
}
|
||||
|
||||
onOptionSelected(option: AutocompleteOption): void {
|
||||
console.log('Option selected:', option);
|
||||
}
|
||||
|
||||
logEvent(type: string, data: any): void {
|
||||
const entry = {
|
||||
id: ++this.eventCounter,
|
||||
type,
|
||||
data: typeof data === 'object' ? JSON.stringify(data) : String(data),
|
||||
time: new Date().toLocaleTimeString()
|
||||
};
|
||||
|
||||
const currentLog = this._eventLog();
|
||||
this._eventLog.set([entry, ...currentLog.slice(0, 19)]); // Keep last 20 events
|
||||
}
|
||||
|
||||
clearEventLog(): void {
|
||||
this._eventLog.set([]);
|
||||
this.eventCounter = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { AvatarComponent } from '../../../../../ui-essentials/src/lib/components/data-display/avatar';
|
||||
|
||||
interface Activity {
|
||||
user: string;
|
||||
action: string;
|
||||
time: string;
|
||||
status?: 'online' | 'offline' | 'away' | 'busy';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-avatar-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
AvatarComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Avatar Component Showcase</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Size Variants</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="xs" name="Extra Small"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">XS (24px)</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="sm" name="Small Avatar"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">SM (32px)</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="md" name="Medium Avatar"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">MD (40px)</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Large Avatar"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">LG (48px)</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="xl" name="Extra Large"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">XL (64px)</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="xxl" name="XX Large"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">XXL (80px)</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- With Images -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>With Images</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar
|
||||
size="lg"
|
||||
imageUrl="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face"
|
||||
name="John Doe"
|
||||
altText="John Doe's profile picture">
|
||||
</ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">John Doe</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar
|
||||
size="lg"
|
||||
imageUrl="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=150&h=150&fit=crop&crop=face"
|
||||
name="Jane Smith"
|
||||
altText="Jane Smith's profile picture">
|
||||
</ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Jane Smith</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar
|
||||
size="lg"
|
||||
imageUrl="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face"
|
||||
name="Alex Rodriguez"
|
||||
altText="Alex Rodriguez's profile picture">
|
||||
</ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Alex Rodriguez</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar
|
||||
size="lg"
|
||||
imageUrl="broken-url"
|
||||
name="Fallback Test"
|
||||
altText="This should show initials">
|
||||
</ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Fallback (Broken URL)</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- With Status -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Status Indicators</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Online User" status="online"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Online</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Offline User" status="offline"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Offline</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Away User" status="away"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Away</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Busy User" status="busy"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Busy</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- With Badges -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Notification Badges</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Badge User" badge="3"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">3 notifications</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Badge User" badge="99+"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">99+ notifications</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Badge User" badge="5" status="online"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Online + Badge</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar
|
||||
size="lg"
|
||||
name="Premium User"
|
||||
badge="⭐"
|
||||
imageUrl="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face">
|
||||
</ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Premium User</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Shape Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Shape Variants</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Circle User" shape="circle"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Circle (Default)</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Square User" shape="square"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Square</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Rounded User" shape="rounded"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Rounded Square</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Color Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Color Variants</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Primary User" color="primary"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Primary</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Secondary User" color="secondary"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Secondary</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Success User" color="success"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Success</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Warning User" color="warning"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Warning</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Danger User" color="danger"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Danger</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Activity List Example -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Activity List Example</h3>
|
||||
<div style="background: #f8f9fa; border-radius: 12px; padding: 1.5rem; max-width: 600px;">
|
||||
<h4 style="margin: 0 0 1rem 0; font-size: 1.125rem;">Recent Activity</h4>
|
||||
@for (activity of activities; track activity.user) {
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 0; border-bottom: 1px solid #e9ecef;">
|
||||
<ui-avatar
|
||||
size="sm"
|
||||
[name]="activity.user"
|
||||
[status]="activity.status">
|
||||
</ui-avatar>
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div style="font-weight: 500; font-size: 14px; margin-bottom: 2px;">{{ activity.user }}</div>
|
||||
<div style="color: #666; font-size: 12px; line-height: 1.3;">{{ activity.action }}</div>
|
||||
</div>
|
||||
<div style="color: #999; font-size: 11px; white-space: nowrap;">{{ activity.time }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- User List Example -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>User List Example</h3>
|
||||
<div style="background: #f8f9fa; border-radius: 12px; padding: 1.5rem; max-width: 800px;">
|
||||
<div style="display: grid; gap: 1rem;">
|
||||
@for (user of userProfiles; track user.id) {
|
||||
<div style="display: flex; align-items: center; gap: 1rem; padding: 1rem; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
||||
<ui-avatar
|
||||
size="lg"
|
||||
[name]="user.name"
|
||||
[imageUrl]="user.avatar"
|
||||
[status]="user.status"
|
||||
[badge]="user.notifications > 0 ? user.notifications.toString() : ''">
|
||||
</ui-avatar>
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<h4 style="margin: 0; font-size: 16px; font-weight: 600;">{{ user.name }}</h4>
|
||||
<p style="margin: 2px 0; color: #666; font-size: 14px;">{{ user.role }}</p>
|
||||
<p style="margin: 0; color: #999; font-size: 12px;">{{ user.email }}</p>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<div style="font-size: 12px; color: #999;">Last seen</div>
|
||||
<div style="font-size: 12px; font-weight: 500;">{{ user.lastSeen }}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Loading and Error States -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Special States</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" [loading]="true"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Loading</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="" altText="Empty name fallback"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Empty Name</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" name="Single" altText="Single character name"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Single Character</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-avatar size="lg" altText="No name provided"></ui-avatar>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">No Name</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Avatar Group Example -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Avatar Group Example</h3>
|
||||
<div style="display: flex; align-items: center; gap: -0.5rem;">
|
||||
@for (member of teamMembers; track member.name; let i = $index) {
|
||||
<ui-avatar
|
||||
size="md"
|
||||
[name]="member.name"
|
||||
[imageUrl]="member.avatar"
|
||||
[status]="member.status"
|
||||
[style.z-index]="teamMembers.length - i"
|
||||
[style.margin-left]="i > 0 ? '-8px' : '0'">
|
||||
</ui-avatar>
|
||||
}
|
||||
<div style="margin-left: 1rem; padding: 0.5rem 1rem; background: #e3f2fd; border-radius: 20px; font-size: 12px; font-weight: 500;">
|
||||
+{{ teamMembers.length }} members
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Usage Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Basic Avatar:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-avatar
|
||||
size="md"
|
||||
name="Current User"
|
||||
status="online">
|
||||
</ui-avatar></code></pre>
|
||||
|
||||
<h4>Avatar with Image:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-avatar
|
||||
size="lg"
|
||||
name="John Doe"
|
||||
imageUrl="https://example.com/avatar.jpg"
|
||||
altText="John's profile picture"
|
||||
status="away"
|
||||
badge="5">
|
||||
</ui-avatar></code></pre>
|
||||
|
||||
<h4>Custom Styled Avatar:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-avatar
|
||||
size="xl"
|
||||
name="Admin User"
|
||||
color="primary"
|
||||
shape="rounded"
|
||||
badge="⭐"
|
||||
[loading]="false">
|
||||
</ui-avatar></code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Example</h3>
|
||||
<div style="display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<button
|
||||
(click)="cycleStatus()"
|
||||
style="padding: 0.5rem 1rem; border: 1px solid #007bff; background: #007bff; color: white; border-radius: 4px; cursor: pointer;">
|
||||
Toggle Status
|
||||
</button>
|
||||
<button
|
||||
(click)="toggleBadge()"
|
||||
style="padding: 0.5rem 1rem; border: 1px solid #28a745; background: #28a745; color: white; border-radius: 4px; cursor: pointer;">
|
||||
Toggle Badge
|
||||
</button>
|
||||
<button
|
||||
(click)="cycleSize()"
|
||||
style="padding: 0.5rem 1rem; border: 1px solid #6c757d; background: #6c757d; color: white; border-radius: 4px; cursor: pointer;">
|
||||
Cycle Size
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<ui-avatar
|
||||
[size]="interactiveSize"
|
||||
name="Interactive User"
|
||||
[status]="interactiveStatus"
|
||||
[badge]="interactiveBadge"
|
||||
imageUrl="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=150&h=150&fit=crop&crop=face">
|
||||
</ui-avatar>
|
||||
<div>
|
||||
<div><strong>Size:</strong> {{ interactiveSize }}</div>
|
||||
<div><strong>Status:</strong> {{ interactiveStatus || 'none' }}</div>
|
||||
<div><strong>Badge:</strong> {{ interactiveBadge || 'none' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(287, 12%, 35%);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid hsl(289, 14%, 90%);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: hsl(286, 20%, 99%);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #d63384;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class AvatarDemoComponent {
|
||||
activities: Activity[] = [
|
||||
{
|
||||
user: 'Alice Johnson',
|
||||
action: 'Updated the project documentation and added new API endpoints',
|
||||
time: '2 min ago',
|
||||
status: 'online'
|
||||
},
|
||||
{
|
||||
user: 'Bob Smith',
|
||||
action: 'Completed code review for the authentication module',
|
||||
time: '15 min ago',
|
||||
status: 'away'
|
||||
},
|
||||
{
|
||||
user: 'Carol Williams',
|
||||
action: 'Created new user interface mockups for the dashboard',
|
||||
time: '1 hour ago',
|
||||
status: 'online'
|
||||
},
|
||||
{
|
||||
user: 'David Brown',
|
||||
action: 'Fixed critical bug in the payment processing system',
|
||||
time: '2 hours ago',
|
||||
status: 'busy'
|
||||
},
|
||||
{
|
||||
user: 'Eve Davis',
|
||||
action: 'Deployed version 2.1.0 to production environment',
|
||||
time: '3 hours ago',
|
||||
status: 'offline'
|
||||
}
|
||||
];
|
||||
|
||||
userProfiles = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Alice Johnson',
|
||||
role: 'Senior Developer',
|
||||
email: 'alice@company.com',
|
||||
status: 'online' as const,
|
||||
notifications: 3,
|
||||
lastSeen: '2 min ago',
|
||||
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=150&h=150&fit=crop&crop=face'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Bob Smith',
|
||||
role: 'Product Manager',
|
||||
email: 'bob@company.com',
|
||||
status: 'away' as const,
|
||||
notifications: 0,
|
||||
lastSeen: '15 min ago',
|
||||
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Carol Williams',
|
||||
role: 'UX Designer',
|
||||
email: 'carol@company.com',
|
||||
status: 'online' as const,
|
||||
notifications: 12,
|
||||
lastSeen: 'Just now',
|
||||
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face'
|
||||
}
|
||||
];
|
||||
|
||||
teamMembers = [
|
||||
{ name: 'Alex Chen', status: 'online' as const, avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face' },
|
||||
{ name: 'Maria Garcia', status: 'away' as const, avatar: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=150&h=150&fit=crop&crop=face' },
|
||||
{ name: 'James Wilson', status: 'busy' as const, avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=150&h=150&fit=crop&crop=face' },
|
||||
{ name: 'Sarah Kim', status: 'online' as const, avatar: 'https://images.unsplash.com/photo-1534751516642-a1af1ef26a56?w=150&h=150&fit=crop&crop=face' },
|
||||
];
|
||||
|
||||
// Interactive demo properties
|
||||
interactiveSize: any = 'lg';
|
||||
interactiveStatus: any = 'online';
|
||||
interactiveBadge = '3';
|
||||
|
||||
private statusCycle: any[] = ['online', 'away', 'busy', 'offline', null];
|
||||
private statusIndex = 0;
|
||||
|
||||
private sizeCycle: any[] = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'];
|
||||
private sizeIndex = 3; // Start with 'lg'
|
||||
|
||||
cycleStatus(): void {
|
||||
this.statusIndex = (this.statusIndex + 1) % this.statusCycle.length;
|
||||
this.interactiveStatus = this.statusCycle[this.statusIndex];
|
||||
}
|
||||
|
||||
toggleBadge(): void {
|
||||
if (this.interactiveBadge) {
|
||||
this.interactiveBadge = '';
|
||||
} else {
|
||||
this.interactiveBadge = Math.floor(Math.random() * 99 + 1).toString();
|
||||
}
|
||||
}
|
||||
|
||||
cycleSize(): void {
|
||||
this.sizeIndex = (this.sizeIndex + 1) % this.sizeCycle.length;
|
||||
this.interactiveSize = this.sizeCycle[this.sizeIndex];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic/index' as tokens;
|
||||
|
||||
.demo-container {
|
||||
padding: tokens.$semantic-spacing-layout-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: tokens.$semantic-spacing-layout-lg;
|
||||
padding: tokens.$semantic-spacing-component-md;
|
||||
background: tokens.$semantic-color-surface-primary;
|
||||
border: tokens.$semantic-border-width-1 solid tokens.$semantic-color-border-primary;
|
||||
border-radius: tokens.$semantic-border-radius-md;
|
||||
box-shadow: tokens.$semantic-shadow-elevation-1;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 tokens.$semantic-spacing-content-paragraph 0;
|
||||
font-size: tokens.$semantic-typography-heading-h3-size;
|
||||
color: tokens.$semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: tokens.$semantic-spacing-component-sm;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: tokens.$semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: tokens.$semantic-spacing-component-md;
|
||||
margin-bottom: tokens.$semantic-spacing-content-paragraph;
|
||||
flex-wrap: wrap;
|
||||
|
||||
label {
|
||||
font-size: tokens.$semantic-typography-font-size-md;
|
||||
color: tokens.$semantic-color-text-primary;
|
||||
font-weight: tokens.$semantic-typography-font-weight-medium;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
margin: 0 tokens.$semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: tokens.$semantic-spacing-component-sm tokens.$semantic-spacing-component-md;
|
||||
background: tokens.$semantic-color-primary;
|
||||
color: tokens.$semantic-color-on-primary;
|
||||
border: none;
|
||||
border-radius: tokens.$semantic-border-radius-sm;
|
||||
font-size: tokens.$semantic-typography-font-size-md;
|
||||
cursor: pointer;
|
||||
transition: all tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: tokens.$semantic-color-primary-hover;
|
||||
box-shadow: tokens.$semantic-shadow-elevation-2;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: tokens.$semantic-shadow-elevation-1;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: tokens.$semantic-color-surface-secondary;
|
||||
color: tokens.$semantic-color-text-primary;
|
||||
border: tokens.$semantic-border-width-1 solid tokens.$semantic-color-border-primary;
|
||||
|
||||
&:hover {
|
||||
background: tokens.$semantic-color-surface-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backdrop content styles
|
||||
.backdrop-content {
|
||||
background: tokens.$semantic-color-surface-primary;
|
||||
padding: tokens.$semantic-spacing-component-lg;
|
||||
border-radius: tokens.$semantic-border-radius-md;
|
||||
box-shadow: tokens.$semantic-shadow-elevation-4;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 tokens.$semantic-spacing-content-paragraph 0;
|
||||
font-size: tokens.$semantic-typography-heading-h4-size;
|
||||
color: tokens.$semantic-color-text-primary;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 tokens.$semantic-spacing-content-paragraph 0;
|
||||
font-size: tokens.$semantic-typography-font-size-md;
|
||||
color: tokens.$semantic-color-text-secondary;
|
||||
}
|
||||
|
||||
&--glass {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: tokens.$semantic-border-width-1 solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
&--large {
|
||||
max-width: 600px;
|
||||
padding: tokens.$semantic-spacing-component-xl;
|
||||
}
|
||||
|
||||
&--small {
|
||||
max-width: 300px;
|
||||
padding: tokens.$semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
// Event log styles
|
||||
.demo-log {
|
||||
background: tokens.$semantic-color-surface-secondary;
|
||||
border: tokens.$semantic-border-width-1 solid tokens.$semantic-color-border-subtle;
|
||||
border-radius: tokens.$semantic-border-radius-sm;
|
||||
padding: tokens.$semantic-spacing-component-md;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: tokens.$semantic-spacing-content-paragraph;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
font-size: tokens.$semantic-typography-font-size-sm;
|
||||
color: tokens.$semantic-color-text-primary;
|
||||
padding: tokens.$semantic-spacing-component-xs 0;
|
||||
border-bottom: tokens.$semantic-border-width-1 solid tokens.$semantic-color-border-subtle;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&--empty {
|
||||
color: tokens.$semantic-color-text-tertiary;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
:host-context(.dark-theme) {
|
||||
.backdrop-content {
|
||||
&--glass {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: tokens.$semantic-border-width-1 solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: #{tokens.$semantic-breakpoint-md - 1px}) {
|
||||
.demo-container {
|
||||
padding: tokens.$semantic-spacing-layout-sm;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
padding: tokens.$semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.backdrop-content {
|
||||
margin: tokens.$semantic-spacing-layout-sm;
|
||||
padding: tokens.$semantic-spacing-component-md;
|
||||
|
||||
&--large,
|
||||
&--small {
|
||||
max-width: none;
|
||||
padding: tokens.$semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
input[type="range"] {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: #{tokens.$semantic-breakpoint-sm - 1px}) {
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BackdropComponent } from '../../../../../ui-essentials/src/lib/components/overlays';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-backdrop-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, BackdropComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Backdrop Demo</h2>
|
||||
|
||||
<!-- Basic Usage -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Backdrop</h3>
|
||||
<div class="demo-row">
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="toggleBasicBackdrop()"
|
||||
>
|
||||
Toggle Basic Backdrop
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ui-backdrop
|
||||
[visible]="showBasicBackdrop"
|
||||
(clicked)="handleBackdropClick('Basic backdrop clicked!')"
|
||||
(visibleChange)="showBasicBackdrop = $event"
|
||||
></ui-backdrop>
|
||||
</section>
|
||||
|
||||
<!-- Backdrop Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (variant of variants; track variant) {
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="showVariantBackdrop(variant)"
|
||||
>
|
||||
{{ variant }} Backdrop
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (currentVariant) {
|
||||
<ui-backdrop
|
||||
[visible]="showVariant"
|
||||
[variant]="currentVariant"
|
||||
[opacity]="0.6"
|
||||
(clicked)="handleBackdropClick($event.type + ' backdrop clicked!')"
|
||||
(visibleChange)="handleVariantVisibilityChange($event)"
|
||||
>
|
||||
<div class="backdrop-content">
|
||||
<h4>{{ currentVariant | titlecase }} Backdrop</h4>
|
||||
<p>Click backdrop to close</p>
|
||||
</div>
|
||||
</ui-backdrop>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Blur Backdrop -->
|
||||
<section class="demo-section">
|
||||
<h3>Blur Effect</h3>
|
||||
<div class="demo-row">
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="toggleBlurBackdrop()"
|
||||
>
|
||||
Toggle Blur Backdrop
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ui-backdrop
|
||||
[visible]="showBlurBackdrop"
|
||||
[blur]="true"
|
||||
[opacity]="0.3"
|
||||
(clicked)="handleBackdropClick('Blur backdrop clicked!')"
|
||||
(visibleChange)="showBlurBackdrop = $event"
|
||||
>
|
||||
<div class="backdrop-content backdrop-content--glass">
|
||||
<h4>Blur Backdrop</h4>
|
||||
<p>Notice the blur effect behind this content</p>
|
||||
<button class="demo-button" (click)="showBlurBackdrop = false">Close</button>
|
||||
</div>
|
||||
</ui-backdrop>
|
||||
</section>
|
||||
|
||||
<!-- Opacity Control -->
|
||||
<section class="demo-section">
|
||||
<h3>Opacity Control</h3>
|
||||
<div class="demo-controls">
|
||||
<label for="opacity-slider">Opacity: {{ currentOpacity }}</label>
|
||||
<input
|
||||
id="opacity-slider"
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="1"
|
||||
step="0.1"
|
||||
[value]="currentOpacity"
|
||||
(input)="updateOpacity($event)"
|
||||
>
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="toggleOpacityBackdrop()"
|
||||
>
|
||||
Show Backdrop
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ui-backdrop
|
||||
[visible]="showOpacityBackdrop"
|
||||
[opacity]="currentOpacity"
|
||||
(clicked)="handleBackdropClick('Opacity backdrop clicked!')"
|
||||
(visibleChange)="showOpacityBackdrop = $event"
|
||||
>
|
||||
<div class="backdrop-content">
|
||||
<h4>Opacity: {{ currentOpacity }}</h4>
|
||||
<p>Adjust the opacity using the slider above</p>
|
||||
</div>
|
||||
</ui-backdrop>
|
||||
</section>
|
||||
|
||||
<!-- Z-Index Control -->
|
||||
<section class="demo-section">
|
||||
<h3>Z-Index & Stacking</h3>
|
||||
<div class="demo-row">
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="showStackedBackdrops()"
|
||||
>
|
||||
Show Stacked Backdrops
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Lower z-index backdrop -->
|
||||
<ui-backdrop
|
||||
[visible]="showFirstBackdrop"
|
||||
[zIndex]="1000"
|
||||
variant="dark"
|
||||
[opacity]="0.4"
|
||||
(clicked)="handleBackdropClick('First backdrop clicked!')"
|
||||
>
|
||||
<div class="backdrop-content backdrop-content--large">
|
||||
<h4>First Backdrop (z-index: 1000)</h4>
|
||||
<p>This is behind the second backdrop</p>
|
||||
</div>
|
||||
</ui-backdrop>
|
||||
|
||||
<!-- Higher z-index backdrop -->
|
||||
<ui-backdrop
|
||||
[visible]="showSecondBackdrop"
|
||||
[zIndex]="1010"
|
||||
variant="light"
|
||||
[opacity]="0.3"
|
||||
(clicked)="handleBackdropClick('Second backdrop clicked!')"
|
||||
(visibleChange)="handleStackedBackdropChange($event)"
|
||||
>
|
||||
<div class="backdrop-content backdrop-content--small">
|
||||
<h4>Second Backdrop (z-index: 1010)</h4>
|
||||
<p>This is in front of the first backdrop</p>
|
||||
<button class="demo-button" (click)="hideStackedBackdrops()">Close Both</button>
|
||||
</div>
|
||||
</ui-backdrop>
|
||||
</section>
|
||||
|
||||
<!-- Non-clickable Backdrop -->
|
||||
<section class="demo-section">
|
||||
<h3>Non-clickable Backdrop</h3>
|
||||
<div class="demo-row">
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="toggleNonClickableBackdrop()"
|
||||
>
|
||||
Show Non-clickable
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ui-backdrop
|
||||
[visible]="showNonClickableBackdrop"
|
||||
[clickable]="false"
|
||||
(visibleChange)="showNonClickableBackdrop = $event"
|
||||
>
|
||||
<div class="backdrop-content">
|
||||
<h4>Non-clickable Backdrop</h4>
|
||||
<p>This backdrop cannot be clicked to close</p>
|
||||
<button class="demo-button" (click)="showNonClickableBackdrop = false">Close</button>
|
||||
</div>
|
||||
</ui-backdrop>
|
||||
</section>
|
||||
|
||||
<!-- Event Log -->
|
||||
<section class="demo-section">
|
||||
<h3>Event Log</h3>
|
||||
<div class="demo-log">
|
||||
@for (event of eventLog; track $index) {
|
||||
<div class="log-entry">{{ event }}</div>
|
||||
}
|
||||
@empty {
|
||||
<div class="log-entry log-entry--empty">No events yet. Interact with backdrops above.</div>
|
||||
}
|
||||
</div>
|
||||
<button class="demo-button demo-button--secondary" (click)="clearEventLog()">Clear Log</button>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './backdrop-demo.component.scss'
|
||||
})
|
||||
export class BackdropDemoComponent {
|
||||
// Basic backdrop
|
||||
showBasicBackdrop = false;
|
||||
|
||||
// Variant backdrop
|
||||
variants = ['default', 'blur', 'dark', 'light'] as const;
|
||||
currentVariant: typeof this.variants[number] | null = null;
|
||||
showVariant = false;
|
||||
|
||||
// Blur backdrop
|
||||
showBlurBackdrop = false;
|
||||
|
||||
// Opacity backdrop
|
||||
showOpacityBackdrop = false;
|
||||
currentOpacity = 0.5;
|
||||
|
||||
// Stacked backdrops
|
||||
showFirstBackdrop = false;
|
||||
showSecondBackdrop = false;
|
||||
|
||||
// Non-clickable backdrop
|
||||
showNonClickableBackdrop = false;
|
||||
|
||||
// Event log
|
||||
eventLog: string[] = [];
|
||||
|
||||
toggleBasicBackdrop(): void {
|
||||
this.showBasicBackdrop = !this.showBasicBackdrop;
|
||||
this.logEvent(`Basic backdrop ${this.showBasicBackdrop ? 'opened' : 'closed'}`);
|
||||
}
|
||||
|
||||
showVariantBackdrop(variant: typeof this.variants[number]): void {
|
||||
this.currentVariant = variant;
|
||||
this.showVariant = true;
|
||||
this.logEvent(`${variant} backdrop opened`);
|
||||
}
|
||||
|
||||
handleVariantVisibilityChange(visible: boolean): void {
|
||||
this.showVariant = visible;
|
||||
if (!visible && this.currentVariant) {
|
||||
this.logEvent(`${this.currentVariant} backdrop closed`);
|
||||
this.currentVariant = null;
|
||||
}
|
||||
}
|
||||
|
||||
toggleBlurBackdrop(): void {
|
||||
this.showBlurBackdrop = !this.showBlurBackdrop;
|
||||
this.logEvent(`Blur backdrop ${this.showBlurBackdrop ? 'opened' : 'closed'}`);
|
||||
}
|
||||
|
||||
updateOpacity(event: Event): void {
|
||||
const target = event.target as HTMLInputElement;
|
||||
this.currentOpacity = parseFloat(target.value);
|
||||
}
|
||||
|
||||
toggleOpacityBackdrop(): void {
|
||||
this.showOpacityBackdrop = !this.showOpacityBackdrop;
|
||||
this.logEvent(`Opacity backdrop ${this.showOpacityBackdrop ? 'opened' : 'closed'} with opacity ${this.currentOpacity}`);
|
||||
}
|
||||
|
||||
showStackedBackdrops(): void {
|
||||
this.showFirstBackdrop = true;
|
||||
this.showSecondBackdrop = true;
|
||||
this.logEvent('Stacked backdrops opened');
|
||||
}
|
||||
|
||||
hideStackedBackdrops(): void {
|
||||
this.showFirstBackdrop = false;
|
||||
this.showSecondBackdrop = false;
|
||||
this.logEvent('Stacked backdrops closed');
|
||||
}
|
||||
|
||||
handleStackedBackdropChange(visible: boolean): void {
|
||||
if (!visible) {
|
||||
this.hideStackedBackdrops();
|
||||
}
|
||||
}
|
||||
|
||||
toggleNonClickableBackdrop(): void {
|
||||
this.showNonClickableBackdrop = !this.showNonClickableBackdrop;
|
||||
this.logEvent(`Non-clickable backdrop ${this.showNonClickableBackdrop ? 'opened' : 'closed'}`);
|
||||
}
|
||||
|
||||
handleBackdropClick(message: string): void {
|
||||
this.logEvent(message);
|
||||
}
|
||||
|
||||
private logEvent(message: string): void {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
this.eventLog.unshift(`[${timestamp}] ${message}`);
|
||||
|
||||
// Keep only last 10 events
|
||||
if (this.eventLog.length > 10) {
|
||||
this.eventLog = this.eventLog.slice(0, 10);
|
||||
}
|
||||
}
|
||||
|
||||
clearEventLog(): void {
|
||||
this.eventLog = [];
|
||||
this.logEvent('Event log cleared');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BadgeComponent } from '../../../../../ui-essentials/src/lib/components/data-display/badge';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-badge-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
BadgeComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Badge Component Showcase</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Size Variants</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
||||
<div style="text-align: center;">
|
||||
<ui-badge size="xs" variant="primary">Extra Small</ui-badge>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">XS</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-badge size="sm" variant="primary">Small</ui-badge>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Small</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-badge size="md" variant="primary">Medium</ui-badge>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Medium</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-badge size="lg" variant="primary">Large</ui-badge>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Large</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Colors -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Color Variants</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
||||
<ui-badge variant="default">Default</ui-badge>
|
||||
<ui-badge variant="primary">Primary</ui-badge>
|
||||
<ui-badge variant="secondary">Secondary</ui-badge>
|
||||
<ui-badge variant="success">Success</ui-badge>
|
||||
<ui-badge variant="warning">Warning</ui-badge>
|
||||
<ui-badge variant="danger">Danger</ui-badge>
|
||||
<ui-badge variant="info">Info</ui-badge>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Shape Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Shape Variants</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
||||
<div style="text-align: center;">
|
||||
<ui-badge shape="pill" variant="primary">Pill Shape</ui-badge>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Pill</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-badge shape="rounded" variant="success">Rounded</ui-badge>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Rounded</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-badge shape="square" variant="warning">Square</ui-badge>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Square</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Dot Badges -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Dot Badges</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
||||
<ui-badge variant="success" [isDot]="true"></ui-badge>
|
||||
<ui-badge variant="warning" [isDot]="true"></ui-badge>
|
||||
<ui-badge variant="danger" [isDot]="true"></ui-badge>
|
||||
<ui-badge variant="info" [isDot]="true"></ui-badge>
|
||||
<ui-badge variant="primary" [isDot]="true"></ui-badge>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Usage Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Basic Badge:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-badge variant="success" size="md">
|
||||
Active
|
||||
</ui-badge></code></pre>
|
||||
|
||||
<h4>Dot Badge:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-badge
|
||||
variant="warning"
|
||||
[isDot]="true"
|
||||
ariaLabel="Warning status">
|
||||
</ui-badge></code></pre>
|
||||
|
||||
<h4>Custom Shape:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-badge
|
||||
variant="danger"
|
||||
shape="square"
|
||||
size="lg"
|
||||
title="Critical status">
|
||||
Critical
|
||||
</ui-badge></code></pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(287, 12%, 35%);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid hsl(289, 14%, 90%);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: hsl(286, 20%, 99%);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #d63384;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class BadgeDemoComponent {
|
||||
// Simple demo with no interactive functionality for now
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
|
||||
import { TextButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
|
||||
import { GhostButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
|
||||
import { FabComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
|
||||
import { SimpleButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
|
||||
import {
|
||||
faDownload,
|
||||
faPlus,
|
||||
faShare,
|
||||
faHeart,
|
||||
faBookmark,
|
||||
faEdit,
|
||||
faTrash,
|
||||
faSave,
|
||||
faArrowRight,
|
||||
faArrowLeft,
|
||||
faSearch,
|
||||
faUpload
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-button-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ButtonComponent,
|
||||
TextButtonComponent,
|
||||
GhostButtonComponent,
|
||||
FabComponent,
|
||||
SimpleButtonComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Button Component Showcase</h2>
|
||||
|
||||
<!-- Filled Button Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Filled Buttons</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-button variant="filled" size="small" (clicked)="handleClick('small filled')">Small</ui-button>
|
||||
<ui-button variant="filled" size="medium" (clicked)="handleClick('medium filled')">Medium</ui-button>
|
||||
<ui-button variant="filled" size="large" (clicked)="handleClick('large filled')">Large</ui-button>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-button variant="filled" [disabled]="true">Disabled</ui-button>
|
||||
<ui-button variant="filled" [loading]="true">Loading</ui-button>
|
||||
<ui-button variant="filled" [fullWidth]="false">Full Width</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tonal Button Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Tonal Buttons</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-button variant="tonal" size="small" (clicked)="handleClick('small tonal')">Small</ui-button>
|
||||
<ui-button variant="tonal" size="medium" (clicked)="handleClick('medium tonal')">Medium</ui-button>
|
||||
<ui-button variant="tonal" size="large" (clicked)="handleClick('large tonal')">Large</ui-button>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
||||
<ui-button variant="tonal" [disabled]="true">Disabled</ui-button>
|
||||
<ui-button variant="tonal" [loading]="true">Loading</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Outlined Button Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Outlined Buttons</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-button variant="outlined" size="small" (clicked)="handleClick('small outlined')">Small</ui-button>
|
||||
<ui-button variant="outlined" size="medium" (clicked)="handleClick('medium outlined')">Medium</ui-button>
|
||||
<ui-button variant="outlined" size="large" (clicked)="handleClick('large outlined')">Large</ui-button>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
||||
<ui-button variant="outlined" [disabled]="true">Disabled</ui-button>
|
||||
<ui-button variant="outlined" [loading]="true">Loading</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Text Buttons -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Text Buttons</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-text-button size="small" (clicked)="handleClick('small text')">Small</ui-text-button>
|
||||
<ui-text-button size="medium" (clicked)="handleClick('medium text')">Medium</ui-text-button>
|
||||
<ui-text-button size="large" (clicked)="handleClick('large text')">Large</ui-text-button>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
||||
<ui-text-button [disabled]="true">Disabled</ui-text-button>
|
||||
<ui-text-button [loading]="true">Loading</ui-text-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Ghost Buttons -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Ghost Buttons</h3>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-ghost-button size="small" (clicked)="handleClick('small ghost')">Small</ui-ghost-button>
|
||||
<ui-ghost-button size="medium" (clicked)="handleClick('medium ghost')">Medium</ui-ghost-button>
|
||||
<ui-ghost-button size="large" (clicked)="handleClick('large ghost')">Large</ui-ghost-button>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
||||
<ui-ghost-button [disabled]="true">Disabled</ui-ghost-button>
|
||||
<ui-ghost-button [loading]="true">Loading</ui-ghost-button>
|
||||
<ui-ghost-button [fullWidth]="false">Full Width</ui-ghost-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FAB Components -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Floating Action Buttons (FAB)</h3>
|
||||
|
||||
<!-- FAB Sizes -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Sizes</h4>
|
||||
<div style="display: flex; align-items: center; gap: 2rem; flex-wrap: wrap;">
|
||||
<div style="text-align: center;">
|
||||
<ui-fab variant="primary" size="small" position="static" (clicked)="handleClick('small fab')">+</ui-fab>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Small</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-fab variant="primary" size="medium" position="static" (clicked)="handleClick('medium fab')">+</ui-fab>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Medium</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-fab variant="primary" size="large" position="static" (clicked)="handleClick('large fab')">✨</ui-fab>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Large</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAB Variants -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Variants</h4>
|
||||
<div style="display: flex; align-items: center; gap: 2rem; flex-wrap: wrap;">
|
||||
<div style="text-align: center;">
|
||||
<ui-fab variant="primary" size="medium" position="static" (clicked)="handleClick('primary fab')">📧</ui-fab>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Primary</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-fab variant="secondary" size="medium" position="static" (clicked)="handleClick('secondary fab')">📞</ui-fab>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Secondary</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-fab variant="tertiary" size="medium" position="static" (clicked)="handleClick('tertiary fab')">💬</ui-fab>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Tertiary</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAB States -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>States</h4>
|
||||
<div style="display: flex; align-items: center; gap: 2rem; flex-wrap: wrap;">
|
||||
<div style="text-align: center;">
|
||||
<ui-fab variant="primary" size="medium" position="static" [pulse]="true" (clicked)="handleClick('pulse fab')">🔔</ui-fab>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Pulse</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-fab variant="primary" size="medium" position="static" [disabled]="true">⚙️</ui-fab>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Disabled</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-fab variant="primary" size="medium" position="static" [loading]="true">📤</ui-fab>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Loading</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<ui-fab variant="primary" size="medium" position="static" [expandOnClick]="true" (clicked)="handleClick('expand fab')">✨</ui-fab>
|
||||
<p style="font-size: 12px; margin: 0.5rem 0;">Expand</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Buttons with Icons -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Buttons with Icons</h3>
|
||||
|
||||
<!-- Left Icons -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Icons on Left</h4>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-button variant="filled" [icon]="faDownload" iconPosition="left" (clicked)="handleClick('download')">Download</ui-button>
|
||||
<ui-button variant="filled" [icon]="faPlus" iconPosition="left" (clicked)="handleClick('add new')">Add New</ui-button>
|
||||
<ui-button variant="filled" [icon]="faUpload" iconPosition="left" (clicked)="handleClick('upload')">Upload</ui-button>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-button variant="tonal" [icon]="faEdit" iconPosition="left" (clicked)="handleClick('edit')">Edit</ui-button>
|
||||
<ui-button variant="tonal" [icon]="faShare" iconPosition="left" (clicked)="handleClick('share')">Share</ui-button>
|
||||
<ui-button variant="tonal" [icon]="faSave" iconPosition="left" (clicked)="handleClick('save')">Save</ui-button>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
||||
<ui-button variant="outlined" [icon]="faBookmark" iconPosition="left" (clicked)="handleClick('bookmark')">Bookmark</ui-button>
|
||||
<ui-button variant="outlined" [icon]="faHeart" iconPosition="left" (clicked)="handleClick('favorite')">Favorite</ui-button>
|
||||
<ui-button variant="outlined" [icon]="faSearch" iconPosition="left" (clicked)="handleClick('search')">Search</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Icons -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Icons on Right</h4>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-button variant="filled" [icon]="faArrowRight" iconPosition="right" (clicked)="handleClick('next')">Next</ui-button>
|
||||
<ui-button variant="filled" [icon]="faArrowRight" iconPosition="right" (clicked)="handleClick('continue')">Continue</ui-button>
|
||||
<ui-button variant="filled" [icon]="faArrowRight" iconPosition="right" (clicked)="handleClick('proceed')">Proceed</ui-button>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-button variant="tonal" [icon]="faArrowLeft" iconPosition="right" (clicked)="handleClick('back')">Back</ui-button>
|
||||
<ui-button variant="outlined" [icon]="faTrash" iconPosition="right" (clicked)="handleClick('delete')">Delete</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Size Variants with Icons -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Different Sizes with Icons</h4>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
||||
<ui-button variant="filled" size="small" [icon]="faPlus" iconPosition="left" (clicked)="handleClick('small icon')">Small</ui-button>
|
||||
<ui-button variant="filled" size="medium" [icon]="faPlus" iconPosition="left" (clicked)="handleClick('medium icon')">Medium</ui-button>
|
||||
<ui-button variant="filled" size="large" [icon]="faPlus" iconPosition="left" (clicked)="handleClick('large icon')">Large</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Icon-only Style -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Icon with Minimal Text</h4>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
||||
<ui-button variant="filled" [icon]="faDownload" iconPosition="left" (clicked)="handleClick('dl')">DL</ui-button>
|
||||
<ui-button variant="tonal" [icon]="faEdit" iconPosition="left" (clicked)="handleClick('edit short')">Edit</ui-button>
|
||||
<ui-button variant="outlined" [icon]="faTrash" iconPosition="right" (clicked)="handleClick('del')">Del</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Usage Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Basic Button:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-button
|
||||
variant="filled"
|
||||
size="medium"
|
||||
(clicked)="handleClick($event)">
|
||||
Click Me
|
||||
</ui-button></code></pre>
|
||||
|
||||
<h4>Text Button:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-text-button
|
||||
size="large"
|
||||
[disabled]="false"
|
||||
(clicked)="handleTextClick($event)">
|
||||
Text Action
|
||||
</ui-text-button></code></pre>
|
||||
|
||||
<h4>Button with Icon:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-button
|
||||
variant="filled"
|
||||
[icon]="faDownload"
|
||||
iconPosition="left"
|
||||
(clicked)="downloadFile()">
|
||||
Download
|
||||
</ui-button></code></pre>
|
||||
|
||||
<h4>FAB with Position:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-fab
|
||||
variant="primary"
|
||||
size="medium"
|
||||
position="bottom-right"
|
||||
[pulse]="true"
|
||||
(clicked)="openDialog()">
|
||||
+
|
||||
</ui-fab></code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- UI Essentials Components -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>UI Essentials Library Components</h3>
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-simple-button variant="primary" (clicked)="handleClick('ui-essentials primary')">Primary</ui-simple-button>
|
||||
<ui-simple-button variant="secondary" (clicked)="handleClick('ui-essentials secondary')">Secondary</ui-simple-button>
|
||||
<ui-simple-button variant="primary" [disabled]="true">Disabled</ui-simple-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Actions</h3>
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||
<ui-button variant="filled" (clicked)="showAlert('Success!')">Show Alert</ui-button>
|
||||
<ui-button variant="tonal" (clicked)="toggleLoading()">{{ isLoading ? 'Stop Loading' : 'Start Loading' }}</ui-button>
|
||||
<ui-text-button (clicked)="resetDemo()">Reset Demo</ui-text-button>
|
||||
</div>
|
||||
|
||||
@if (lastClickedButton) {
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #e3f2fd; border-radius: 4px;">
|
||||
<strong>Last clicked:</strong> {{ lastClickedButton }}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Fixed FAB Demo -->
|
||||
<ui-fab
|
||||
variant="primary"
|
||||
size="medium"
|
||||
position="bottom-right"
|
||||
[pulse]="shouldPulse"
|
||||
(clicked)="handleFabClick()">
|
||||
💡
|
||||
</ui-fab>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(287, 12%, 35%);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid hsl(289, 14%, 90%);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: hsl(286, 20%, 99%);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #d63384;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class ButtonDemoComponent {
|
||||
lastClickedButton: string = '';
|
||||
isLoading: boolean = false;
|
||||
shouldPulse: boolean = true;
|
||||
|
||||
// Font Awesome icons
|
||||
faDownload = faDownload;
|
||||
faPlus = faPlus;
|
||||
faShare = faShare;
|
||||
faHeart = faHeart;
|
||||
faBookmark = faBookmark;
|
||||
faEdit = faEdit;
|
||||
faTrash = faTrash;
|
||||
faSave = faSave;
|
||||
faArrowRight = faArrowRight;
|
||||
faArrowLeft = faArrowLeft;
|
||||
faSearch = faSearch;
|
||||
faUpload = faUpload;
|
||||
|
||||
handleClick(buttonType: string): void {
|
||||
this.lastClickedButton = buttonType;
|
||||
console.log(`Clicked: ${buttonType}`);
|
||||
}
|
||||
|
||||
showAlert(message: string): void {
|
||||
alert(message);
|
||||
this.handleClick('Alert button');
|
||||
}
|
||||
|
||||
toggleLoading(): void {
|
||||
this.isLoading = !this.isLoading;
|
||||
this.handleClick(`Loading toggle - ${this.isLoading ? 'started' : 'stopped'}`);
|
||||
}
|
||||
|
||||
resetDemo(): void {
|
||||
this.lastClickedButton = '';
|
||||
this.isLoading = false;
|
||||
this.shouldPulse = true;
|
||||
console.log('Demo reset');
|
||||
}
|
||||
|
||||
handleFabClick(): void {
|
||||
this.shouldPulse = !this.shouldPulse;
|
||||
this.handleClick(`FAB - Pulse ${this.shouldPulse ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CardComponent, GlassVariant } from '../../../../../ui-essentials/src/lib/components/data-display/card';
|
||||
import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-card-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
CardComponent,
|
||||
ButtonComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Card Component Showcase</h2>
|
||||
|
||||
<!-- Basic Card Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Card Variants</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; margin-bottom: 2rem;">
|
||||
<ui-card variant="elevated" size="md" elevation="sm" radius="md">
|
||||
<div slot="header">
|
||||
<h4>Elevated Card</h4>
|
||||
<p>Default elevated card with shadow</p>
|
||||
</div>
|
||||
<p>This is the most common card variant with a subtle elevation that lifts it above the surface.</p>
|
||||
<div slot="footer">
|
||||
<ui-button variant="filled" size="small" (clicked)="handleClick('elevated')">Action</ui-button>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="filled" size="md" elevation="none" radius="md">
|
||||
<div slot="header">
|
||||
<h4>Filled Card</h4>
|
||||
<p>Card with background color</p>
|
||||
</div>
|
||||
<p>Filled cards use background color to separate content from the surrounding surface.</p>
|
||||
<div slot="footer">
|
||||
<ui-button variant="tonal" size="small" (clicked)="handleClick('filled')">Action</ui-button>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="outlined" size="md" elevation="none" radius="md">
|
||||
<div slot="header">
|
||||
<h4>Outlined Card</h4>
|
||||
<p>Card with border emphasis</p>
|
||||
</div>
|
||||
<p>Outlined cards use borders to separate content while maintaining a clean appearance.</p>
|
||||
<div slot="footer">
|
||||
<ui-button variant="outlined" size="small" (clicked)="handleClick('outlined')">Action</ui-button>
|
||||
</div>
|
||||
</ui-card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Size Variants</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; margin-bottom: 2rem;">
|
||||
<ui-card variant="elevated" size="sm" elevation="sm" radius="md">
|
||||
<div slot="header">
|
||||
<h5>Small Card</h5>
|
||||
</div>
|
||||
<p>Compact card for minimal content.</p>
|
||||
<div slot="footer">
|
||||
<ui-button variant="filled" size="small" (clicked)="handleClick('small')">Small</ui-button>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="md" elevation="sm" radius="md">
|
||||
<div slot="header">
|
||||
<h4>Medium Card</h4>
|
||||
</div>
|
||||
<p>Standard card size for most use cases with balanced spacing.</p>
|
||||
<div slot="footer">
|
||||
<ui-button variant="filled" size="medium" (clicked)="handleClick('medium')">Medium</ui-button>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="lg" elevation="sm" radius="md">
|
||||
<div slot="header">
|
||||
<h3>Large Card</h3>
|
||||
</div>
|
||||
<p>Spacious card for detailed content with generous padding and larger typography.</p>
|
||||
<div slot="footer">
|
||||
<ui-button variant="filled" size="large" (clicked)="handleClick('large')">Large</ui-button>
|
||||
</div>
|
||||
</ui-card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Elevation Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Elevation Variants</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem; margin-bottom: 2rem;">
|
||||
<ui-card variant="elevated" size="md" elevation="none" radius="md">
|
||||
<div style="text-align: center; padding: 1rem;">
|
||||
<h5>None</h5>
|
||||
<p>No shadow</p>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="md" elevation="sm" radius="md">
|
||||
<div style="text-align: center; padding: 1rem;">
|
||||
<h5>Small</h5>
|
||||
<p>Subtle shadow</p>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="md" elevation="md" radius="md">
|
||||
<div style="text-align: center; padding: 1rem;">
|
||||
<h5>Medium</h5>
|
||||
<p>Moderate shadow</p>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="md" elevation="lg" radius="md">
|
||||
<div style="text-align: center; padding: 1rem;">
|
||||
<h5>Large</h5>
|
||||
<p>Prominent shadow</p>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="md" elevation="xl" radius="md">
|
||||
<div style="text-align: center; padding: 1rem;">
|
||||
<h5>Extra Large</h5>
|
||||
<p>Deep shadow</p>
|
||||
</div>
|
||||
</ui-card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Radius Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Corner Radius Variants</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem; margin-bottom: 2rem;">
|
||||
<ui-card variant="elevated" size="md" elevation="sm" radius="none">
|
||||
<div style="text-align: center; padding: 1rem;">
|
||||
<h5>No Radius</h5>
|
||||
<p>Sharp corners</p>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="md" elevation="sm" radius="sm">
|
||||
<div style="text-align: center; padding: 1rem;">
|
||||
<h5>Small</h5>
|
||||
<p>Subtle rounding</p>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="md" elevation="sm" radius="md">
|
||||
<div style="text-align: center; padding: 1rem;">
|
||||
<h5>Medium</h5>
|
||||
<p>Standard rounding</p>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="md" elevation="sm" radius="lg">
|
||||
<div style="text-align: center; padding: 1rem;">
|
||||
<h5>Large</h5>
|
||||
<p>Generous rounding</p>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="sm" elevation="sm" radius="full">
|
||||
<div style="text-align: center; padding: 0.5rem; aspect-ratio: 1; display: flex; align-items: center; justify-content: center;">
|
||||
<div>
|
||||
<h6 style="margin: 0; font-size: 0.75rem;">Full</h6>
|
||||
<p style="margin: 0; font-size: 0.625rem;">Circle</p>
|
||||
</div>
|
||||
</div>
|
||||
</ui-card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive States -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive States</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; margin-bottom: 2rem;">
|
||||
<ui-card variant="elevated" size="md" elevation="sm" radius="md" [clickable]="true" (cardClick)="handleCardClick('clickable')">
|
||||
<div slot="header">
|
||||
<h4>Clickable Card</h4>
|
||||
<p>Click me!</p>
|
||||
</div>
|
||||
<p>This card responds to clicks and has hover effects. Try clicking or focusing with keyboard.</p>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="md" elevation="sm" radius="md" [disabled]="true">
|
||||
<div slot="header">
|
||||
<h4>Disabled Card</h4>
|
||||
<p>Cannot interact</p>
|
||||
</div>
|
||||
<p>This card is disabled and cannot be interacted with.</p>
|
||||
<div slot="footer">
|
||||
<ui-button variant="filled" size="small" [disabled]="true">Disabled</ui-button>
|
||||
</div>
|
||||
</ui-card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Glass Effect Variants -->
|
||||
<section style="margin-bottom: 3rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 2rem; border-radius: 1rem;">
|
||||
<h3 style="color: white; margin-bottom: 2rem;">Glass Effect Variants</h3>
|
||||
<p style="color: rgba(255,255,255,0.9); margin-bottom: 2rem;">
|
||||
Glass morphism effects using semantic design tokens from the shared-ui library.
|
||||
Each variant offers different levels of transparency and blur effects.
|
||||
</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-bottom: 2rem;">
|
||||
<ui-card variant="elevated" size="md" elevation="md" radius="md" [glass]="true" glassVariant="translucent">
|
||||
<div slot="header">
|
||||
<h4 style="color: white;">Translucent Glass</h4>
|
||||
<p style="color: rgba(255,255,255,0.8);">Ultra-light transparency</p>
|
||||
</div>
|
||||
<p style="color: rgba(255,255,255,0.9);">
|
||||
Barely visible background with subtle blur effect for minimal visual interference.
|
||||
Perfect for overlays that need to preserve background visibility.
|
||||
</p>
|
||||
<div slot="footer">
|
||||
<ui-button variant="outlined" size="small" style="border-color: white; color: white;" (clicked)="handleClick('glass-translucent')">Translucent</ui-button>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="md" elevation="md" radius="md" [glass]="true" glassVariant="light">
|
||||
<div slot="header">
|
||||
<h4 style="color: white;">Light Glass</h4>
|
||||
<p style="color: rgba(255,255,255,0.8);">Gentle transparency</p>
|
||||
</div>
|
||||
<p style="color: rgba(255,255,255,0.9);">
|
||||
Gentle glass effect that maintains excellent readability while adding modern depth.
|
||||
Ideal for cards that need subtle separation from background.
|
||||
</p>
|
||||
<div slot="footer">
|
||||
<ui-button variant="outlined" size="small" style="border-color: white; color: white;" (clicked)="handleClick('glass-light')">Light</ui-button>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="md" elevation="md" radius="md" [glass]="true" glassVariant="medium">
|
||||
<div slot="header">
|
||||
<h4 style="color: white;">Medium Glass</h4>
|
||||
<p style="color: rgba(255,255,255,0.8);">Balanced effect</p>
|
||||
</div>
|
||||
<p style="color: rgba(255,255,255,0.9);">
|
||||
Perfect balance between visibility and glass morphism effect.
|
||||
The most versatile variant suitable for most interface components.
|
||||
</p>
|
||||
<div slot="footer">
|
||||
<ui-button variant="outlined" size="small" style="border-color: white; color: white;" (clicked)="handleClick('glass-medium')">Medium</ui-button>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="md" elevation="md" radius="md" [glass]="true" glassVariant="heavy">
|
||||
<div slot="header">
|
||||
<h4 style="color: white;">Heavy Glass</h4>
|
||||
<p style="color: rgba(255,255,255,0.8);">Strong presence</p>
|
||||
</div>
|
||||
<p style="color: rgba(255,255,255,0.9);">
|
||||
Prominent glass effect with substantial background presence.
|
||||
Great for important UI elements that need to stand out prominently.
|
||||
</p>
|
||||
<div slot="footer">
|
||||
<ui-button variant="outlined" size="small" style="border-color: white; color: white;" (clicked)="handleClick('glass-heavy')">Heavy</ui-button>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="md" elevation="lg" radius="lg" [glass]="true" glassVariant="frosted">
|
||||
<div slot="header">
|
||||
<h4 style="color: white;">Frosted Glass</h4>
|
||||
<p style="color: rgba(255,255,255,0.8);">Maximum opacity with enhanced blur</p>
|
||||
</div>
|
||||
<p style="color: rgba(255,255,255,0.9);">
|
||||
Nearly opaque with enhanced blur for a true frosted glass appearance.
|
||||
Perfect for modals, overlays, and primary interface panels.
|
||||
</p>
|
||||
<div slot="footer">
|
||||
<ui-button variant="outlined" size="small" style="border-color: white; color: white;" (clicked)="handleClick('glass-frosted')">Frosted</ui-button>
|
||||
</div>
|
||||
</ui-card>
|
||||
</div>
|
||||
|
||||
<h4 style="color: white; margin-bottom: 1rem;">Interactive Glass Cards</h4>
|
||||
<p style="color: rgba(255,255,255,0.8); margin-bottom: 1.5rem;">
|
||||
Glass effects with full interactivity, smooth animations, and hover states.
|
||||
</p>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
|
||||
<ui-card variant="elevated" size="md" elevation="lg" radius="lg" [glass]="true" glassVariant="light" [clickable]="true" (cardClick)="handleCardClick('glass-interactive-light')">
|
||||
<div slot="header">
|
||||
<h4 style="color: white;">Clickable Light Glass</h4>
|
||||
<p style="color: rgba(255,255,255,0.8);">Interactive with smooth animations</p>
|
||||
</div>
|
||||
<p style="color: rgba(255,255,255,0.9);">
|
||||
Click me! Light glass variant with full interactivity and animation support.
|
||||
Features hover effects and touch feedback.
|
||||
</p>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="md" elevation="lg" radius="lg" [glass]="true" glassVariant="heavy" [clickable]="true" (cardClick)="handleCardClick('glass-interactive-heavy')">
|
||||
<div slot="header">
|
||||
<h4 style="color: white;">Clickable Heavy Glass</h4>
|
||||
<p style="color: rgba(255,255,255,0.8);">Strong glass with responsive feedback</p>
|
||||
</div>
|
||||
<p style="color: rgba(255,255,255,0.9);">
|
||||
Heavy glass variant combining strong visual presence with responsive interactions.
|
||||
Perfect for call-to-action cards and important interactive elements.
|
||||
</p>
|
||||
</ui-card>
|
||||
|
||||
<ui-card variant="elevated" size="md" elevation="lg" radius="lg" [glass]="true" glassVariant="medium" [clickable]="true" (cardClick)="handleCardClick('glass-interactive-medium')">
|
||||
<div slot="header">
|
||||
<h4 style="color: white;">Balanced Interactive Glass</h4>
|
||||
<p style="color: rgba(255,255,255,0.8);">Medium glass with keyboard navigation</p>
|
||||
</div>
|
||||
<p style="color: rgba(255,255,255,0.9);">
|
||||
Medium glass effect with complete accessibility support including keyboard navigation,
|
||||
focus states, and screen reader compatibility.
|
||||
</p>
|
||||
</ui-card>
|
||||
</div>
|
||||
|
||||
<h4 style="color: white; margin-bottom: 1rem; margin-top: 2rem;">Glass Implementation Details</h4>
|
||||
<div style="background: rgba(0,0,0,0.2); padding: 1.5rem; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1);">
|
||||
<p style="color: rgba(255,255,255,0.9); margin-bottom: 1rem;">
|
||||
<strong>Enhanced Glass System:</strong> All glass effects now use semantic design tokens from the shared-ui library,
|
||||
ensuring consistent theming, better performance, and improved browser compatibility.
|
||||
</p>
|
||||
<ul style="color: rgba(255,255,255,0.8); line-height: 1.6;">
|
||||
<li><strong>Semantic Tokens:</strong> Uses <code>@include glass-{{ '{' }}variant{{ '}' }}(true)</code> mixins</li>
|
||||
<li><strong>Animation Support:</strong> Built-in smooth transitions and hover effects</li>
|
||||
<li><strong>Accessibility:</strong> Proper focus states and keyboard navigation</li>
|
||||
<li><strong>Browser Support:</strong> Automatic fallbacks for older browsers</li>
|
||||
<li><strong>Performance:</strong> Optimized with <code>will-change</code> and <code>isolation</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Background Layer Cards -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Background Layer Cards</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; margin-bottom: 2rem;">
|
||||
<ui-card
|
||||
variant="elevated"
|
||||
size="md"
|
||||
elevation="md"
|
||||
radius="lg"
|
||||
[hasBackgroundLayer]="true"
|
||||
[hasHeader]="true"
|
||||
[hasFooter]="true">
|
||||
<div slot="background" style="
|
||||
background: linear-gradient(45deg, #ff6b6b, #feca57);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
"></div>
|
||||
<div slot="header">
|
||||
<h4 style="color: white; text-shadow: 0 1px 3px rgba(0,0,0,0.3);">Gradient Background</h4>
|
||||
<p style="color: rgba(255,255,255,0.9);">Beautiful gradient overlay</p>
|
||||
</div>
|
||||
<p style="color: rgba(255,255,255,0.95);">This card has a gradient background layer that creates visual depth and interest.</p>
|
||||
<div slot="footer">
|
||||
<ui-button variant="outlined" size="small" style="border-color: white; color: white;" (clicked)="handleClick('gradient')">Action</ui-button>
|
||||
</div>
|
||||
</ui-card>
|
||||
|
||||
<ui-card
|
||||
variant="elevated"
|
||||
size="md"
|
||||
elevation="md"
|
||||
radius="lg"
|
||||
[hasBackgroundLayer]="true"
|
||||
[hasHeader]="true"
|
||||
[clickable]="true"
|
||||
(cardClick)="handleCardClick('pattern')">
|
||||
<div slot="background" style="
|
||||
background-image: radial-gradient(circle at 25% 25%, #ff9a9e 0%, transparent 50%),
|
||||
radial-gradient(circle at 75% 75%, #fecfef 0%, transparent 50%),
|
||||
radial-gradient(circle at 50% 50%, #ffecd2 0%, transparent 50%);
|
||||
background-size: 150px 150px, 100px 100px, 200px 200px;
|
||||
background-position: 0 0, 50px 50px, 100px 100px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
"></div>
|
||||
<div slot="header">
|
||||
<h4>Pattern Background</h4>
|
||||
<p>Custom pattern design</p>
|
||||
</div>
|
||||
<p>Click me! Cards can have complex patterns and remain fully interactive with proper layering.</p>
|
||||
</ui-card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Usage Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Basic Card:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-card variant="elevated" size="md" elevation="sm" radius="md">
|
||||
<div slot="header">
|
||||
<h4>Card Title</h4>
|
||||
<p>Card subtitle</p>
|
||||
</div>
|
||||
<p>Card content goes here</p>
|
||||
<div slot="footer">
|
||||
<ui-button variant="filled">Action</ui-button>
|
||||
</div>
|
||||
</ui-card></code></pre>
|
||||
|
||||
<h4>Interactive Card:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-card
|
||||
variant="elevated"
|
||||
[clickable]="true"
|
||||
(cardClick)="handleCardClick($event)">
|
||||
<p>Click me!</p>
|
||||
</ui-card></code></pre>
|
||||
|
||||
<h4>Glass Effect Card:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-card
|
||||
variant="elevated"
|
||||
[glass]="true"
|
||||
glassVariant="medium"
|
||||
elevation="md"
|
||||
radius="lg">
|
||||
<p>Glass morphism effect with semantic tokens</p>
|
||||
</ui-card></code></pre>
|
||||
|
||||
<h4>Glass Variant Examples:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><!-- All glass variants using semantic design tokens -->
|
||||
<ui-card [glass]="true" glassVariant="translucent">Ultra-light transparency</ui-card>
|
||||
<ui-card [glass]="true" glassVariant="light">Gentle glass effect</ui-card>
|
||||
<ui-card [glass]="true" glassVariant="medium">Balanced glass morphism</ui-card>
|
||||
<ui-card [glass]="true" glassVariant="heavy">Strong visual presence</ui-card>
|
||||
<ui-card [glass]="true" glassVariant="frosted">Maximum opacity with blur</ui-card></code></pre>
|
||||
|
||||
<h4>Interactive Glass Card:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-card
|
||||
variant="elevated"
|
||||
[glass]="true"
|
||||
glassVariant="light"
|
||||
[clickable]="true"
|
||||
(cardClick)="handleCardClick($event)"
|
||||
elevation="lg"
|
||||
radius="lg">
|
||||
<p>Clickable glass card with animations</p>
|
||||
</ui-card></code></pre>
|
||||
|
||||
<h4>Background Layer Card:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-card
|
||||
variant="elevated"
|
||||
[hasBackgroundLayer]="true"
|
||||
[hasHeader]="true">
|
||||
<div slot="background">
|
||||
<!-- Background content -->
|
||||
</div>
|
||||
<div slot="header">
|
||||
<h4>Card with Background</h4>
|
||||
</div>
|
||||
<p>Content over background</p>
|
||||
</ui-card></code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Actions</h3>
|
||||
@if (lastAction) {
|
||||
<div style="margin-bottom: 1rem; padding: 1rem; background: #e3f2fd; border-radius: 4px;">
|
||||
<strong>Last action:</strong> {{ lastAction }}
|
||||
</div>
|
||||
}
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 2rem;">
|
||||
<ui-button variant="filled" (clicked)="resetDemo()">Reset Demo</ui-button>
|
||||
<ui-button variant="tonal" (clicked)="showAlert('Card showcase complete!')">Show Alert</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(287, 12%, 35%);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid hsl(289, 14%, 90%);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: hsl(286, 20%, 99%);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #d63384;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class CardDemoComponent {
|
||||
lastAction: string = '';
|
||||
|
||||
handleClick(buttonType: string): void {
|
||||
this.lastAction = `Button clicked: ${buttonType}`;
|
||||
console.log(`Button clicked: ${buttonType}`);
|
||||
}
|
||||
|
||||
handleCardClick(cardType: string): void {
|
||||
this.lastAction = `Card clicked: ${cardType}`;
|
||||
console.log(`Card clicked: ${cardType}`);
|
||||
}
|
||||
|
||||
showAlert(message: string): void {
|
||||
alert(message);
|
||||
this.lastAction = 'Alert shown';
|
||||
}
|
||||
|
||||
resetDemo(): void {
|
||||
this.lastAction = '';
|
||||
console.log('Demo reset');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
@import "../../../../../shared-ui/src/styles/semantic";
|
||||
|
||||
.carousel-demo {
|
||||
padding: $semantic-spacing-layout-lg;
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-section-md;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: $semantic-typography-font-size-xl;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
line-height: $semantic-typography-line-height-normal;
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
font-size: $semantic-typography-font-size-md;
|
||||
font-weight: $semantic-typography-font-weight-normal;
|
||||
line-height: $semantic-typography-line-height-relaxed;
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
.carousel-container {
|
||||
margin-bottom: $semantic-spacing-layout-lg;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-content-paragraph;
|
||||
margin-top: $semantic-spacing-layout-lg;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: $semantic-color-surface-container;
|
||||
border-radius: 12px;
|
||||
padding: $semantic-spacing-content-paragraph;
|
||||
border: 1px solid $semantic-color-border-primary;
|
||||
|
||||
.feature-title {
|
||||
font-size: $semantic-typography-font-size-md;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
line-height: $semantic-typography-line-height-normal;
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.feature-description {
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
font-weight: $semantic-typography-font-weight-normal;
|
||||
line-height: $semantic-typography-line-height-normal;
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.feature-example {
|
||||
background: $semantic-color-surface-variant;
|
||||
border-radius: 8px;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border: 1px solid $semantic-color-border-secondary;
|
||||
|
||||
.example-label {
|
||||
font-size: $semantic-typography-font-size-xs;
|
||||
font-weight: $semantic-typography-font-weight-normal;
|
||||
line-height: $semantic-typography-line-height-normal;
|
||||
color: $semantic-color-text-tertiary;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.example-content {
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
font-weight: $semantic-typography-font-weight-normal;
|
||||
line-height: $semantic-typography-line-height-normal;
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-snippet {
|
||||
background: $semantic-color-surface-container;
|
||||
border: 1px solid $semantic-color-border-primary;
|
||||
border-radius: 8px;
|
||||
padding: $semantic-spacing-content-paragraph;
|
||||
margin-top: $semantic-spacing-content-paragraph;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
color: $semantic-color-text-primary;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CarouselComponent, CarouselItem } from '../../../../../ui-essentials/src/lib/components/data-display/carousel';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-carousel-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, CarouselComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Carousel Component Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Small</h4>
|
||||
<ui-carousel
|
||||
[items]="imageItems"
|
||||
size="sm"
|
||||
variant="image"
|
||||
[autoplay]="false">
|
||||
</ui-carousel>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Medium</h4>
|
||||
<ui-carousel
|
||||
[items]="imageItems"
|
||||
size="md"
|
||||
variant="image"
|
||||
[autoplay]="false">
|
||||
</ui-carousel>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Large</h4>
|
||||
<ui-carousel
|
||||
[items]="imageItems"
|
||||
size="lg"
|
||||
variant="image"
|
||||
[autoplay]="false">
|
||||
</ui-carousel>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Types -->
|
||||
<section class="demo-section">
|
||||
<h3>Variant Types</h3>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Image Carousel</h4>
|
||||
<ui-carousel
|
||||
[items]="imageItems"
|
||||
variant="image"
|
||||
size="md"
|
||||
[autoplay]="false">
|
||||
</ui-carousel>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Card Carousel</h4>
|
||||
<ui-carousel
|
||||
[items]="cardItems"
|
||||
variant="card"
|
||||
size="md"
|
||||
[autoplay]="false">
|
||||
</ui-carousel>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Content Carousel</h4>
|
||||
<ui-carousel
|
||||
[items]="contentItems"
|
||||
variant="content"
|
||||
size="md"
|
||||
[autoplay]="false">
|
||||
</ui-carousel>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Transition Effects -->
|
||||
<section class="demo-section">
|
||||
<h3>Transition Effects</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Slide</h4>
|
||||
<ui-carousel
|
||||
[items]="imageItems.slice(0, 3)"
|
||||
transition="slide"
|
||||
size="md"
|
||||
[autoplay]="false">
|
||||
</ui-carousel>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Fade</h4>
|
||||
<ui-carousel
|
||||
[items]="imageItems.slice(0, 3)"
|
||||
transition="fade"
|
||||
size="md"
|
||||
[autoplay]="false">
|
||||
</ui-carousel>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Scale</h4>
|
||||
<ui-carousel
|
||||
[items]="imageItems.slice(0, 3)"
|
||||
transition="scale"
|
||||
size="md"
|
||||
[autoplay]="false">
|
||||
</ui-carousel>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Indicator Styles -->
|
||||
<section class="demo-section">
|
||||
<h3>Indicator Styles</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Dots</h4>
|
||||
<ui-carousel
|
||||
[items]="imageItems.slice(0, 4)"
|
||||
indicatorStyle="dots"
|
||||
size="md"
|
||||
[autoplay]="false">
|
||||
</ui-carousel>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Bars</h4>
|
||||
<ui-carousel
|
||||
[items]="imageItems.slice(0, 4)"
|
||||
indicatorStyle="bars"
|
||||
size="md"
|
||||
[autoplay]="false">
|
||||
</ui-carousel>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Thumbnails</h4>
|
||||
<ui-carousel
|
||||
[items]="thumbnailItems"
|
||||
indicatorStyle="thumbnails"
|
||||
size="md"
|
||||
[autoplay]="false">
|
||||
</ui-carousel>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features -->
|
||||
<section class="demo-section">
|
||||
<h3>Special Features</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Autoplay</h4>
|
||||
<ui-carousel
|
||||
[items]="imageItems.slice(0, 3)"
|
||||
size="md"
|
||||
[autoplay]="true"
|
||||
[autoplayInterval]="2000">
|
||||
</ui-carousel>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>No Loop</h4>
|
||||
<ui-carousel
|
||||
[items]="imageItems.slice(0, 3)"
|
||||
size="md"
|
||||
[loop]="false"
|
||||
[autoplay]="false">
|
||||
</ui-carousel>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Disabled</h4>
|
||||
<ui-carousel
|
||||
[items]="imageItems.slice(0, 3)"
|
||||
size="md"
|
||||
[disabled]="true">
|
||||
</ui-carousel>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Example</h3>
|
||||
<ui-carousel
|
||||
[items]="interactiveItems"
|
||||
variant="card"
|
||||
size="lg"
|
||||
[autoplay]="false"
|
||||
(slideChange)="handleSlideChange($event)"
|
||||
(itemClick)="handleItemClick($event)">
|
||||
</ui-carousel>
|
||||
<p class="demo-status">Current slide: {{ currentSlide + 1 }} of {{ interactiveItems.length }}</p>
|
||||
@if (lastClickedItem) {
|
||||
<p class="demo-status">Last clicked: {{ lastClickedItem.title }}</p>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './carousel-demo.component.scss'
|
||||
})
|
||||
export class CarouselDemoComponent {
|
||||
currentSlide = 0;
|
||||
lastClickedItem: CarouselItem | null = null;
|
||||
|
||||
imageItems: CarouselItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=400&fit=crop',
|
||||
title: 'Mountain Landscape',
|
||||
subtitle: 'Serene mountain views'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1501594907352-04cda38ebc29?w=800&h=400&fit=crop',
|
||||
title: 'Ocean Waves',
|
||||
subtitle: 'Peaceful ocean scenery'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800&h=400&fit=crop',
|
||||
title: 'Forest Path',
|
||||
subtitle: 'Mystical forest trail'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1501436513145-30f24e19fcc4?w=800&h=400&fit=crop',
|
||||
title: 'Desert Sunset',
|
||||
subtitle: 'Golden desert landscape'
|
||||
}
|
||||
];
|
||||
|
||||
cardItems: CarouselItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=400&h=200&fit=crop',
|
||||
title: 'Analytics Dashboard',
|
||||
subtitle: 'Data Visualization',
|
||||
content: 'Powerful analytics tools to understand your business metrics and make data-driven decisions.'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=400&h=200&fit=crop',
|
||||
title: 'Team Collaboration',
|
||||
subtitle: 'Productivity Tools',
|
||||
content: 'Enhanced collaboration features to keep your team connected and productive from anywhere.'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1563207153-f403bf289096?w=400&h=200&fit=crop',
|
||||
title: 'Mobile Experience',
|
||||
subtitle: 'Cross-Platform',
|
||||
content: 'Seamless experience across all devices with our responsive design and mobile optimization.'
|
||||
}
|
||||
];
|
||||
|
||||
contentItems: CarouselItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Welcome to Our Platform',
|
||||
subtitle: 'Getting Started Guide',
|
||||
content: 'Discover amazing features and capabilities that will transform the way you work and collaborate with your team.'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Advanced Features',
|
||||
subtitle: 'Power User Tips',
|
||||
content: 'Unlock the full potential of our platform with advanced features designed for professional workflows.'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Success Stories',
|
||||
subtitle: 'Customer Testimonials',
|
||||
content: 'Join thousands of satisfied customers who have achieved remarkable results using our solutions.'
|
||||
}
|
||||
];
|
||||
|
||||
thumbnailItems: CarouselItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1517180102446-f3ece451e9d8?w=800&h=400&fit=crop',
|
||||
thumbnail: 'https://images.unsplash.com/photo-1517180102446-f3ece451e9d8?w=100&h=60&fit=crop',
|
||||
title: 'Urban Architecture',
|
||||
subtitle: 'Modern city design'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=800&h=400&fit=crop',
|
||||
thumbnail: 'https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=100&h=60&fit=crop',
|
||||
title: 'Natural Beauty',
|
||||
subtitle: 'Landscape photography'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=400&fit=crop',
|
||||
thumbnail: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=100&h=60&fit=crop',
|
||||
title: 'Mountain Vista',
|
||||
subtitle: 'Alpine scenery'
|
||||
}
|
||||
];
|
||||
|
||||
interactiveItems: CarouselItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1557804506-669a67965ba0?w=400&h=200&fit=crop',
|
||||
title: 'Project Alpha',
|
||||
subtitle: 'Innovation Hub',
|
||||
content: 'Revolutionary project management tools designed to streamline your workflow and boost productivity.'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=400&h=200&fit=crop',
|
||||
title: 'Team Dynamics',
|
||||
subtitle: 'Collaboration Suite',
|
||||
content: 'Advanced collaboration features that bring teams together, regardless of location or time zone.'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1504384308090-c894fdcc538d?w=400&h=200&fit=crop',
|
||||
title: 'Data Insights',
|
||||
subtitle: 'Analytics Platform',
|
||||
content: 'Comprehensive analytics and reporting tools to help you make informed business decisions.'
|
||||
}
|
||||
];
|
||||
|
||||
handleSlideChange(index: number): void {
|
||||
this.currentSlide = index;
|
||||
console.log('Slide changed to:', index);
|
||||
}
|
||||
|
||||
handleItemClick(event: {item: CarouselItem, index: number}): void {
|
||||
this.lastClickedItem = event.item;
|
||||
console.log('Item clicked:', event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './carousel-demo.component';
|
||||
@@ -0,0 +1,399 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { CheckboxComponent } from '../../../../../ui-essentials/src/lib/components/forms/checkbox';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-checkbox-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
CheckboxComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Checkbox Component Showcase</h2>
|
||||
|
||||
<!-- Basic Checkboxes -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Basic Checkboxes</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-checkbox
|
||||
label="Accept terms and conditions"
|
||||
[(ngModel)]="basicChecked1"
|
||||
(checkboxChange)="onCheckboxChange('basic-1', $event)"
|
||||
/>
|
||||
<ui-checkbox
|
||||
label="Subscribe to newsletter"
|
||||
description="Receive updates about our products and services"
|
||||
[(ngModel)]="basicChecked2"
|
||||
(checkboxChange)="onCheckboxChange('basic-2', $event)"
|
||||
/>
|
||||
<ui-checkbox
|
||||
label="Remember me"
|
||||
[(ngModel)]="basicChecked3"
|
||||
(checkboxChange)="onCheckboxChange('basic-3', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Size Variants</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-checkbox
|
||||
label="Small checkbox"
|
||||
size="sm"
|
||||
[(ngModel)]="sizeSmall"
|
||||
(checkboxChange)="onCheckboxChange('size-sm', $event)"
|
||||
/>
|
||||
<ui-checkbox
|
||||
label="Medium checkbox (default)"
|
||||
size="md"
|
||||
[(ngModel)]="sizeMedium"
|
||||
(checkboxChange)="onCheckboxChange('size-md', $event)"
|
||||
/>
|
||||
<ui-checkbox
|
||||
label="Large checkbox"
|
||||
size="lg"
|
||||
[(ngModel)]="sizeLarge"
|
||||
(checkboxChange)="onCheckboxChange('size-lg', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Colors -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Color Variants</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-checkbox
|
||||
label="Primary variant"
|
||||
variant="primary"
|
||||
[(ngModel)]="variantPrimary"
|
||||
(checkboxChange)="onCheckboxChange('variant-primary', $event)"
|
||||
/>
|
||||
<ui-checkbox
|
||||
label="Secondary variant"
|
||||
variant="secondary"
|
||||
[(ngModel)]="variantSecondary"
|
||||
(checkboxChange)="onCheckboxChange('variant-secondary', $event)"
|
||||
/>
|
||||
<ui-checkbox
|
||||
label="Success variant"
|
||||
variant="success"
|
||||
[(ngModel)]="variantSuccess"
|
||||
(checkboxChange)="onCheckboxChange('variant-success', $event)"
|
||||
/>
|
||||
<ui-checkbox
|
||||
label="Warning variant"
|
||||
variant="warning"
|
||||
[(ngModel)]="variantWarning"
|
||||
(checkboxChange)="onCheckboxChange('variant-warning', $event)"
|
||||
/>
|
||||
<ui-checkbox
|
||||
label="Danger variant"
|
||||
variant="danger"
|
||||
[(ngModel)]="variantDanger"
|
||||
(checkboxChange)="onCheckboxChange('variant-danger', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>States</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-checkbox
|
||||
label="Disabled unchecked"
|
||||
[disabled]="true"
|
||||
[(ngModel)]="stateDisabledUnchecked"
|
||||
/>
|
||||
<ui-checkbox
|
||||
label="Disabled checked"
|
||||
[disabled]="true"
|
||||
[(ngModel)]="stateDisabledChecked"
|
||||
/>
|
||||
<ui-checkbox
|
||||
label="Required field"
|
||||
[required]="true"
|
||||
[(ngModel)]="stateRequired"
|
||||
(checkboxChange)="onCheckboxChange('state-required', $event)"
|
||||
/>
|
||||
<ui-checkbox
|
||||
label="Error state"
|
||||
state="error"
|
||||
[(ngModel)]="stateError"
|
||||
(checkboxChange)="onCheckboxChange('state-error', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- With Descriptions -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>With Descriptions</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-checkbox
|
||||
label="Marketing emails"
|
||||
description="Receive promotional emails and special offers"
|
||||
[(ngModel)]="descChecked1"
|
||||
(checkboxChange)="onCheckboxChange('desc-1', $event)"
|
||||
/>
|
||||
<ui-checkbox
|
||||
label="Product updates"
|
||||
description="Get notified about new features and product announcements"
|
||||
[(ngModel)]="descChecked2"
|
||||
(checkboxChange)="onCheckboxChange('desc-2', $event)"
|
||||
/>
|
||||
<ui-checkbox
|
||||
label="Security alerts"
|
||||
description="Important notifications about your account security"
|
||||
[required]="true"
|
||||
[(ngModel)]="descChecked3"
|
||||
(checkboxChange)="onCheckboxChange('desc-3', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Controls -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Controls</h3>
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<button
|
||||
style="padding: 0.5rem 1rem; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;"
|
||||
(click)="checkAll()"
|
||||
>
|
||||
Check All
|
||||
</button>
|
||||
<button
|
||||
style="padding: 0.5rem 1rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;"
|
||||
(click)="uncheckAll()"
|
||||
>
|
||||
Uncheck All
|
||||
</button>
|
||||
<button
|
||||
style="padding: 0.5rem 1rem; background: #6f42c1; color: white; border: none; border-radius: 4px; cursor: pointer;"
|
||||
(click)="toggleDisabled()"
|
||||
>
|
||||
{{ globalDisabled() ? 'Enable All' : 'Disable All' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (lastSelection()) {
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #e3f2fd; border-radius: 4px;">
|
||||
<strong>Last selection:</strong> {{ lastSelection() }}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Code Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Code Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">
|
||||
<h4>Basic Usage:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-checkbox
|
||||
label="Accept terms"
|
||||
[(ngModel)]="isAccepted"
|
||||
(checkboxChange)="onAccept($event)">
|
||||
</ui-checkbox></code></pre>
|
||||
|
||||
<h4>With Description:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-checkbox
|
||||
label="Marketing emails"
|
||||
description="Receive promotional content"
|
||||
variant="success"
|
||||
size="lg"
|
||||
[(ngModel)]="emailOptIn">
|
||||
</ui-checkbox></code></pre>
|
||||
|
||||
<h4>Required Field:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-checkbox
|
||||
label="I agree to terms"
|
||||
[required]="true"
|
||||
state="error"
|
||||
[(ngModel)]="agreedToTerms">
|
||||
</ui-checkbox></code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Current Values -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Current Values</h3>
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; max-height: 400px; overflow-y: auto;">
|
||||
<pre>{{ getCurrentValues() }}</pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(279, 14%, 35%);
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
code {
|
||||
color: hsl(279, 14%, 15%);
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid #e9ecef;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class CheckboxDemoComponent {
|
||||
// Basic checkboxes
|
||||
basicChecked1 = signal<boolean>(false);
|
||||
basicChecked2 = signal<boolean>(true);
|
||||
basicChecked3 = signal<boolean>(false);
|
||||
|
||||
// Size variants
|
||||
sizeSmall = signal<boolean>(false);
|
||||
sizeMedium = signal<boolean>(true);
|
||||
sizeLarge = signal<boolean>(false);
|
||||
|
||||
// Color variants
|
||||
variantPrimary = signal<boolean>(true);
|
||||
variantSecondary = signal<boolean>(false);
|
||||
variantSuccess = signal<boolean>(true);
|
||||
variantWarning = signal<boolean>(false);
|
||||
variantDanger = signal<boolean>(false);
|
||||
|
||||
// States
|
||||
stateDisabledUnchecked = signal<boolean>(false);
|
||||
stateDisabledChecked = signal<boolean>(true);
|
||||
stateRequired = signal<boolean>(false);
|
||||
stateError = signal<boolean>(false);
|
||||
|
||||
// With descriptions
|
||||
descChecked1 = signal<boolean>(false);
|
||||
descChecked2 = signal<boolean>(true);
|
||||
descChecked3 = signal<boolean>(true);
|
||||
|
||||
// Control states
|
||||
globalDisabled = signal<boolean>(false);
|
||||
lastSelection = signal<string>('');
|
||||
|
||||
onCheckboxChange(checkboxId: string, checked: boolean): void {
|
||||
this.lastSelection.set(`${checkboxId}: ${checked ? 'checked' : 'unchecked'}`);
|
||||
console.log(`Checkbox ${checkboxId} changed:`, checked);
|
||||
}
|
||||
|
||||
checkAll(): void {
|
||||
// Update all non-disabled checkboxes to checked
|
||||
this.basicChecked1.set(true);
|
||||
this.basicChecked2.set(true);
|
||||
this.basicChecked3.set(true);
|
||||
this.sizeSmall.set(true);
|
||||
this.sizeMedium.set(true);
|
||||
this.sizeLarge.set(true);
|
||||
this.variantPrimary.set(true);
|
||||
this.variantSecondary.set(true);
|
||||
this.variantSuccess.set(true);
|
||||
this.variantWarning.set(true);
|
||||
this.variantDanger.set(true);
|
||||
this.stateRequired.set(true);
|
||||
this.stateError.set(true);
|
||||
this.descChecked1.set(true);
|
||||
this.descChecked2.set(true);
|
||||
this.descChecked3.set(true);
|
||||
|
||||
this.lastSelection.set('All checkboxes checked');
|
||||
}
|
||||
|
||||
uncheckAll(): void {
|
||||
// Update all non-disabled checkboxes to unchecked
|
||||
this.basicChecked1.set(false);
|
||||
this.basicChecked2.set(false);
|
||||
this.basicChecked3.set(false);
|
||||
this.sizeSmall.set(false);
|
||||
this.sizeMedium.set(false);
|
||||
this.sizeLarge.set(false);
|
||||
this.variantPrimary.set(false);
|
||||
this.variantSecondary.set(false);
|
||||
this.variantSuccess.set(false);
|
||||
this.variantWarning.set(false);
|
||||
this.variantDanger.set(false);
|
||||
this.stateRequired.set(false);
|
||||
this.stateError.set(false);
|
||||
this.descChecked1.set(false);
|
||||
this.descChecked2.set(false);
|
||||
this.descChecked3.set(false);
|
||||
|
||||
this.lastSelection.set('All checkboxes unchecked');
|
||||
}
|
||||
|
||||
toggleDisabled(): void {
|
||||
this.globalDisabled.update(disabled => !disabled);
|
||||
this.lastSelection.set(`Global disabled: ${this.globalDisabled() ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
|
||||
getCurrentValues(): string {
|
||||
const values = {
|
||||
basic: {
|
||||
checked1: this.basicChecked1(),
|
||||
checked2: this.basicChecked2(),
|
||||
checked3: this.basicChecked3()
|
||||
},
|
||||
sizes: {
|
||||
small: this.sizeSmall(),
|
||||
medium: this.sizeMedium(),
|
||||
large: this.sizeLarge()
|
||||
},
|
||||
variants: {
|
||||
primary: this.variantPrimary(),
|
||||
secondary: this.variantSecondary(),
|
||||
success: this.variantSuccess(),
|
||||
warning: this.variantWarning(),
|
||||
danger: this.variantDanger()
|
||||
},
|
||||
states: {
|
||||
disabledUnchecked: this.stateDisabledUnchecked(),
|
||||
disabledChecked: this.stateDisabledChecked(),
|
||||
required: this.stateRequired(),
|
||||
error: this.stateError()
|
||||
},
|
||||
descriptions: {
|
||||
desc1: this.descChecked1(),
|
||||
desc2: this.descChecked2(),
|
||||
desc3: this.descChecked3()
|
||||
},
|
||||
controls: {
|
||||
globalDisabled: this.globalDisabled(),
|
||||
lastSelection: this.lastSelection()
|
||||
}
|
||||
};
|
||||
|
||||
return JSON.stringify(values, null, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChipComponent } from '../../../../../ui-essentials/src/lib/components/data-display/chip';
|
||||
import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
|
||||
import {
|
||||
faHeart,
|
||||
faUser,
|
||||
faStar,
|
||||
faTag,
|
||||
faHome,
|
||||
faEnvelope,
|
||||
faPhone,
|
||||
faMapMarker,
|
||||
faCalendar,
|
||||
faCog,
|
||||
faTimes,
|
||||
faPlus,
|
||||
faCheck
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
interface ChipData {
|
||||
id: string;
|
||||
label: string;
|
||||
selected: boolean;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-chip-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ChipComponent,
|
||||
ButtonComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Chip Component Showcase</h2>
|
||||
|
||||
<!-- Basic Chip Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Chip Variants</h3>
|
||||
<div class="ui-chip-set" style="margin-bottom: 1.5rem;">
|
||||
<ui-chip variant="filled" label="Filled Chip" [clickable]="true" (chipClick)="handleChipClick('filled')"></ui-chip>
|
||||
<ui-chip variant="outlined" label="Outlined Chip" [clickable]="true" (chipClick)="handleChipClick('outlined')"></ui-chip>
|
||||
<ui-chip variant="elevated" label="Elevated Chip" [clickable]="true" (chipClick)="handleChipClick('elevated')"></ui-chip>
|
||||
</div>
|
||||
|
||||
<h4>Selected State</h4>
|
||||
<div class="ui-chip-set" style="margin-bottom: 1.5rem;">
|
||||
<ui-chip variant="filled" label="Selected Filled" [selected]="true" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="outlined" label="Selected Outlined" [selected]="true" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="elevated" label="Selected Elevated" [selected]="true" [clickable]="true"></ui-chip>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Size Variants</h3>
|
||||
<div class="ui-chip-set" style="margin-bottom: 1.5rem;">
|
||||
<ui-chip variant="filled" size="sm" label="Small" [clickable]="true" (chipClick)="handleChipClick('small')"></ui-chip>
|
||||
<ui-chip variant="filled" size="md" label="Medium" [clickable]="true" (chipClick)="handleChipClick('medium')"></ui-chip>
|
||||
<ui-chip variant="filled" size="lg" label="Large" [clickable]="true" (chipClick)="handleChipClick('large')"></ui-chip>
|
||||
</div>
|
||||
|
||||
<h4>With Icons</h4>
|
||||
<div class="ui-chip-set" style="margin-bottom: 1.5rem;">
|
||||
<ui-chip variant="outlined" size="sm" label="Small" [leadingIcon]="faHeart" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="outlined" size="md" label="Medium" [leadingIcon]="faStar" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="outlined" size="lg" label="Large" [leadingIcon]="faUser" [clickable]="true"></ui-chip>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Radius Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Corner Radius Variants</h3>
|
||||
<div class="ui-chip-set" style="margin-bottom: 1.5rem;">
|
||||
<ui-chip variant="filled" radius="none" label="No Radius" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="filled" radius="sm" label="Small" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="filled" radius="md" label="Medium" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="filled" radius="lg" label="Large" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="filled" radius="capsule" label="Capsule" [clickable]="true"></ui-chip>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Icons and Avatars -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Chips with Icons</h3>
|
||||
|
||||
<h4>Leading Icons</h4>
|
||||
<div class="ui-chip-set" style="margin-bottom: 1.5rem;">
|
||||
<ui-chip variant="outlined" label="Home" [leadingIcon]="faHome" [clickable]="true" (chipClick)="handleChipClick('home')"></ui-chip>
|
||||
<ui-chip variant="outlined" label="Email" [leadingIcon]="faEnvelope" [clickable]="true" (chipClick)="handleChipClick('email')"></ui-chip>
|
||||
<ui-chip variant="outlined" label="Phone" [leadingIcon]="faPhone" [clickable]="true" (chipClick)="handleChipClick('phone')"></ui-chip>
|
||||
<ui-chip variant="outlined" label="Location" [leadingIcon]="faMapMarker" [clickable]="true" (chipClick)="handleChipClick('location')"></ui-chip>
|
||||
</div>
|
||||
|
||||
<h4>Trailing Icons</h4>
|
||||
<div class="ui-chip-set" style="margin-bottom: 1.5rem;">
|
||||
<ui-chip variant="filled" label="Settings" [trailingIcon]="faCog" [clickable]="true" (chipClick)="handleChipClick('settings')"></ui-chip>
|
||||
<ui-chip variant="filled" label="Calendar" [trailingIcon]="faCalendar" [clickable]="true" (chipClick)="handleChipClick('calendar')"></ui-chip>
|
||||
<ui-chip variant="filled" label="Favorite" [trailingIcon]="faHeart" [clickable]="true" (chipClick)="handleChipClick('favorite')"></ui-chip>
|
||||
</div>
|
||||
|
||||
<h4>With Avatars</h4>
|
||||
<div class="ui-chip-set" style="margin-bottom: 1.5rem;">
|
||||
<ui-chip
|
||||
variant="outlined"
|
||||
label="John Doe"
|
||||
avatar="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop&crop=face"
|
||||
avatarAlt="John Doe"
|
||||
[clickable]="true"
|
||||
(chipClick)="handleChipClick('john')">
|
||||
</ui-chip>
|
||||
<ui-chip
|
||||
variant="outlined"
|
||||
label="Jane Smith"
|
||||
avatar="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face"
|
||||
avatarAlt="Jane Smith"
|
||||
[clickable]="true"
|
||||
(chipClick)="handleChipClick('jane')">
|
||||
</ui-chip>
|
||||
<ui-chip
|
||||
variant="outlined"
|
||||
label="Alex Johnson"
|
||||
avatar="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face"
|
||||
avatarAlt="Alex Johnson"
|
||||
[clickable]="true"
|
||||
(chipClick)="handleChipClick('alex')">
|
||||
</ui-chip>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Removable Chips -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Removable Chips</h3>
|
||||
<div class="ui-chip-set" style="margin-bottom: 1.5rem;">
|
||||
@for (tag of removableTags; track tag.id) {
|
||||
<ui-chip
|
||||
variant="filled"
|
||||
[label]="tag.label"
|
||||
[removable]="true"
|
||||
[closeIcon]="faTimes"
|
||||
[clickable]="true"
|
||||
(chipClick)="handleChipClick(tag.id)"
|
||||
(chipRemove)="removeTag(tag.id)">
|
||||
</ui-chip>
|
||||
}
|
||||
</div>
|
||||
|
||||
<h4>Removable with Icons</h4>
|
||||
<div class="ui-chip-set" style="margin-bottom: 1.5rem;">
|
||||
@for (tagWithIcon of removableTagsWithIcons; track tagWithIcon.id) {
|
||||
<ui-chip
|
||||
variant="outlined"
|
||||
[label]="tagWithIcon.label"
|
||||
[leadingIcon]="tagWithIcon.icon"
|
||||
[removable]="true"
|
||||
[closeIcon]="faTimes"
|
||||
[clickable]="true"
|
||||
(chipClick)="handleChipClick(tagWithIcon.id)"
|
||||
(chipRemove)="removeTagWithIcon(tagWithIcon.id)">
|
||||
</ui-chip>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ui-button variant="outlined" size="small" [icon]="faPlus" iconPosition="left" (clicked)="addTags()">
|
||||
Add Tags Back
|
||||
</ui-button>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Selection -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Selection</h3>
|
||||
<p>Click chips to toggle selection:</p>
|
||||
<div class="ui-chip-set" style="margin-bottom: 1.5rem;">
|
||||
@for (item of selectableItems; track item.id) {
|
||||
<ui-chip
|
||||
variant="outlined"
|
||||
[label]="item.label"
|
||||
[selected]="item.selected"
|
||||
[disabled]="item.disabled"
|
||||
[clickable]="true"
|
||||
(chipClick)="toggleSelection(item.id)">
|
||||
</ui-chip>
|
||||
}
|
||||
</div>
|
||||
|
||||
<p><strong>Selected:</strong> {{ getSelectedItems() }}</p>
|
||||
<ui-button variant="tonal" size="small" (clicked)="clearSelection()">Clear Selection</ui-button>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Chip States</h3>
|
||||
|
||||
<h4>Interactive States</h4>
|
||||
<div class="ui-chip-set" style="margin-bottom: 1.5rem;">
|
||||
<ui-chip variant="filled" label="Normal" [clickable]="true" (chipClick)="handleChipClick('normal')"></ui-chip>
|
||||
<ui-chip variant="filled" label="Selected" [selected]="true" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="filled" label="Disabled" [disabled]="true"></ui-chip>
|
||||
<ui-chip variant="filled" label="Disabled Selected" [disabled]="true" [selected]="true"></ui-chip>
|
||||
</div>
|
||||
|
||||
<h4>Non-interactive (Labels)</h4>
|
||||
<div class="ui-chip-set" style="margin-bottom: 1.5rem;">
|
||||
<ui-chip variant="outlined" radius="capsule" label="Status: Active" [leadingIcon]="faCheck"></ui-chip>
|
||||
<ui-chip variant="outlined" radius="capsule" label="Category: Design"></ui-chip>
|
||||
<ui-chip variant="outlined" radius="capsule" label="Priority: High"></ui-chip>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Chip Sets -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Chip Sets</h3>
|
||||
|
||||
<h4>Horizontal Scrolling Set</h4>
|
||||
<div class="ui-chip-set ui-chip-set--row" style="margin-bottom: 1.5rem; max-width: 500px; border: 1px dashed #ccc; padding: 1rem;">
|
||||
<ui-chip variant="filled" label="Technology" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="filled" label="Programming" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="filled" label="Web Development" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="filled" label="User Interface" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="filled" label="User Experience" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="filled" label="Design Systems" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="filled" label="Frontend" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="filled" label="Backend" [clickable]="true"></ui-chip>
|
||||
</div>
|
||||
|
||||
<h4>Wrapped Set</h4>
|
||||
<div class="ui-chip-set" style="margin-bottom: 1.5rem; max-width: 500px; border: 1px dashed #ccc; padding: 1rem;">
|
||||
<ui-chip variant="outlined" label="Angular" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="outlined" label="TypeScript" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="outlined" label="SCSS" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="outlined" label="HTML" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="outlined" label="CSS" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="outlined" label="JavaScript" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="outlined" label="Responsive Design" [clickable]="true"></ui-chip>
|
||||
<ui-chip variant="outlined" label="Accessibility" [clickable]="true"></ui-chip>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Usage Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Basic Chip:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-chip
|
||||
variant="filled"
|
||||
label="Basic Chip"
|
||||
[clickable]="true"
|
||||
(chipClick)="handleClick($event)">
|
||||
</ui-chip></code></pre>
|
||||
|
||||
<h4>Chip with Icon:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-chip
|
||||
variant="outlined"
|
||||
label="Home"
|
||||
[leadingIcon]="faHome"
|
||||
[clickable]="true"
|
||||
(chipClick)="goHome()">
|
||||
</ui-chip></code></pre>
|
||||
|
||||
<h4>Removable Chip:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-chip
|
||||
variant="filled"
|
||||
label="Tag Name"
|
||||
[removable]="true"
|
||||
[closeIcon]="faTimes"
|
||||
(chipRemove)="removeTag($event)">
|
||||
</ui-chip></code></pre>
|
||||
|
||||
<h4>Chip with Avatar:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-chip
|
||||
variant="outlined"
|
||||
label="John Doe"
|
||||
avatar="path/to/avatar.jpg"
|
||||
avatarAlt="John Doe"
|
||||
[clickable]="true"
|
||||
(chipClick)="viewProfile('john')">
|
||||
</ui-chip></code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Actions -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Actions</h3>
|
||||
@if (lastAction) {
|
||||
<div style="margin-bottom: 1rem; padding: 1rem; background: #e3f2fd; border-radius: 4px;">
|
||||
<strong>Last action:</strong> {{ lastAction }}
|
||||
</div>
|
||||
}
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||
<ui-button variant="filled" (clicked)="resetDemo()">Reset Demo</ui-button>
|
||||
<ui-button variant="tonal" (clicked)="showAlert('Chip showcase complete!')">Show Alert</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(287, 12%, 35%);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid hsl(289, 14%, 90%);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: hsl(286, 20%, 99%);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #d63384;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class ChipDemoComponent {
|
||||
lastAction: string = '';
|
||||
|
||||
// FontAwesome icons
|
||||
faHeart = faHeart;
|
||||
faUser = faUser;
|
||||
faStar = faStar;
|
||||
faTag = faTag;
|
||||
faHome = faHome;
|
||||
faEnvelope = faEnvelope;
|
||||
faPhone = faPhone;
|
||||
faMapMarker = faMapMarker;
|
||||
faCalendar = faCalendar;
|
||||
faCog = faCog;
|
||||
faTimes = faTimes;
|
||||
faPlus = faPlus;
|
||||
faCheck = faCheck;
|
||||
|
||||
removableTags: ChipData[] = [
|
||||
{ id: 'react', label: 'React', selected: false, disabled: false },
|
||||
{ id: 'angular', label: 'Angular', selected: false, disabled: false },
|
||||
{ id: 'vue', label: 'Vue.js', selected: false, disabled: false },
|
||||
{ id: 'svelte', label: 'Svelte', selected: false, disabled: false }
|
||||
];
|
||||
|
||||
removableTagsWithIcons: any[] = [
|
||||
{ id: 'home-tag', label: 'Home', icon: faHome },
|
||||
{ id: 'email-tag', label: 'Email', icon: faEnvelope },
|
||||
{ id: 'star-tag', label: 'Starred', icon: faStar }
|
||||
];
|
||||
|
||||
selectableItems: ChipData[] = [
|
||||
{ id: 'option1', label: 'Option 1', selected: false, disabled: false },
|
||||
{ id: 'option2', label: 'Option 2', selected: true, disabled: false },
|
||||
{ id: 'option3', label: 'Option 3', selected: false, disabled: false },
|
||||
{ id: 'option4', label: 'Option 4', selected: false, disabled: false },
|
||||
{ id: 'option5', label: 'Disabled', selected: false, disabled: true }
|
||||
];
|
||||
|
||||
private originalRemovableTags = [...this.removableTags];
|
||||
private originalRemovableTagsWithIcons = [...this.removableTagsWithIcons];
|
||||
|
||||
handleChipClick(chipId: string): void {
|
||||
this.lastAction = `Chip clicked: ${chipId}`;
|
||||
console.log(`Chip clicked: ${chipId}`);
|
||||
}
|
||||
|
||||
removeTag(tagId: string): void {
|
||||
this.removableTags = this.removableTags.filter(tag => tag.id !== tagId);
|
||||
this.lastAction = `Tag removed: ${tagId}`;
|
||||
console.log(`Tag removed: ${tagId}`);
|
||||
}
|
||||
|
||||
removeTagWithIcon(tagId: string): void {
|
||||
this.removableTagsWithIcons = this.removableTagsWithIcons.filter(tag => tag.id !== tagId);
|
||||
this.lastAction = `Tag with icon removed: ${tagId}`;
|
||||
console.log(`Tag with icon removed: ${tagId}`);
|
||||
}
|
||||
|
||||
addTags(): void {
|
||||
this.removableTags = [...this.originalRemovableTags];
|
||||
this.removableTagsWithIcons = [...this.originalRemovableTagsWithIcons];
|
||||
this.lastAction = 'Tags restored';
|
||||
console.log('Tags restored');
|
||||
}
|
||||
|
||||
toggleSelection(itemId: string): void {
|
||||
const item = this.selectableItems.find(i => i.id === itemId);
|
||||
if (item && !item.disabled) {
|
||||
item.selected = !item.selected;
|
||||
this.lastAction = `Selection toggled: ${itemId} (${item.selected ? 'selected' : 'deselected'})`;
|
||||
console.log(`Selection toggled: ${itemId}`, item.selected);
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedItems(): string {
|
||||
return this.selectableItems
|
||||
.filter(item => item.selected)
|
||||
.map(item => item.label)
|
||||
.join(', ') || 'None';
|
||||
}
|
||||
|
||||
clearSelection(): void {
|
||||
this.selectableItems.forEach(item => {
|
||||
if (!item.disabled) {
|
||||
item.selected = false;
|
||||
}
|
||||
});
|
||||
this.lastAction = 'Selection cleared';
|
||||
console.log('Selection cleared');
|
||||
}
|
||||
|
||||
showAlert(message: string): void {
|
||||
alert(message);
|
||||
this.lastAction = 'Alert shown';
|
||||
}
|
||||
|
||||
resetDemo(): void {
|
||||
this.lastAction = '';
|
||||
this.clearSelection();
|
||||
this.addTags();
|
||||
console.log('Demo reset');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-wrapper {
|
||||
padding: $semantic-spacing-layout-md;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden; // Prevent horizontal overflow
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-xl;
|
||||
width: 100%;
|
||||
overflow-x: hidden; // Prevent horizontal overflow
|
||||
|
||||
h2 {
|
||||
margin-bottom: $semantic-spacing-layout-md;
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
color: $semantic-color-text-secondary;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-containers-showcase {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
margin-bottom: $semantic-spacing-layout-lg;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden; // Prevent horizontal overflow
|
||||
box-sizing: border-box;
|
||||
|
||||
// Ensure all child containers are properly constrained
|
||||
.ui-container {
|
||||
max-width: min(100%, var(--container-max-width, 100%));
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-container-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center; // Center the containers
|
||||
overflow-x: hidden; // Prevent horizontal overflow
|
||||
}
|
||||
|
||||
.demo-label {
|
||||
font-size: 0.875rem;
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.demo-container-outline {
|
||||
border: 2px dashed $semantic-color-border-subtle;
|
||||
background: rgba($semantic-color-container-primary, 0.05);
|
||||
max-width: 100%; // Ensure containers don't overflow their parent
|
||||
box-sizing: border-box; // Include padding and border in width calculation
|
||||
margin: 0 auto; // Center the container
|
||||
position: relative; // For proper positioning context
|
||||
|
||||
// Override container component's auto margins that might be causing issues
|
||||
&.ui-container {
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
}
|
||||
|
||||
&.demo-container-tall {
|
||||
min-height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
color: $semantic-color-text-primary;
|
||||
text-align: center;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-wrap: break-word; // Handle long text properly
|
||||
word-wrap: break-word;
|
||||
|
||||
&.hero-content {
|
||||
h2 {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $semantic-color-text-secondary;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-variant-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
}
|
||||
|
||||
.demo-variant-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-flex-item {
|
||||
background: $semantic-color-container-secondary;
|
||||
color: $semantic-color-on-container-secondary;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
width: 100%; // Ensure flex items take full width
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0; // Prevent shrinking
|
||||
}
|
||||
|
||||
.demo-grid-item {
|
||||
background: $semantic-color-container-tertiary;
|
||||
color: $semantic-color-on-container-tertiary;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.demo-paragraph {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
color: $semantic-color-text-primary;
|
||||
line-height: 1.5;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-wide-content {
|
||||
white-space: nowrap;
|
||||
color: $semantic-color-text-primary;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
// Feature cards styling
|
||||
:host ::ng-deep {
|
||||
.demo-section ui-container[variant="card"] {
|
||||
h3 {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $semantic-color-text-secondary;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Additional container demo specific styles
|
||||
.demo-containers-showcase {
|
||||
// Force all containers to behave properly
|
||||
.ui-container {
|
||||
// Ensure containers don't push beyond boundaries
|
||||
position: relative;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
// Size-specific constraints to prevent overflow
|
||||
.ui-container--xs { --container-max-width: 475px; }
|
||||
.ui-container--sm { --container-max-width: 640px; }
|
||||
.ui-container--md { --container-max-width: 768px; }
|
||||
.ui-container--lg { --container-max-width: 1024px; }
|
||||
.ui-container--xl { --container-max-width: 1280px; }
|
||||
.ui-container--2xl { --container-max-width: 1536px; }
|
||||
.ui-container--full { --container-max-width: 100%; }
|
||||
|
||||
// Fix flex container alignment issues
|
||||
.ui-container--flex {
|
||||
align-items: stretch; // Default to stretch for consistent layout
|
||||
|
||||
&:not(.ui-container--flex-center):not(.ui-container--flex-start):not(.ui-container--flex-end) {
|
||||
align-items: stretch; // Force default alignment for flex containers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
// Force smaller containers on tablet
|
||||
.demo-containers-showcase .ui-container {
|
||||
max-width: min(100%, var(--container-max-width, 100%)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-variant-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-containers-showcase {
|
||||
gap: $semantic-spacing-component-md;
|
||||
|
||||
// Force all containers to be full width on mobile
|
||||
.ui-container {
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-wrapper {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ContainerComponent, GridSystemComponent } from '../../../../../ui-essentials/src/lib/components/layout';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-container-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ContainerComponent, GridSystemComponent],
|
||||
template: `
|
||||
<div class="demo-wrapper">
|
||||
<h2>Container Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
<div class="demo-containers-showcase">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-container-wrapper">
|
||||
<div class="demo-label">{{ size }} (max-width: {{ getMaxWidth(size) }})</div>
|
||||
<ui-container [size]="size" class="demo-container-outline">
|
||||
<div class="demo-content">{{ size }} container content</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Types -->
|
||||
<section class="demo-section">
|
||||
<h3>Container Variants</h3>
|
||||
|
||||
<div class="demo-variant-grid">
|
||||
<div class="demo-variant-item">
|
||||
<h4>Default Container</h4>
|
||||
<ui-container size="md" class="demo-container-outline">
|
||||
<div class="demo-content">Basic container with standard padding and centering</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-variant-item">
|
||||
<h4>Card Container</h4>
|
||||
<ui-container size="md" variant="card">
|
||||
<div class="demo-content">Card-style container with background, border, and shadow</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-variant-item">
|
||||
<h4>Section Container</h4>
|
||||
<ui-container size="lg" variant="section" background="surface">
|
||||
<div class="demo-content">Section container with larger vertical padding for page sections</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-variant-item">
|
||||
<h4>Hero Container</h4>
|
||||
<ui-container size="xl" variant="hero" background="surface-elevated">
|
||||
<div class="demo-content hero-content">
|
||||
<h2>Hero Section</h2>
|
||||
<p>Large padding and centered content for hero sections</p>
|
||||
</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Flex Containers -->
|
||||
<section class="demo-section">
|
||||
<h3>Flex Container Variants</h3>
|
||||
|
||||
<div class="demo-variant-grid">
|
||||
<div class="demo-variant-item">
|
||||
<h4>Flex Column (Default)</h4>
|
||||
<ui-container size="sm" variant="flex" class="demo-container-outline">
|
||||
<div class="demo-flex-item">Item 1</div>
|
||||
<div class="demo-flex-item">Item 2</div>
|
||||
<div class="demo-flex-item">Item 3</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-variant-item">
|
||||
<h4>Flex Row</h4>
|
||||
<ui-container size="sm" variant="flex" flexDirection="row" class="demo-container-outline">
|
||||
<div class="demo-flex-item">Item 1</div>
|
||||
<div class="demo-flex-item">Item 2</div>
|
||||
<div class="demo-flex-item">Item 3</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-variant-item">
|
||||
<h4>Flex Center</h4>
|
||||
<ui-container size="sm" variant="flex" flexJustify="center" class="demo-container-outline demo-container-tall">
|
||||
<div class="demo-flex-item">Centered</div>
|
||||
<div class="demo-flex-item">Content</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-variant-item">
|
||||
<h4>Flex Space Between</h4>
|
||||
<ui-container size="sm" variant="flex" flexDirection="row" flexJustify="between" class="demo-container-outline">
|
||||
<div class="demo-flex-item">Start</div>
|
||||
<div class="demo-flex-item">End</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Grid Containers -->
|
||||
<section class="demo-section">
|
||||
<h3>Grid Container Variants</h3>
|
||||
|
||||
<div class="demo-variant-grid">
|
||||
<div class="demo-variant-item">
|
||||
<h4>Grid Auto</h4>
|
||||
<ui-container size="lg" variant="grid" gridColumns="auto" class="demo-container-outline">
|
||||
@for (item of getGridItems(6); track $index) {
|
||||
<div class="demo-grid-item">{{ item }}</div>
|
||||
}
|
||||
</ui-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-variant-item">
|
||||
<h4>Grid 3 Columns</h4>
|
||||
<ui-container size="lg" variant="grid" [gridColumns]="3" class="demo-container-outline">
|
||||
@for (item of getGridItems(6); track $index) {
|
||||
<div class="demo-grid-item">{{ item }}</div>
|
||||
}
|
||||
</ui-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Padding Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Padding Variants</h3>
|
||||
|
||||
<div class="demo-variant-grid">
|
||||
@for (padding of paddingSizes; track padding) {
|
||||
<div class="demo-variant-item">
|
||||
<h4>Padding: {{ padding }}</h4>
|
||||
<ui-container size="sm" [padding]="padding" class="demo-container-outline">
|
||||
<div class="demo-content">{{ padding }} padding content</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Background Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Background Variants</h3>
|
||||
|
||||
<div class="demo-variant-grid">
|
||||
@for (bg of backgrounds; track bg) {
|
||||
<div class="demo-variant-item">
|
||||
<h4>Background: {{ bg }}</h4>
|
||||
<ui-container size="sm" [background]="bg" padding="md" class="demo-container-outline">
|
||||
<div class="demo-content">{{ bg }} background</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scrollable Container -->
|
||||
<section class="demo-section">
|
||||
<h3>Scrollable Containers</h3>
|
||||
|
||||
<div class="demo-variant-grid">
|
||||
<div class="demo-variant-item">
|
||||
<h4>Vertical Scroll</h4>
|
||||
<ui-container size="sm" scrollable="y" customMaxHeight="200px" variant="card">
|
||||
@for (item of getLongContent(); track $index) {
|
||||
<p class="demo-paragraph">{{ item }}</p>
|
||||
}
|
||||
</ui-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-variant-item">
|
||||
<h4>Horizontal Scroll</h4>
|
||||
<ui-container size="sm" scrollable="x" variant="card">
|
||||
<div class="demo-wide-content">
|
||||
This is a very long line of content that will cause horizontal scrolling when it exceeds the container width. Keep reading to see the scroll behavior in action.
|
||||
</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Real World Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Real World Example</h3>
|
||||
<ui-container size="lg" variant="section" background="surface">
|
||||
<ui-container size="md" variant="centered">
|
||||
<h2>Product Features</h2>
|
||||
<p>Discover what makes our product special</p>
|
||||
|
||||
<ui-container variant="grid" [gridColumns]="3" customMaxWidth="800px">
|
||||
@for (feature of features; track feature.title) {
|
||||
<ui-container variant="card" padding="md">
|
||||
<h3>{{ feature.title }}</h3>
|
||||
<p>{{ feature.description }}</p>
|
||||
</ui-container>
|
||||
}
|
||||
</ui-container>
|
||||
</ui-container>
|
||||
</ui-container>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './container-demo.component.scss'
|
||||
})
|
||||
export class ContainerDemoComponent {
|
||||
sizes: ('xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full')[] = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', 'full'];
|
||||
paddingSizes: ('xs' | 'sm' | 'md' | 'lg' | 'xl' | 'none')[] = ['xs', 'sm', 'md', 'lg', 'xl', 'none'];
|
||||
backgrounds: ('transparent' | 'surface' | 'surface-secondary' | 'surface-elevated')[] = [
|
||||
'transparent', 'surface', 'surface-secondary', 'surface-elevated'
|
||||
];
|
||||
|
||||
features = [
|
||||
{ title: 'Fast', description: 'Lightning-fast performance optimized for speed' },
|
||||
{ title: 'Secure', description: 'Enterprise-grade security built from the ground up' },
|
||||
{ title: 'Scalable', description: 'Grows with your business needs seamlessly' },
|
||||
{ title: 'Reliable', description: '99.9% uptime guaranteed with redundant systems' },
|
||||
{ title: 'Easy to Use', description: 'Intuitive interface designed for productivity' },
|
||||
{ title: 'Supported', description: '24/7 customer support when you need it' }
|
||||
];
|
||||
|
||||
getMaxWidth(size: string): string {
|
||||
const sizes: Record<string, string> = {
|
||||
'xs': '475px',
|
||||
'sm': '640px',
|
||||
'md': '768px',
|
||||
'lg': '1024px',
|
||||
'xl': '1280px',
|
||||
'2xl': '1536px',
|
||||
'full': 'none'
|
||||
};
|
||||
return sizes[size] || 'auto';
|
||||
}
|
||||
|
||||
getGridItems(count: number): string[] {
|
||||
return Array.from({ length: count }, (_, i) => `Item ${i + 1}`);
|
||||
}
|
||||
|
||||
getLongContent(): string[] {
|
||||
return [
|
||||
'This is paragraph 1 with some content to demonstrate vertical scrolling.',
|
||||
'This is paragraph 2 with more content to show how the container handles overflow.',
|
||||
'This is paragraph 3 continuing the demonstration of scrollable content.',
|
||||
'This is paragraph 4 adding even more content to ensure scrolling is needed.',
|
||||
'This is paragraph 5 with the final bit of content to complete the demo.',
|
||||
'This is paragraph 6 making sure we have enough content for scroll.',
|
||||
'This is paragraph 7 - almost done with our scrollable content demo.',
|
||||
'This is paragraph 8 - the last paragraph in our scrollable demo.'
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
.demo-container {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
h4 {
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-interactive {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.demo-output {
|
||||
h4 {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #6b7280;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-form {
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.submit-btn, .reset-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: 1px solid #3b82f6;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #2563eb;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: transparent;
|
||||
color: #6b7280;
|
||||
border: 1px solid #d1d5db;
|
||||
|
||||
&:hover {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
}
|
||||
|
||||
.form-status {
|
||||
background: #f3f4f6;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
|
||||
p {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #e5e7eb;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-interactive {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DatePickerComponent } from '../../../../../ui-essentials/src/lib/components/forms/date-picker/date-picker.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-date-picker-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, DatePickerComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Date Picker Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ size | uppercase }}</h4>
|
||||
<ui-date-picker
|
||||
[size]="size"
|
||||
[label]="size + ' Date Picker'"
|
||||
[placeholder]="'Select date'"
|
||||
[(ngModel)]="sampleValues[size]">
|
||||
</ui-date-picker>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Styles -->
|
||||
<section class="demo-section">
|
||||
<h3>Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ variant | titlecase }}</h4>
|
||||
<ui-date-picker
|
||||
[variant]="variant"
|
||||
[label]="variant + ' Variant'"
|
||||
[placeholder]="'Select date'"
|
||||
[(ngModel)]="variantValues[variant]">
|
||||
</ui-date-picker>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Default</h4>
|
||||
<ui-date-picker
|
||||
label="Default State"
|
||||
placeholder="Select date"
|
||||
helperText="Choose your preferred date"
|
||||
[(ngModel)]="stateValues['default']">
|
||||
</ui-date-picker>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Error</h4>
|
||||
<ui-date-picker
|
||||
label="Error State"
|
||||
placeholder="Select date"
|
||||
state="error"
|
||||
errorMessage="Please select a valid date"
|
||||
[(ngModel)]="stateValues['error']">
|
||||
</ui-date-picker>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Success</h4>
|
||||
<ui-date-picker
|
||||
label="Success State"
|
||||
placeholder="Select date"
|
||||
state="success"
|
||||
helperText="Great choice!"
|
||||
[(ngModel)]="stateValues['success']">
|
||||
</ui-date-picker>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Warning</h4>
|
||||
<ui-date-picker
|
||||
label="Warning State"
|
||||
placeholder="Select date"
|
||||
state="warning"
|
||||
helperText="Date is in the past"
|
||||
[(ngModel)]="stateValues['warning']">
|
||||
</ui-date-picker>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Special Features -->
|
||||
<section class="demo-section">
|
||||
<h3>Features</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Required Field</h4>
|
||||
<ui-date-picker
|
||||
label="Required Date"
|
||||
placeholder="Select date"
|
||||
[required]="true"
|
||||
helperText="This field is required"
|
||||
[(ngModel)]="featureValues['required']">
|
||||
</ui-date-picker>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Disabled</h4>
|
||||
<ui-date-picker
|
||||
label="Disabled Date Picker"
|
||||
placeholder="Cannot select"
|
||||
[disabled]="true"
|
||||
helperText="This field is disabled"
|
||||
[(ngModel)]="featureValues['disabled']">
|
||||
</ui-date-picker>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Not Clearable</h4>
|
||||
<ui-date-picker
|
||||
label="No Clear Button"
|
||||
placeholder="Select date"
|
||||
[clearable]="false"
|
||||
helperText="Clear button is hidden"
|
||||
[(ngModel)]="featureValues['notClearable']">
|
||||
</ui-date-picker>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Date Range Limits</h4>
|
||||
<ui-date-picker
|
||||
label="Limited Date Range"
|
||||
placeholder="Select date"
|
||||
[minDate]="minDate"
|
||||
[maxDate]="maxDate"
|
||||
helperText="Only dates in 2024 allowed"
|
||||
[(ngModel)]="featureValues['limited']">
|
||||
</ui-date-picker>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Example</h3>
|
||||
<div class="demo-interactive">
|
||||
<ui-date-picker
|
||||
label="Event Date"
|
||||
placeholder="Select event date"
|
||||
helperText="Choose a date for your event"
|
||||
[(ngModel)]="interactiveValue"
|
||||
(dateChange)="onDateChange($event)">
|
||||
</ui-date-picker>
|
||||
|
||||
<div class="demo-output">
|
||||
<h4>Selected Date:</h4>
|
||||
<p>{{ interactiveValue ? formatDate(interactiveValue) : 'No date selected' }}</p>
|
||||
<p><strong>Change count:</strong> {{ changeCount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Form Integration -->
|
||||
<section class="demo-section">
|
||||
<h3>Form Integration</h3>
|
||||
<form (ngSubmit)="onSubmit()" #demoForm="ngForm" class="demo-form">
|
||||
<div class="form-row">
|
||||
<ui-date-picker
|
||||
label="Start Date"
|
||||
name="startDate"
|
||||
placeholder="Select start date"
|
||||
[(ngModel)]="formValues.startDate"
|
||||
[required]="true"
|
||||
#startDate="ngModel">
|
||||
</ui-date-picker>
|
||||
|
||||
<ui-date-picker
|
||||
label="End Date"
|
||||
name="endDate"
|
||||
placeholder="Select end date"
|
||||
[(ngModel)]="formValues.endDate"
|
||||
[minDate]="formValues.startDate"
|
||||
[required]="true"
|
||||
#endDate="ngModel">
|
||||
</ui-date-picker>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" [disabled]="!demoForm.valid" class="submit-btn">
|
||||
Submit Form
|
||||
</button>
|
||||
<button type="button" (click)="resetForm(demoForm)" class="reset-btn">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-status">
|
||||
<p><strong>Form Valid:</strong> {{ demoForm.valid }}</p>
|
||||
<p><strong>Form Values:</strong></p>
|
||||
<pre>{{ formValues | json }}</pre>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './date-picker-demo.component.scss'
|
||||
})
|
||||
export class DatePickerDemoComponent {
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
variants = ['outlined', 'filled', 'underlined'] as const;
|
||||
|
||||
sampleValues: Record<string, Date | null> = {
|
||||
sm: null,
|
||||
md: null,
|
||||
lg: null
|
||||
};
|
||||
|
||||
variantValues: Record<string, Date | null> = {
|
||||
outlined: null,
|
||||
filled: null,
|
||||
underlined: null
|
||||
};
|
||||
|
||||
stateValues: Record<string, Date | null> = {
|
||||
default: null,
|
||||
error: null,
|
||||
success: new Date(),
|
||||
warning: null
|
||||
};
|
||||
|
||||
featureValues: Record<string, Date | null> = {
|
||||
required: null,
|
||||
disabled: new Date(),
|
||||
notClearable: null,
|
||||
limited: null
|
||||
};
|
||||
|
||||
interactiveValue: Date | null = null;
|
||||
changeCount = 0;
|
||||
|
||||
formValues = {
|
||||
startDate: null as Date | null,
|
||||
endDate: null as Date | null
|
||||
};
|
||||
|
||||
// Date limits for demo
|
||||
minDate = new Date(2024, 0, 1); // January 1, 2024
|
||||
maxDate = new Date(2024, 11, 31); // December 31, 2024
|
||||
|
||||
onDateChange(date: Date | null): void {
|
||||
this.changeCount++;
|
||||
console.log('Date changed:', date);
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
console.log('Form submitted:', this.formValues);
|
||||
alert('Form submitted! Check console for values.');
|
||||
}
|
||||
|
||||
resetForm(form: any): void {
|
||||
form.resetForm();
|
||||
this.formValues = {
|
||||
startDate: null,
|
||||
endDate: null
|
||||
};
|
||||
}
|
||||
|
||||
formatDate(date: Date): string {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
}
|
||||
224
projects/demo-ui-essentials/src/app/demos/demos.routes.ts
Normal file
224
projects/demo-ui-essentials/src/app/demos/demos.routes.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import {
|
||||
Component,
|
||||
ViewEncapsulation,
|
||||
Input
|
||||
} from '@angular/core';
|
||||
import { AvatarDemoComponent } from './avatar-demo/avatar-demo.component';
|
||||
import { ButtonDemoComponent } from './button-demo/button-demo.component';
|
||||
import { TableDemoComponent } from './table-demo/table-demo.component';
|
||||
import { BadgeDemoComponent } from './badge-demo/badge-demo.component';
|
||||
import { MenuDemoComponent } from './menu-demo/menu-demo.component';
|
||||
import { InputDemoComponent } from './input-demo/input-demo.component';
|
||||
import { LayoutDemoComponent } from './layout-demo/layout-demo.component';
|
||||
import { RadioDemoComponent } from './radio-demo/radio-demo.component';
|
||||
import { CheckboxDemoComponent } from './checkbox-demo/checkbox-demo.component';
|
||||
import { SearchDemoComponent } from './search-demo/search-demo.component';
|
||||
import { SwitchDemoComponent } from './switch-demo/switch-demo.component';
|
||||
import { ProgressDemoComponent } from './progress-demo/progress-demo.component';
|
||||
import { CardDemoComponent } from './card-demo/card-demo.component';
|
||||
import { ChipDemoComponent } from './chip-demo/chip-demo.component';
|
||||
import { AppbarDemoComponent } from './appbar-demo/appbar-demo.component';
|
||||
import { FontAwesomeDemoComponent } from './fontawesome-demo/fontawesome-demo.component';
|
||||
import { ImageContainerDemoComponent } from './image-container-demo/image-container-demo.component';
|
||||
import { CarouselDemoComponent } from './carousel-demo/carousel-demo.component';
|
||||
import { VideoPlayerDemoComponent } from "./video-player-demo/video-player-demo.component";
|
||||
import { ListDemoComponent } from "./list-demo/list-demo.component";
|
||||
import { ModalDemoComponent } from "./modal-demo/modal-demo.component";
|
||||
import { DrawerDemoComponent } from "./drawer-demo/drawer-demo.component";
|
||||
import { DatePickerDemoComponent } from './date-picker-demo/date-picker-demo.component';
|
||||
import { TimePickerDemoComponent } from './time-picker-demo/time-picker-demo.component';
|
||||
import { GridSystemDemoComponent } from './grid-system-demo/grid-system-demo.component';
|
||||
import { SpacerDemoComponent } from './spacer-demo/spacer-demo.component';
|
||||
import { ContainerDemoComponent } from './container-demo/container-demo.component';
|
||||
import { PaginationDemoComponent } from './pagination-demo/pagination-demo.component';
|
||||
import { SkeletonLoaderDemoComponent } from './skeleton-loader-demo/skeleton-loader-demo.component';
|
||||
import { EmptyStateDemoComponent } from './empty-state-demo/empty-state-demo.component';
|
||||
import { FileUploadDemoComponent } from './file-upload-demo/file-upload-demo.component';
|
||||
import { FormFieldDemoComponent } from './form-field-demo/form-field-demo.component';
|
||||
import { AutocompleteDemoComponent } from './autocomplete-demo/autocomplete-demo.component';
|
||||
import { BackdropDemoComponent } from './backdrop-demo/backdrop-demo.component';
|
||||
import { OverlayContainerDemoComponent } from './overlay-container-demo/overlay-container-demo.component';
|
||||
import { LoadingSpinnerDemoComponent } from './loading-spinner-demo/loading-spinner-demo.component';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'ui-demo-routes',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: true,
|
||||
template: `
|
||||
|
||||
@switch (this.route) {
|
||||
@case ("home") {
|
||||
}
|
||||
|
||||
@case ("appbar") {
|
||||
<ui-appbar-demo></ui-appbar-demo>
|
||||
}
|
||||
|
||||
@case ("avatar") {
|
||||
<ui-avatar-demo></ui-avatar-demo>
|
||||
}
|
||||
|
||||
@case ("buttons") {
|
||||
<ui-button-demo></ui-button-demo>
|
||||
}
|
||||
|
||||
@case ("cards") {
|
||||
<ui-card-demo></ui-card-demo>
|
||||
}
|
||||
|
||||
@case ("chips") {
|
||||
<ui-chip-demo></ui-chip-demo>
|
||||
}
|
||||
|
||||
@case ("badge") {
|
||||
<ui-badge-demo></ui-badge-demo>
|
||||
}
|
||||
|
||||
@case ("fontawesome") {
|
||||
<ui-fontawesome-demo></ui-fontawesome-demo>
|
||||
}
|
||||
|
||||
@case ("input") {
|
||||
<ui-input-demo></ui-input-demo>
|
||||
}
|
||||
@case ("layout") {
|
||||
<ui-layout-demo></ui-layout-demo>
|
||||
}
|
||||
@case ("radio") {
|
||||
<ui-radio-demo></ui-radio-demo>
|
||||
}
|
||||
|
||||
@case ("checkbox") {
|
||||
<ui-checkbox-demo></ui-checkbox-demo>
|
||||
}
|
||||
|
||||
@case ("search") {
|
||||
<ui-search-demo></ui-search-demo>
|
||||
}
|
||||
|
||||
@case ("switch") {
|
||||
<ui-switch-demo></ui-switch-demo>
|
||||
}
|
||||
|
||||
@case ("progress") {
|
||||
<ui-progress-demo></ui-progress-demo>
|
||||
}
|
||||
|
||||
|
||||
@case ("lists") {
|
||||
<ui-list-demo></ui-list-demo>
|
||||
}
|
||||
@case ("menu") {
|
||||
<ui-menu-demo></ui-menu-demo>
|
||||
}
|
||||
|
||||
@case ("table") {
|
||||
<ui-table-demo></ui-table-demo>
|
||||
}
|
||||
|
||||
@case ("image-container") {
|
||||
<ui-image-container-demo></ui-image-container-demo>
|
||||
}
|
||||
|
||||
@case ("carousel") {
|
||||
<ui-carousel-demo></ui-carousel-demo>
|
||||
}
|
||||
|
||||
@case ("video-player") {
|
||||
<ui-video-player-demo></ui-video-player-demo>
|
||||
}
|
||||
|
||||
@case ("modal") {
|
||||
<ui-modal-demo></ui-modal-demo>
|
||||
}
|
||||
|
||||
@case ("drawer") {
|
||||
<ui-drawer-demo></ui-drawer-demo>
|
||||
}
|
||||
|
||||
@case ("date-picker") {
|
||||
<ui-date-picker-demo></ui-date-picker-demo>
|
||||
}
|
||||
|
||||
@case ("time-picker") {
|
||||
<ui-time-picker-demo></ui-time-picker-demo>
|
||||
}
|
||||
|
||||
@case ("grid-system") {
|
||||
<ui-grid-system-demo></ui-grid-system-demo>
|
||||
}
|
||||
|
||||
@case ("spacer") {
|
||||
<ui-spacer-demo></ui-spacer-demo>
|
||||
}
|
||||
|
||||
@case ("container") {
|
||||
<ui-container-demo></ui-container-demo>
|
||||
}
|
||||
|
||||
@case ("pagination") {
|
||||
<ui-pagination-demo></ui-pagination-demo>
|
||||
}
|
||||
|
||||
@case ("skeleton-loader") {
|
||||
<ui-skeleton-loader-demo></ui-skeleton-loader-demo>
|
||||
}
|
||||
|
||||
@case ("empty-state") {
|
||||
<ui-empty-state-demo></ui-empty-state-demo>
|
||||
}
|
||||
|
||||
@case ("file-upload") {
|
||||
<ui-file-upload-demo></ui-file-upload-demo>
|
||||
}
|
||||
|
||||
@case ("form-field") {
|
||||
<ui-form-field-demo></ui-form-field-demo>
|
||||
}
|
||||
|
||||
@case ("autocomplete") {
|
||||
<ui-autocomplete-demo></ui-autocomplete-demo>
|
||||
}
|
||||
|
||||
@case ("backdrop") {
|
||||
<ui-backdrop-demo></ui-backdrop-demo>
|
||||
}
|
||||
|
||||
@case ("overlay-container") {
|
||||
<ui-overlay-container-demo></ui-overlay-container-demo>
|
||||
}
|
||||
|
||||
@case ("loading-spinner") {
|
||||
<ui-loading-spinner-demo></ui-loading-spinner-demo>
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
`,
|
||||
imports: [AvatarDemoComponent, ButtonDemoComponent, CardDemoComponent,
|
||||
ChipDemoComponent, TableDemoComponent, BadgeDemoComponent,
|
||||
MenuDemoComponent, InputDemoComponent, InputDemoComponent,
|
||||
LayoutDemoComponent, RadioDemoComponent, CheckboxDemoComponent,
|
||||
SearchDemoComponent, SwitchDemoComponent, ProgressDemoComponent,
|
||||
AppbarDemoComponent, FontAwesomeDemoComponent, ImageContainerDemoComponent,
|
||||
CarouselDemoComponent, VideoPlayerDemoComponent, ListDemoComponent,
|
||||
ModalDemoComponent, DrawerDemoComponent, DatePickerDemoComponent, TimePickerDemoComponent,
|
||||
GridSystemDemoComponent, SpacerDemoComponent, ContainerDemoComponent, PaginationDemoComponent,
|
||||
SkeletonLoaderDemoComponent, EmptyStateDemoComponent, FileUploadDemoComponent, FormFieldDemoComponent,
|
||||
AutocompleteDemoComponent, BackdropDemoComponent, OverlayContainerDemoComponent, LoadingSpinnerDemoComponent]
|
||||
})
|
||||
|
||||
|
||||
export class DemoRoutes {
|
||||
|
||||
@Input() route: string;
|
||||
|
||||
constructor() {
|
||||
this.route = "home"
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: $semantic-spacing-layout-lg;
|
||||
|
||||
h2 {
|
||||
font-size: $semantic-typography-heading-h2-size;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
color: $semantic-color-on-surface;
|
||||
margin-bottom: $semantic-spacing-layout-lg;
|
||||
border-bottom: 2px solid $semantic-color-outline;
|
||||
padding-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-xl;
|
||||
|
||||
h3 {
|
||||
font-size: $semantic-typography-heading-h3-size;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
color: $semantic-color-on-surface;
|
||||
margin-bottom: $semantic-spacing-layout-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $semantic-spacing-component-md;
|
||||
margin-bottom: $semantic-spacing-layout-sm;
|
||||
}
|
||||
|
||||
.config-panel {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: 1px solid $semantic-color-outline;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
|
||||
.config-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-md;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
margin-top: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
color: $semantic-color-on-surface;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
border: 1px solid $semantic-color-outline;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-on-surface;
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin: 0;
|
||||
accent-color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.event-log {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: 1px solid $semantic-color-outline;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
|
||||
.event-item {
|
||||
padding: $semantic-spacing-component-xs 0;
|
||||
color: $semantic-color-on-surface-variant;
|
||||
border-bottom: 1px solid $semantic-color-outline-variant;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-md $semantic-spacing-component-lg;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
color: $semantic-color-on-surface;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-variant;
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $semantic-color-surface-container;
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
width: 20px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: $semantic-typography-font-size-md;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.config-panel .config-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
|
||||
label {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
select {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { faBars, faUser, faCog, faHome, faChartLine, faEnvelope, faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { ButtonComponent, DrawerComponent } from "../../../../../ui-essentials/src/public-api";
|
||||
|
||||
@Component({
|
||||
selector: 'ui-drawer-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, FontAwesomeModule, ButtonComponent, DrawerComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Drawer Demo</h2>
|
||||
|
||||
<!-- Basic Usage -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Usage</h3>
|
||||
<div class="demo-row">
|
||||
<ui-button (clicked)="basicDrawer.set(true)">
|
||||
<fa-icon [icon]="faBars"></fa-icon>
|
||||
Open Basic Drawer
|
||||
</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Position Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Positions</h3>
|
||||
<div class="demo-row">
|
||||
<ui-button (clicked)="leftDrawer.set(true)">Left Drawer</ui-button>
|
||||
<ui-button (clicked)="rightDrawer.set(true)">Right Drawer</ui-button>
|
||||
<ui-button (clicked)="topDrawer.set(true)">Top Drawer</ui-button>
|
||||
<ui-button (clicked)="bottomDrawer.set(true)">Bottom Drawer</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
<ui-button (clicked)="smallDrawer.set(true)">Small Drawer</ui-button>
|
||||
<ui-button (clicked)="mediumDrawer.set(true)">Medium Drawer</ui-button>
|
||||
<ui-button (clicked)="largeDrawer.set(true)">Large Drawer</ui-button>
|
||||
<ui-button (clicked)="xlDrawer.set(true)">XL Drawer</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Configuration Options -->
|
||||
<section class="demo-section">
|
||||
<h3>Configuration Options</h3>
|
||||
<div class="demo-row">
|
||||
<ui-button (clicked)="noHeaderDrawer.set(true)">No Header</ui-button>
|
||||
<ui-button (clicked)="withFooterDrawer.set(true)">With Footer</ui-button>
|
||||
<ui-button (clicked)="noPaddingDrawer.set(true)">No Padding</ui-button>
|
||||
<ui-button (clicked)="persistentDrawer.set(true)">Persistent</ui-button>
|
||||
<ui-button (clicked)="notClosableDrawer.set(true)">Not Closable</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Features -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Features</h3>
|
||||
<div class="demo-row">
|
||||
<ui-button (clicked)="loadingDrawer.set(true)">Loading Drawer</ui-button>
|
||||
<ui-button (clicked)="scrollableDrawer.set(true)">Scrollable Content</ui-button>
|
||||
<ui-button (clicked)="navigationDrawer.set(true)">Navigation Drawer</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Configuration Panel -->
|
||||
<section class="demo-section">
|
||||
<h3>Try It Yourself</h3>
|
||||
<div class="config-panel">
|
||||
<div class="config-row">
|
||||
<label>Size:</label>
|
||||
<select [(ngModel)]="customConfig.size">
|
||||
<option value="sm">Small</option>
|
||||
<option value="md">Medium</option>
|
||||
<option value="lg">Large</option>
|
||||
<option value="xl">Extra Large</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="config-row">
|
||||
<label>Position:</label>
|
||||
<select [(ngModel)]="customConfig.position">
|
||||
<option value="left">Left</option>
|
||||
<option value="right">Right</option>
|
||||
<option value="top">Top</option>
|
||||
<option value="bottom">Bottom</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="config-row">
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="customConfig.closable">
|
||||
Closable
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="customConfig.backdropClosable">
|
||||
Backdrop Closable
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="config-row">
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="customConfig.showHeader">
|
||||
Show Header
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="customConfig.showFooter">
|
||||
Show Footer
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="config-row">
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="customConfig.persistent">
|
||||
Persistent
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ui-button (clicked)="customDrawer.set(true)">Open Custom Drawer</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Event Log -->
|
||||
<section class="demo-section">
|
||||
<h3>Event Log</h3>
|
||||
<div class="event-log">
|
||||
@for (event of eventLog; track $index) {
|
||||
<div class="event-item">{{ event }}</div>
|
||||
}
|
||||
@if (eventLog.length === 0) {
|
||||
<div class="event-item">No events yet...</div>
|
||||
}
|
||||
</div>
|
||||
<ui-button (clicked)="clearEventLog()" variant="outlined">Clear Log</ui-button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Basic Drawer -->
|
||||
<ui-drawer
|
||||
[open]="basicDrawer()"
|
||||
(openChange)="basicDrawer.set($event); logEvent('Basic drawer: ' + ($event ? 'opened' : 'closed'))"
|
||||
title="Basic Drawer"
|
||||
(opened)="logEvent('Basic drawer opened')"
|
||||
(closed)="logEvent('Basic drawer closed')"
|
||||
>
|
||||
<p>This is a basic drawer with default settings.</p>
|
||||
<p>You can close it by clicking the X button, pressing Escape, or clicking outside the drawer.</p>
|
||||
</ui-drawer>
|
||||
|
||||
<!-- Position Drawers -->
|
||||
<ui-drawer
|
||||
[open]="leftDrawer()"
|
||||
(openChange)="leftDrawer.set($event)"
|
||||
position="left"
|
||||
title="Left Drawer"
|
||||
>
|
||||
<p>This drawer slides in from the left side.</p>
|
||||
<p>Default position for navigation menus and sidebars.</p>
|
||||
</ui-drawer>
|
||||
|
||||
<ui-drawer
|
||||
[open]="rightDrawer()"
|
||||
(openChange)="rightDrawer.set($event)"
|
||||
position="right"
|
||||
title="Right Drawer"
|
||||
>
|
||||
<p>This drawer slides in from the right side.</p>
|
||||
<p>Great for secondary actions, filters, or settings panels.</p>
|
||||
</ui-drawer>
|
||||
|
||||
<ui-drawer
|
||||
[open]="topDrawer()"
|
||||
(openChange)="topDrawer.set($event)"
|
||||
position="top"
|
||||
title="Top Drawer"
|
||||
>
|
||||
<p>This drawer slides down from the top.</p>
|
||||
<p>Perfect for notifications, announcements, or quick access panels.</p>
|
||||
</ui-drawer>
|
||||
|
||||
<ui-drawer
|
||||
[open]="bottomDrawer()"
|
||||
(openChange)="bottomDrawer.set($event)"
|
||||
position="bottom"
|
||||
title="Bottom Drawer"
|
||||
>
|
||||
<p>This drawer slides up from the bottom.</p>
|
||||
<p>Commonly used for mobile interfaces and action sheets.</p>
|
||||
</ui-drawer>
|
||||
|
||||
<!-- Size Drawers -->
|
||||
<ui-drawer
|
||||
[open]="smallDrawer()"
|
||||
(openChange)="smallDrawer.set($event)"
|
||||
size="sm"
|
||||
title="Small Drawer (280px)"
|
||||
>
|
||||
<p>This is a small drawer (280px width).</p>
|
||||
<p>Perfect for simple navigation or minimal content.</p>
|
||||
</ui-drawer>
|
||||
|
||||
<ui-drawer
|
||||
[open]="mediumDrawer()"
|
||||
(openChange)="mediumDrawer.set($event)"
|
||||
size="md"
|
||||
title="Medium Drawer (360px)"
|
||||
>
|
||||
<p>This is a medium drawer (360px width) - the default size.</p>
|
||||
<p>Good balance between content space and screen real estate.</p>
|
||||
</ui-drawer>
|
||||
|
||||
<ui-drawer
|
||||
[open]="largeDrawer()"
|
||||
(openChange)="largeDrawer.set($event)"
|
||||
size="lg"
|
||||
title="Large Drawer (480px)"
|
||||
>
|
||||
<p>This is a large drawer (480px width).</p>
|
||||
<p>Ideal for detailed content, forms, or complex interfaces.</p>
|
||||
</ui-drawer>
|
||||
|
||||
<ui-drawer
|
||||
[open]="xlDrawer()"
|
||||
(openChange)="xlDrawer.set($event)"
|
||||
size="xl"
|
||||
title="Extra Large Drawer (640px)"
|
||||
>
|
||||
<p>This is an extra large drawer (640px width).</p>
|
||||
<p>Use for very detailed interfaces or when you need more space.</p>
|
||||
</ui-drawer>
|
||||
|
||||
<!-- Configuration Drawers -->
|
||||
<ui-drawer
|
||||
[open]="noHeaderDrawer()"
|
||||
(openChange)="noHeaderDrawer.set($event)"
|
||||
[showHeader]="false"
|
||||
>
|
||||
<div style="padding: 16px; text-align: center;">
|
||||
<fa-icon [icon]="faBars" style="font-size: 48px; color: #6750a4; margin-bottom: 16px;"></fa-icon>
|
||||
<h3 style="margin: 0 0 16px 0;">Drawer without header</h3>
|
||||
<p>This drawer doesn't have a header section.</p>
|
||||
<ui-button (clicked)="noHeaderDrawer.set(false)" style="margin-top: 16px;">Close</ui-button>
|
||||
</div>
|
||||
</ui-drawer>
|
||||
|
||||
<ui-drawer
|
||||
[open]="withFooterDrawer()"
|
||||
(openChange)="withFooterDrawer.set($event)"
|
||||
title="Drawer with Footer"
|
||||
[showFooter]="true"
|
||||
footerAlignment="between"
|
||||
>
|
||||
<p>This drawer has a footer with buttons.</p>
|
||||
<p>The footer stays at the bottom even if the content is scrollable.</p>
|
||||
|
||||
<ng-container slot="footer">
|
||||
<div>
|
||||
<ui-button (clicked)="logEvent('Help clicked')" variant="outlined">Help</ui-button>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<ui-button (clicked)="withFooterDrawer.set(false)" variant="outlined">Cancel</ui-button>
|
||||
<ui-button (clicked)="withFooterDrawer.set(false)" variant="filled">Save</ui-button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ui-drawer>
|
||||
|
||||
<ui-drawer
|
||||
[open]="noPaddingDrawer()"
|
||||
(openChange)="noPaddingDrawer.set($event)"
|
||||
title="No Padding Drawer"
|
||||
[bodyPadding]="false"
|
||||
>
|
||||
<div style="padding: 0; background: linear-gradient(45deg, #6750a4, #7c3aed); height: 200px; display: flex; align-items: center; justify-content: center; color: white;">
|
||||
<p style="margin: 0; text-align: center;">This content spans the full width<br>with no padding constraints.</p>
|
||||
</div>
|
||||
<div style="padding: 16px;">
|
||||
<p>Content below can still have its own padding.</p>
|
||||
</div>
|
||||
</ui-drawer>
|
||||
|
||||
<ui-drawer
|
||||
[open]="persistentDrawer()"
|
||||
(openChange)="persistentDrawer.set($event)"
|
||||
title="Persistent Drawer"
|
||||
[persistent]="true"
|
||||
>
|
||||
<p>This is a persistent drawer that doesn't have a backdrop.</p>
|
||||
<p>It stays open until explicitly closed and doesn't block interaction with the rest of the page.</p>
|
||||
<p>Perfect for navigation menus that should remain visible.</p>
|
||||
<ui-button (clicked)="persistentDrawer.set(false)" style="margin-top: 16px;">Close Drawer</ui-button>
|
||||
</ui-drawer>
|
||||
|
||||
<ui-drawer
|
||||
[open]="notClosableDrawer()"
|
||||
(openChange)="notClosableDrawer.set($event)"
|
||||
title="Not Closable Drawer"
|
||||
[closable]="false"
|
||||
[backdropClosable]="false"
|
||||
[escapeClosable]="false"
|
||||
[showFooter]="true"
|
||||
>
|
||||
<p>This drawer cannot be closed by clicking the X, pressing Escape, or clicking the backdrop.</p>
|
||||
<p>You must use the Close button below.</p>
|
||||
|
||||
<ng-container slot="footer">
|
||||
<ui-button (clicked)="notClosableDrawer.set(false)" variant="filled">Close</ui-button>
|
||||
</ng-container>
|
||||
</ui-drawer>
|
||||
|
||||
<!-- Interactive Drawers -->
|
||||
<ui-drawer
|
||||
[open]="loadingDrawer()"
|
||||
(openChange)="loadingDrawer.set($event)"
|
||||
title="Loading Data"
|
||||
[loading]="true"
|
||||
>
|
||||
<!-- Content will be replaced by loading spinner -->
|
||||
</ui-drawer>
|
||||
|
||||
<ui-drawer
|
||||
[open]="scrollableDrawer()"
|
||||
(openChange)="scrollableDrawer.set($event)"
|
||||
title="Scrollable Content"
|
||||
size="md"
|
||||
>
|
||||
<div style="height: 800px;">
|
||||
<h4>Long Content</h4>
|
||||
@for (item of longContent; track $index) {
|
||||
<p>{{ item }}</p>
|
||||
}
|
||||
</div>
|
||||
</ui-drawer>
|
||||
|
||||
<ui-drawer
|
||||
[open]="navigationDrawer()"
|
||||
(openChange)="navigationDrawer.set($event)"
|
||||
title="Navigation"
|
||||
size="sm"
|
||||
[showFooter]="true"
|
||||
>
|
||||
<nav style="margin: -16px 0;">
|
||||
<div class="nav-item" (click)="logEvent('Home clicked')">
|
||||
<fa-icon [icon]="faHome"></fa-icon>
|
||||
<span>Home</span>
|
||||
</div>
|
||||
<div class="nav-item" (click)="logEvent('Dashboard clicked')">
|
||||
<fa-icon [icon]="faChartLine"></fa-icon>
|
||||
<span>Dashboard</span>
|
||||
</div>
|
||||
<div class="nav-item" (click)="logEvent('Profile clicked')">
|
||||
<fa-icon [icon]="faUser"></fa-icon>
|
||||
<span>Profile</span>
|
||||
</div>
|
||||
<div class="nav-item" (click)="logEvent('Messages clicked')">
|
||||
<fa-icon [icon]="faEnvelope"></fa-icon>
|
||||
<span>Messages</span>
|
||||
</div>
|
||||
<div class="nav-item" (click)="logEvent('Settings clicked')">
|
||||
<fa-icon [icon]="faCog"></fa-icon>
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<ng-container slot="footer">
|
||||
<ui-button (clicked)="logEvent('Help clicked')" variant="outlined">
|
||||
<fa-icon [icon]="faQuestionCircle"></fa-icon>
|
||||
Help
|
||||
</ui-button>
|
||||
</ng-container>
|
||||
</ui-drawer>
|
||||
|
||||
<!-- Custom Configuration Drawer -->
|
||||
<ui-drawer
|
||||
[open]="customDrawer()"
|
||||
(openChange)="customDrawer.set($event)"
|
||||
[size]="customConfig.size"
|
||||
[position]="customConfig.position"
|
||||
[closable]="customConfig.closable"
|
||||
[backdropClosable]="customConfig.backdropClosable"
|
||||
[showHeader]="customConfig.showHeader"
|
||||
[showFooter]="customConfig.showFooter"
|
||||
[persistent]="customConfig.persistent"
|
||||
title="Custom Configured Drawer"
|
||||
>
|
||||
<p>This drawer is configured based on your selections:</p>
|
||||
<ul>
|
||||
<li>Size: {{ customConfig.size }}</li>
|
||||
<li>Position: {{ customConfig.position }}</li>
|
||||
<li>Closable: {{ customConfig.closable ? 'Yes' : 'No' }}</li>
|
||||
<li>Backdrop Closable: {{ customConfig.backdropClosable ? 'Yes' : 'No' }}</li>
|
||||
<li>Show Header: {{ customConfig.showHeader ? 'Yes' : 'No' }}</li>
|
||||
<li>Show Footer: {{ customConfig.showFooter ? 'Yes' : 'No' }}</li>
|
||||
<li>Persistent: {{ customConfig.persistent ? 'Yes' : 'No' }}</li>
|
||||
</ul>
|
||||
|
||||
@if (customConfig.showFooter) {
|
||||
<ng-container slot="footer">
|
||||
<ui-button (clicked)="customDrawer.set(false)" variant="filled">Close</ui-button>
|
||||
</ng-container>
|
||||
}
|
||||
</ui-drawer>
|
||||
`,
|
||||
styleUrl: './drawer-demo.component.scss'
|
||||
})
|
||||
export class DrawerDemoComponent {
|
||||
// Icons
|
||||
readonly faBars = faBars;
|
||||
readonly faUser = faUser;
|
||||
readonly faCog = faCog;
|
||||
readonly faHome = faHome;
|
||||
readonly faChartLine = faChartLine;
|
||||
readonly faEnvelope = faEnvelope;
|
||||
readonly faQuestionCircle = faQuestionCircle;
|
||||
|
||||
// Drawer states
|
||||
basicDrawer = signal(false);
|
||||
|
||||
leftDrawer = signal(false);
|
||||
rightDrawer = signal(false);
|
||||
topDrawer = signal(false);
|
||||
bottomDrawer = signal(false);
|
||||
|
||||
smallDrawer = signal(false);
|
||||
mediumDrawer = signal(false);
|
||||
largeDrawer = signal(false);
|
||||
xlDrawer = signal(false);
|
||||
|
||||
noHeaderDrawer = signal(false);
|
||||
withFooterDrawer = signal(false);
|
||||
noPaddingDrawer = signal(false);
|
||||
persistentDrawer = signal(false);
|
||||
notClosableDrawer = signal(false);
|
||||
|
||||
loadingDrawer = signal(false);
|
||||
scrollableDrawer = signal(false);
|
||||
navigationDrawer = signal(false);
|
||||
customDrawer = signal(false);
|
||||
|
||||
// Demo data
|
||||
eventLog: string[] = [];
|
||||
|
||||
customConfig = {
|
||||
size: 'md' as any,
|
||||
position: 'left' as any,
|
||||
closable: true,
|
||||
backdropClosable: true,
|
||||
showHeader: true,
|
||||
showFooter: false,
|
||||
persistent: false
|
||||
};
|
||||
|
||||
longContent = Array.from({ length: 50 }, (_, i) =>
|
||||
`This is paragraph ${i + 1}. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
|
||||
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.`
|
||||
);
|
||||
|
||||
// Simulate loading
|
||||
constructor() {
|
||||
setTimeout(() => {
|
||||
if (this.loadingDrawer()) {
|
||||
this.loadingDrawer.set(false);
|
||||
this.logEvent('Loading completed');
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
logEvent(event: string): void {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
this.eventLog.unshift(`[${timestamp}] ${event}`);
|
||||
if (this.eventLog.length > 10) {
|
||||
this.eventLog = this.eventLog.slice(0, 10);
|
||||
}
|
||||
}
|
||||
|
||||
clearEventLog(): void {
|
||||
this.eventLog = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
margin-bottom: $semantic-spacing-layout-lg;
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: $base-typography-font-size-2xl;
|
||||
font-weight: $base-typography-font-weight-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-xl;
|
||||
|
||||
h3 {
|
||||
margin-bottom: $semantic-spacing-layout-md;
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: $base-typography-font-size-lg;
|
||||
font-weight: $base-typography-font-weight-semibold;
|
||||
border-bottom: 1px solid $semantic-color-border-subtle;
|
||||
padding-bottom: $semantic-spacing-content-line-normal;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-layout-md;
|
||||
align-items: start;
|
||||
|
||||
ui-empty-state {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-label {
|
||||
margin: $semantic-spacing-content-line-normal 0 0 0;
|
||||
text-align: center;
|
||||
font-size: $base-typography-font-size-sm;
|
||||
color: $semantic-color-text-secondary;
|
||||
font-weight: $base-typography-font-weight-medium;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.demo-output {
|
||||
margin-top: $semantic-spacing-content-heading;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: 1px solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
|
||||
p {
|
||||
margin: 0 0 $semantic-spacing-content-line-tight 0;
|
||||
font-size: $base-typography-font-size-sm;
|
||||
color: $semantic-color-text-secondary;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 1024px) {
|
||||
.demo-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: $semantic-spacing-layout-sm;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-sm;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-lg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { EmptyStateComponent } from '../../../../../ui-essentials/src/lib/components/feedback';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-empty-state-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, EmptyStateComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Empty State Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-grid">
|
||||
@for (size of sizes; track size) {
|
||||
<ui-empty-state
|
||||
[size]="size"
|
||||
title="No items found"
|
||||
description="There are no items to display at the moment."
|
||||
actionLabel="Add Item"
|
||||
icon="<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2 2v-5m16 0h-5.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 009.586 13H4' /></svg>"
|
||||
(action)="handleDemoClick('Size: ' + size)">
|
||||
</ui-empty-state>
|
||||
<p class="demo-label">{{ size }} size</p>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Types -->
|
||||
<section class="demo-section">
|
||||
<h3>Variants</h3>
|
||||
<div class="demo-grid">
|
||||
<ui-empty-state
|
||||
variant="default"
|
||||
title="No data available"
|
||||
description="There's nothing here yet. Start by adding some content."
|
||||
actionLabel="Get Started"
|
||||
icon="<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' /></svg>"
|
||||
(action)="handleDemoClick('Default variant')">
|
||||
</ui-empty-state>
|
||||
<p class="demo-label">Default</p>
|
||||
|
||||
<ui-empty-state
|
||||
variant="search"
|
||||
title="No search results"
|
||||
description="We couldn't find anything matching your search. Try adjusting your filters or search terms."
|
||||
actionLabel="Clear Filters"
|
||||
icon="<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' /></svg>"
|
||||
(action)="handleDemoClick('Search variant')">
|
||||
</ui-empty-state>
|
||||
<p class="demo-label">Search</p>
|
||||
|
||||
<ui-empty-state
|
||||
variant="error"
|
||||
title="Something went wrong"
|
||||
description="We encountered an error while loading your content. Please try again."
|
||||
actionLabel="Retry"
|
||||
icon="<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z' /></svg>"
|
||||
(action)="handleDemoClick('Error variant')">
|
||||
</ui-empty-state>
|
||||
<p class="demo-label">Error</p>
|
||||
|
||||
<ui-empty-state
|
||||
variant="loading"
|
||||
title="Loading content..."
|
||||
description="Please wait while we fetch your data.">
|
||||
</ui-empty-state>
|
||||
<p class="demo-label">Loading</p>
|
||||
|
||||
<ui-empty-state
|
||||
variant="success"
|
||||
title="All done!"
|
||||
description="You've completed all tasks. Great work!"
|
||||
actionLabel="Start Over"
|
||||
icon="<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z' /></svg>"
|
||||
(action)="handleDemoClick('Success variant')">
|
||||
</ui-empty-state>
|
||||
<p class="demo-label">Success</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-grid">
|
||||
<ui-empty-state
|
||||
title="Normal state"
|
||||
description="This is a normal empty state with an action button."
|
||||
actionLabel="Take Action"
|
||||
icon="<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M13 10V3L4 14h7v7l9-11h-7z' /></svg>"
|
||||
(action)="handleDemoClick('Normal state')">
|
||||
</ui-empty-state>
|
||||
<p class="demo-label">Normal</p>
|
||||
|
||||
<ui-empty-state
|
||||
title="Disabled action"
|
||||
description="This empty state has a disabled action button."
|
||||
actionLabel="Disabled Action"
|
||||
[disabled]="true"
|
||||
icon="<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18.364 5.636M5.636 18.364l12.728-12.728' /></svg>"
|
||||
(action)="handleDemoClick('Disabled state')">
|
||||
</ui-empty-state>
|
||||
<p class="demo-label">Disabled</p>
|
||||
|
||||
<ui-empty-state
|
||||
title="No action button"
|
||||
description="This empty state doesn't have an action button."
|
||||
icon="<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' /></svg>">
|
||||
</ui-empty-state>
|
||||
<p class="demo-label">No Action</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Content Projection -->
|
||||
<section class="demo-section">
|
||||
<h3>Custom Content</h3>
|
||||
<div class="demo-grid">
|
||||
<ui-empty-state
|
||||
title="Custom content"
|
||||
description="You can add custom content inside the empty state."
|
||||
icon="<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zM7 21h10a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4z' /></svg>">
|
||||
<div style="margin: 16px 0; padding: 16px; background: var(--semantic-color-surface-secondary, #f8f9fa); border-radius: 8px;">
|
||||
<p style="margin: 0; color: var(--semantic-color-text-secondary, #6c757d); font-size: 14px;">
|
||||
This is custom content projected into the empty state component.
|
||||
You can add any HTML or Angular components here.
|
||||
</p>
|
||||
</div>
|
||||
</ui-empty-state>
|
||||
<p class="demo-label">With Content Projection</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Example</h3>
|
||||
<ui-empty-state
|
||||
title="Interactive Demo"
|
||||
description="Click the button below to see the interaction in action."
|
||||
actionLabel="Click Me!"
|
||||
icon="<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'><path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122' /></svg>"
|
||||
(action)="handleDemoClick('Interactive example')">
|
||||
</ui-empty-state>
|
||||
<div class="demo-output">
|
||||
<p><strong>Action clicked:</strong> {{ lastAction || 'None' }}</p>
|
||||
<p><strong>Click count:</strong> {{ clickCount }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './empty-state-demo.component.scss'
|
||||
})
|
||||
export class EmptyStateDemoComponent {
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
variants = ['default', 'search', 'error', 'loading', 'success'] as const;
|
||||
clickCount = 0;
|
||||
lastAction: string | null = null;
|
||||
|
||||
handleDemoClick(action: string): void {
|
||||
this.clickCount++;
|
||||
this.lastAction = action;
|
||||
console.log('Empty state action clicked:', action);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
font-size: $semantic-typography-heading-h2-size;
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
font-weight: $semantic-typography-font-weight-bold;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: $semantic-typography-heading-h3-size;
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-lg;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-layout-md;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
|
||||
h4 {
|
||||
font-size: $semantic-typography-heading-h4-size;
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-list-item;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-actions {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-content-paragraph;
|
||||
align-items: center;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-variant;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border: none;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-primary-hover;
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
color: $semantic-color-text-tertiary;
|
||||
margin-top: $semantic-spacing-content-list-item;
|
||||
}
|
||||
|
||||
.file-list-demo {
|
||||
margin-top: $semantic-spacing-content-paragraph;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
h5 {
|
||||
font-size: $semantic-typography-font-size-lg;
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-list-item;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: $semantic-spacing-component-xs;
|
||||
background: $semantic-color-surface-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
margin-bottom: $semantic-spacing-content-line-normal;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: $semantic-typography-font-size-xs;
|
||||
color: $semantic-color-text-tertiary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-demo {
|
||||
background: $semantic-color-surface-variant;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
margin-top: $semantic-spacing-content-paragraph;
|
||||
|
||||
pre {
|
||||
font-family: 'JetBrains Mono', 'Consolas', monospace;
|
||||
font-size: $semantic-typography-font-size-xs;
|
||||
color: $semantic-color-text-secondary;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { FileUploadComponent, UploadedFile } from '../../../../../../projects/ui-essentials/src/lib/components/forms/file-upload';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-file-upload-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, FileUploadComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>File Upload Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-grid">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ size.toUpperCase() }} Size</h4>
|
||||
<p>File upload with {{ size }} sizing.</p>
|
||||
<ui-file-upload
|
||||
[size]="size"
|
||||
label="Upload Files ({{ size }})"
|
||||
[multiple]="true"
|
||||
helperText="Select one or more files to upload"
|
||||
(filesSelected)="onFilesSelected('size-' + size, $event)"
|
||||
>
|
||||
</ui-file-upload>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Styles -->
|
||||
<section class="demo-section">
|
||||
<h3>Variants</h3>
|
||||
<div class="demo-grid">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ variant | titlecase }} Variant</h4>
|
||||
<p>File upload with {{ variant }} styling.</p>
|
||||
<ui-file-upload
|
||||
[variant]="variant"
|
||||
label="Upload Files ({{ variant }})"
|
||||
[multiple]="true"
|
||||
helperText="Drag & drop or browse to select files"
|
||||
(filesSelected)="onFilesSelected('variant-' + variant, $event)"
|
||||
>
|
||||
</ui-file-upload>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- File Type Restrictions -->
|
||||
<section class="demo-section">
|
||||
<h3>File Type Restrictions</h3>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>Images Only</h4>
|
||||
<p>Accepts only image files (JPEG, PNG, GIF, WebP).</p>
|
||||
<ui-file-upload
|
||||
label="Upload Images"
|
||||
[acceptedTypes]="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
|
||||
accept="image/*"
|
||||
[multiple]="true"
|
||||
helperText="Only image files are accepted"
|
||||
(filesSelected)="onFilesSelected('images', $event)"
|
||||
>
|
||||
</ui-file-upload>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Documents Only</h4>
|
||||
<p>Accepts PDF, Word, and Excel documents.</p>
|
||||
<ui-file-upload
|
||||
label="Upload Documents"
|
||||
[acceptedTypes]="['.pdf', '.doc', '.docx', '.xls', '.xlsx']"
|
||||
accept=".pdf,.doc,.docx,.xls,.xlsx"
|
||||
[multiple]="true"
|
||||
helperText="PDF, Word, and Excel files only"
|
||||
(filesSelected)="onFilesSelected('documents', $event)"
|
||||
>
|
||||
</ui-file-upload>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Limits -->
|
||||
<section class="demo-section">
|
||||
<h3>Size & File Limits</h3>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>Size Limit (5MB)</h4>
|
||||
<p>Maximum file size of 5MB per file.</p>
|
||||
<ui-file-upload
|
||||
label="Upload Files (5MB limit)"
|
||||
[maxFileSize]="5242880"
|
||||
[multiple]="true"
|
||||
helperText="Max file size: 5MB"
|
||||
(filesSelected)="onFilesSelected('size-limit', $event)"
|
||||
>
|
||||
</ui-file-upload>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>File Count Limit</h4>
|
||||
<p>Maximum of 3 files can be selected.</p>
|
||||
<ui-file-upload
|
||||
label="Upload Files (3 max)"
|
||||
[maxFiles]="3"
|
||||
[multiple]="true"
|
||||
helperText="Maximum 3 files allowed"
|
||||
(filesSelected)="onFilesSelected('file-limit', $event)"
|
||||
>
|
||||
</ui-file-upload>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Single File</h4>
|
||||
<p>Only one file can be selected.</p>
|
||||
<ui-file-upload
|
||||
label="Upload Single File"
|
||||
[multiple]="false"
|
||||
helperText="Select a single file to upload"
|
||||
(filesSelected)="onFilesSelected('single-file', $event)"
|
||||
>
|
||||
</ui-file-upload>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>Disabled State</h4>
|
||||
<p>File upload in disabled state.</p>
|
||||
<ui-file-upload
|
||||
label="Disabled Upload"
|
||||
[disabled]="true"
|
||||
helperText="This upload is disabled"
|
||||
(filesSelected)="onFilesSelected('disabled', $event)"
|
||||
>
|
||||
</ui-file-upload>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Error State</h4>
|
||||
<p>File upload showing error state.</p>
|
||||
<ui-file-upload
|
||||
label="Upload with Error"
|
||||
state="error"
|
||||
errorMessage="Please select valid files"
|
||||
[multiple]="true"
|
||||
(filesSelected)="onFilesSelected('error', $event)"
|
||||
>
|
||||
</ui-file-upload>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Success State</h4>
|
||||
<p>File upload showing success state.</p>
|
||||
<ui-file-upload
|
||||
label="Successful Upload"
|
||||
state="success"
|
||||
helperText="Files uploaded successfully!"
|
||||
[multiple]="true"
|
||||
(filesSelected)="onFilesSelected('success', $event)"
|
||||
>
|
||||
</ui-file-upload>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Reactive Form Integration -->
|
||||
<section class="demo-section">
|
||||
<h3>Reactive Form Integration</h3>
|
||||
<div class="demo-item">
|
||||
<h4>Form with Validation</h4>
|
||||
<p>File upload integrated with Angular reactive forms and validation.</p>
|
||||
|
||||
<form [formGroup]="uploadForm" (ngSubmit)="onSubmit()">
|
||||
<ui-file-upload
|
||||
label="Required File Upload"
|
||||
formControlName="files"
|
||||
[multiple]="true"
|
||||
[required]="true"
|
||||
[acceptedTypes]="['.pdf', '.doc', '.docx', 'image/*']"
|
||||
[maxFileSize]="10485760"
|
||||
[maxFiles]="5"
|
||||
helperText="Required field - select 1-5 files (PDF, Word, or images, max 10MB each)"
|
||||
[state]="getFormFieldState('files')"
|
||||
[errorMessage]="getFormFieldError('files')"
|
||||
(filesSelected)="onFormFilesSelected($event)"
|
||||
>
|
||||
</ui-file-upload>
|
||||
|
||||
<div class="demo-actions">
|
||||
<button
|
||||
type="submit"
|
||||
class="demo-button"
|
||||
[disabled]="uploadForm.invalid"
|
||||
>
|
||||
Submit Form
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="demo-button demo-button--secondary"
|
||||
(click)="resetForm()"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<div class="demo-info">
|
||||
Form Status: {{ uploadForm.status }} |
|
||||
Form Valid: {{ uploadForm.valid }} |
|
||||
Files Count: {{ uploadForm.get('files')?.value?.length || 0 }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if (submittedFiles.length > 0) {
|
||||
<div class="file-list-demo">
|
||||
<h5>Submitted Files:</h5>
|
||||
@for (file of submittedFiles; track file.id) {
|
||||
<div class="file-item">
|
||||
<div>
|
||||
<div class="file-info">{{ file.name }}</div>
|
||||
<div class="file-size">{{ formatFileSize(file.size) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Event Handling -->
|
||||
<section class="demo-section">
|
||||
<h3>Event Handling</h3>
|
||||
<div class="demo-item">
|
||||
<h4>Event Monitoring</h4>
|
||||
<p>Monitor file upload events in real-time.</p>
|
||||
|
||||
<ui-file-upload
|
||||
label="Event Demo Upload"
|
||||
[multiple]="true"
|
||||
[maxFiles]="3"
|
||||
helperText="Add/remove files to see events below"
|
||||
(filesSelected)="onEventFilesSelected($event)"
|
||||
(fileAdded)="onFileAdded($event)"
|
||||
(fileRemoved)="onFileRemoved($event)"
|
||||
>
|
||||
</ui-file-upload>
|
||||
|
||||
<div class="file-list-demo">
|
||||
<h5>Recent Events:</h5>
|
||||
@if (recentEvents.length === 0) {
|
||||
<p class="demo-info">No events yet. Add or remove files to see events.</p>
|
||||
} @else {
|
||||
@for (event of recentEvents.slice(-5).reverse(); track $index) {
|
||||
<div class="file-item">
|
||||
<div>
|
||||
<div class="file-info">{{ event.type }}: {{ event.fileName }}</div>
|
||||
<div class="file-size">{{ event.timestamp | date:'medium' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Code Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Usage Example</h3>
|
||||
<div class="code-demo">
|
||||
<pre><code>{{ codeExample }}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './file-upload-demo.component.scss'
|
||||
})
|
||||
export class FileUploadDemoComponent {
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
variants = ['outlined', 'filled', 'underlined'] as const;
|
||||
|
||||
uploadForm = new FormGroup({
|
||||
files: new FormControl<UploadedFile[]>([], [Validators.required])
|
||||
});
|
||||
|
||||
submittedFiles: UploadedFile[] = [];
|
||||
recentEvents: Array<{type: string, fileName: string, timestamp: Date}> = [];
|
||||
|
||||
readonly codeExample = `import { FileUploadComponent, UploadedFile } from 'ui-essentials';
|
||||
|
||||
// Basic usage
|
||||
<ui-file-upload
|
||||
label="Upload Files"
|
||||
[multiple]="true"
|
||||
[acceptedTypes]="['image/*', '.pdf']"
|
||||
[maxFileSize]="5242880"
|
||||
[maxFiles]="5"
|
||||
helperText="Select up to 5 files (images or PDF, max 5MB each)"
|
||||
(filesSelected)="onFilesSelected($event)"
|
||||
>
|
||||
</ui-file-upload>
|
||||
|
||||
// With reactive forms
|
||||
<ui-file-upload
|
||||
formControlName="files"
|
||||
[required]="true"
|
||||
[state]="getFieldState('files')"
|
||||
[errorMessage]="getFieldError('files')"
|
||||
>
|
||||
</ui-file-upload>
|
||||
|
||||
// Event handling
|
||||
onFilesSelected(files: UploadedFile[]): void {
|
||||
console.log('Selected files:', files);
|
||||
files.forEach(file => {
|
||||
console.log(file.name, file.size, file.type);
|
||||
});
|
||||
}`;
|
||||
|
||||
onFilesSelected(context: string, files: UploadedFile[]): void {
|
||||
console.log(`Files selected in ${context}:`, files);
|
||||
}
|
||||
|
||||
onFormFilesSelected(files: UploadedFile[]): void {
|
||||
this.uploadForm.patchValue({ files });
|
||||
this.uploadForm.get('files')?.markAsTouched();
|
||||
}
|
||||
|
||||
onEventFilesSelected(files: UploadedFile[]): void {
|
||||
this.addEvent('filesSelected', `${files.length} files total`);
|
||||
}
|
||||
|
||||
onFileAdded(file: UploadedFile): void {
|
||||
this.addEvent('fileAdded', file.name);
|
||||
}
|
||||
|
||||
onFileRemoved(file: UploadedFile): void {
|
||||
this.addEvent('fileRemoved', file.name);
|
||||
}
|
||||
|
||||
private addEvent(type: string, fileName: string): void {
|
||||
this.recentEvents.push({
|
||||
type,
|
||||
fileName,
|
||||
timestamp: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.uploadForm.valid) {
|
||||
this.submittedFiles = this.uploadForm.get('files')?.value || [];
|
||||
console.log('Form submitted with files:', this.submittedFiles);
|
||||
}
|
||||
}
|
||||
|
||||
resetForm(): void {
|
||||
this.uploadForm.reset();
|
||||
this.uploadForm.get('files')?.setValue([]);
|
||||
this.submittedFiles = [];
|
||||
}
|
||||
|
||||
getFormFieldState(fieldName: string): 'default' | 'error' | 'success' {
|
||||
const control = this.uploadForm.get(fieldName);
|
||||
if (control?.invalid && control?.touched) {
|
||||
return 'error';
|
||||
}
|
||||
if (control?.valid && control?.value?.length > 0) {
|
||||
return 'success';
|
||||
}
|
||||
return 'default';
|
||||
}
|
||||
|
||||
getFormFieldError(fieldName: string): string {
|
||||
const control = this.uploadForm.get(fieldName);
|
||||
if (control?.invalid && control?.touched) {
|
||||
if (control.errors?.['required']) {
|
||||
return 'Please select at least one file';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
@import "../../../../../shared-ui/src/styles/semantic";
|
||||
.demo-container {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid #6366f1;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #374151;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: #6b7280;
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
// Sections
|
||||
.demo-section {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.intro-section {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
background: #f8fafc;
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.info-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: #dbeafe;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
|
||||
.info-icon {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #1f2937;
|
||||
}
|
||||
}
|
||||
|
||||
// Icon Grid
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.icon-example {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f3f4f6;
|
||||
border-color: #6366f1;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.icon-name {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0.5rem 0 0.25rem;
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.icon-usage {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.75rem;
|
||||
background: #f3f4f6;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
color: #6b7280;
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Size Demo
|
||||
.size-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.size-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
|
||||
.size-label {
|
||||
font-weight: 600;
|
||||
min-width: 30px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
color: #6366f1;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #ffffff;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
color: #6b7280;
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Styling Demo
|
||||
.styling-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.style-example {
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
|
||||
h4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.color-row,
|
||||
.animation-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
fa-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Use Cases
|
||||
.use-cases {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.use-case {
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.nav-example {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
fa-icon {
|
||||
margin-right: 0.5rem;
|
||||
color: #6366f1;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
|
||||
&.success {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
|
||||
fa-icon {
|
||||
color: #22c55e;
|
||||
}
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
|
||||
fa-icon {
|
||||
color: #f59e0b;
|
||||
}
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
|
||||
fa-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
|
||||
fa-icon {
|
||||
color: #3b82f6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Code Examples
|
||||
.code-examples {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.code-example {
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
|
||||
h4 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
background: #ffffff;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #e5e7eb;
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
color: #1f2937;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Selected Icon Details
|
||||
.selected-icon {
|
||||
background: #ede9fe;
|
||||
border-color: #6366f1;
|
||||
}
|
||||
|
||||
.icon-details {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
align-items: flex-start;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-display {
|
||||
flex-shrink: 0;
|
||||
padding: 2rem;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #6366f1;
|
||||
|
||||
fa-icon {
|
||||
color: #6366f1;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-info {
|
||||
flex: 1;
|
||||
|
||||
h4 {
|
||||
color: #5b21b6;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0.75rem;
|
||||
color: #1f2937;
|
||||
}
|
||||
}
|
||||
|
||||
.copy-code {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e5e7eb;
|
||||
|
||||
code {
|
||||
flex: 1;
|
||||
background: none;
|
||||
color: #1f2937;
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Interactive Demo
|
||||
.action-feedback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
background: #dbeafe;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
|
||||
fa-icon {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #1f2937;
|
||||
}
|
||||
}
|
||||
|
||||
.interactive-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
// Best Practices
|
||||
.best-practices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.practice-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e5e7eb;
|
||||
|
||||
.practice-icon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
&.success {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: #f59e0b;
|
||||
}
|
||||
}
|
||||
|
||||
.practice-content {
|
||||
flex: 1;
|
||||
|
||||
h4 {
|
||||
color: #1f2937;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #6b7280;
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f3f4f6;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
color: #374151;
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Quick Reference
|
||||
.reference-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.reference-category {
|
||||
padding: 1rem;
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e5e7eb;
|
||||
|
||||
h4 {
|
||||
color: #6366f1;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.reference-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
|
||||
code {
|
||||
background: #f3f4f6;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
color: #6366f1;
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.icon-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.action-buttons,
|
||||
.interactive-buttons {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.nav-example {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.size-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.color-row,
|
||||
.animation-row {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,629 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons';
|
||||
import {
|
||||
// Solid icons from shared-ui
|
||||
faUser,
|
||||
faHome,
|
||||
faCog,
|
||||
faSearch,
|
||||
faPlus,
|
||||
faEdit,
|
||||
faTrash,
|
||||
faSave,
|
||||
faCancel,
|
||||
faCheck,
|
||||
faTimes,
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faChevronUp,
|
||||
faChevronDown,
|
||||
faBars,
|
||||
faEllipsisV,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faDownload,
|
||||
faUpload,
|
||||
faRefresh,
|
||||
faSpinner,
|
||||
faExclamationTriangle,
|
||||
faInfoCircle,
|
||||
faCheckCircle,
|
||||
faTimesCircle,
|
||||
faEnvelope
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import {
|
||||
// Regular icons from shared-ui
|
||||
faHeart,
|
||||
faBookmark,
|
||||
faComment,
|
||||
faThumbsUp
|
||||
} from '@fortawesome/free-regular-svg-icons';
|
||||
import {
|
||||
// Brand icons from shared-ui
|
||||
faGithub,
|
||||
faTwitter,
|
||||
faLinkedin,
|
||||
faGoogle
|
||||
} from '@fortawesome/free-brands-svg-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-fontawesome-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FaIconComponent,
|
||||
ButtonComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Font Awesome Icons Showcase</h2>
|
||||
|
||||
<!-- Introduction -->
|
||||
<section class="intro-section">
|
||||
<h3>Font Awesome Integration</h3>
|
||||
<p>This demo showcases the Font Awesome icons available in the shared-ui library. Icons are pre-configured and ready to use across the application.</p>
|
||||
<div class="info-box">
|
||||
<fa-icon [icon]="faInfoCircle" class="info-icon"></fa-icon>
|
||||
<span><strong>Available Categories:</strong> Solid, Regular, and Brand icons from Font Awesome</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Basic Usage -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Usage</h3>
|
||||
<div class="icon-grid">
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faUser" size="2x"></fa-icon>
|
||||
<span class="icon-name">faUser</span>
|
||||
<code>fa-icon [icon]="faUser"</code>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faHome" size="2x"></fa-icon>
|
||||
<span class="icon-name">faHome</span>
|
||||
<code>fa-icon [icon]="faHome"</code>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faCog" size="2x"></fa-icon>
|
||||
<span class="icon-name">faCog</span>
|
||||
<code>fa-icon [icon]="faCog"</code>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Icon Sizes -->
|
||||
<section class="demo-section">
|
||||
<h3>Icon Sizes</h3>
|
||||
<div class="size-demo">
|
||||
<div class="size-row">
|
||||
<span class="size-label">xs:</span>
|
||||
<fa-icon [icon]="faCheck" size="xs"></fa-icon>
|
||||
<code>size="xs"</code>
|
||||
</div>
|
||||
<div class="size-row">
|
||||
<span class="size-label">sm:</span>
|
||||
<fa-icon [icon]="faCheck" size="sm"></fa-icon>
|
||||
<code>size="sm"</code>
|
||||
</div>
|
||||
<div class="size-row">
|
||||
<span class="size-label">lg:</span>
|
||||
<fa-icon [icon]="faCheck" size="lg"></fa-icon>
|
||||
<code>size="lg"</code>
|
||||
</div>
|
||||
<div class="size-row">
|
||||
<span class="size-label">2x:</span>
|
||||
<fa-icon [icon]="faCheck" size="2x"></fa-icon>
|
||||
<code>size="2x"</code>
|
||||
</div>
|
||||
<div class="size-row">
|
||||
<span class="size-label">3x:</span>
|
||||
<fa-icon [icon]="faCheck" size="3x"></fa-icon>
|
||||
<code>size="3x"</code>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Solid Icons -->
|
||||
<section class="demo-section">
|
||||
<h3>Solid Icons (free-solid-svg-icons)</h3>
|
||||
<p>Most commonly used icons for UI elements, actions, and navigation.</p>
|
||||
<div class="icon-grid">
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faUser" size="2x"></fa-icon>
|
||||
<span class="icon-name">faUser</span>
|
||||
<span class="icon-usage">User profiles, accounts</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faHome" size="2x"></fa-icon>
|
||||
<span class="icon-name">faHome</span>
|
||||
<span class="icon-usage">Home navigation</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faCog" size="2x"></fa-icon>
|
||||
<span class="icon-name">faCog</span>
|
||||
<span class="icon-usage">Settings</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faSearch" size="2x"></fa-icon>
|
||||
<span class="icon-name">faSearch</span>
|
||||
<span class="icon-usage">Search functionality</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faPlus" size="2x"></fa-icon>
|
||||
<span class="icon-name">faPlus</span>
|
||||
<span class="icon-usage">Add new items</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faEdit" size="2x"></fa-icon>
|
||||
<span class="icon-name">faEdit</span>
|
||||
<span class="icon-usage">Edit content</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faTrash" size="2x"></fa-icon>
|
||||
<span class="icon-name">faTrash</span>
|
||||
<span class="icon-usage">Delete items</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faSave" size="2x"></fa-icon>
|
||||
<span class="icon-name">faSave</span>
|
||||
<span class="icon-usage">Save changes</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faCheck" size="2x"></fa-icon>
|
||||
<span class="icon-name">faCheck</span>
|
||||
<span class="icon-usage">Confirm, success</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faTimes" size="2x"></fa-icon>
|
||||
<span class="icon-name">faTimes</span>
|
||||
<span class="icon-usage">Close, dismiss</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faArrowLeft" size="2x"></fa-icon>
|
||||
<span class="icon-name">faArrowLeft</span>
|
||||
<span class="icon-usage">Back navigation</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faArrowRight" size="2x"></fa-icon>
|
||||
<span class="icon-name">faArrowRight</span>
|
||||
<span class="icon-usage">Forward navigation</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faDownload" size="2x"></fa-icon>
|
||||
<span class="icon-name">faDownload</span>
|
||||
<span class="icon-usage">Download files</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faUpload" size="2x"></fa-icon>
|
||||
<span class="icon-name">faUpload</span>
|
||||
<span class="icon-usage">Upload files</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faRefresh" size="2x"></fa-icon>
|
||||
<span class="icon-name">faRefresh</span>
|
||||
<span class="icon-usage">Refresh content</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faSpinner" size="2x"></fa-icon>
|
||||
<span class="icon-name">faSpinner</span>
|
||||
<span class="icon-usage">Loading states</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Regular Icons -->
|
||||
<section class="demo-section">
|
||||
<h3>Regular Icons (free-regular-svg-icons)</h3>
|
||||
<p>Outlined versions of icons, great for secondary actions and states.</p>
|
||||
<div class="icon-grid">
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faHeart" size="2x"></fa-icon>
|
||||
<span class="icon-name">faHeart</span>
|
||||
<span class="icon-usage">Favorites, likes</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faBookmark" size="2x"></fa-icon>
|
||||
<span class="icon-name">faBookmark</span>
|
||||
<span class="icon-usage">Saved items</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faComment" size="2x"></fa-icon>
|
||||
<span class="icon-name">faComment</span>
|
||||
<span class="icon-usage">Comments</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faThumbsUp" size="2x"></fa-icon>
|
||||
<span class="icon-name">faThumbsUp</span>
|
||||
<span class="icon-usage">Approval</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Brand Icons -->
|
||||
<section class="demo-section">
|
||||
<h3>Brand Icons (free-brands-svg-icons)</h3>
|
||||
<p>Social media and company logos for external integrations.</p>
|
||||
<div class="icon-grid">
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faGithub" size="2x"></fa-icon>
|
||||
<span class="icon-name">faGithub</span>
|
||||
<span class="icon-usage">GitHub integration</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faTwitter" size="2x"></fa-icon>
|
||||
<span class="icon-name">faTwitter</span>
|
||||
<span class="icon-usage">Twitter sharing</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faLinkedin" size="2x"></fa-icon>
|
||||
<span class="icon-name">faLinkedin</span>
|
||||
<span class="icon-usage">LinkedIn integration</span>
|
||||
</div>
|
||||
<div class="icon-example">
|
||||
<fa-icon [icon]="faGoogle" size="2x"></fa-icon>
|
||||
<span class="icon-name">faGoogle</span>
|
||||
<span class="icon-usage">Google services</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Icon Styling -->
|
||||
<section class="demo-section">
|
||||
<h3>Icon Styling & Transformations</h3>
|
||||
<div class="styling-demo">
|
||||
<div class="style-example">
|
||||
<h4>Colors</h4>
|
||||
<div class="color-row">
|
||||
<fa-icon [icon]="faHeart" size="2x" style="color: #dc3545;"></fa-icon>
|
||||
<fa-icon [icon]="faHeart" size="2x" style="color: #28a745;"></fa-icon>
|
||||
<fa-icon [icon]="faHeart" size="2x" style="color: #007bff;"></fa-icon>
|
||||
<fa-icon [icon]="faHeart" size="2x" style="color: #ffc107;"></fa-icon>
|
||||
</div>
|
||||
<code>style="color: #dc3545;"</code>
|
||||
</div>
|
||||
|
||||
<div class="style-example">
|
||||
<h4>Different Sizes</h4>
|
||||
<div class="animation-row">
|
||||
<fa-icon [icon]="faSpinner" size="lg"></fa-icon>
|
||||
<fa-icon [icon]="faRefresh" size="2x"></fa-icon>
|
||||
<fa-icon [icon]="faCog" size="3x"></fa-icon>
|
||||
</div>
|
||||
<code>size="lg", size="2x", size="3x"</code>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Common Use Cases -->
|
||||
<section class="demo-section">
|
||||
<h3>Common Use Cases</h3>
|
||||
<div class="use-cases">
|
||||
<div class="use-case">
|
||||
<h4>Navigation</h4>
|
||||
<div class="nav-example">
|
||||
<div><fa-icon [icon]="faHome"></fa-icon> Home</div>
|
||||
<div><fa-icon [icon]="faUser"></fa-icon> Profile</div>
|
||||
<div><fa-icon [icon]="faCog"></fa-icon> Settings</div>
|
||||
<div><fa-icon [icon]="faSearch"></fa-icon> Search</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="use-case">
|
||||
<h4>Action Buttons</h4>
|
||||
<div class="action-buttons">
|
||||
<ui-button variant="filled" [icon]="faPlus" iconPosition="left">Add Item</ui-button>
|
||||
<ui-button variant="outlined" [icon]="faEdit" iconPosition="left">Edit</ui-button>
|
||||
<ui-button variant="outlined" [icon]="faTrash" iconPosition="left">Delete</ui-button>
|
||||
<ui-button variant="tonal" [icon]="faSave" iconPosition="left">Save</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="use-case">
|
||||
<h4>Status Indicators</h4>
|
||||
<div class="status-row">
|
||||
<div class="status-item success">
|
||||
<fa-icon [icon]="faCheckCircle"></fa-icon>
|
||||
<span>Success</span>
|
||||
</div>
|
||||
<div class="status-item warning">
|
||||
<fa-icon [icon]="faExclamationTriangle"></fa-icon>
|
||||
<span>Warning</span>
|
||||
</div>
|
||||
<div class="status-item error">
|
||||
<fa-icon [icon]="faTimesCircle"></fa-icon>
|
||||
<span>Error</span>
|
||||
</div>
|
||||
<div class="status-item info">
|
||||
<fa-icon [icon]="faInfoCircle"></fa-icon>
|
||||
<span>Info</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Code Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Implementation Examples</h3>
|
||||
<div class="code-examples">
|
||||
<div class="code-example">
|
||||
<h4>1. Import the Icon</h4>
|
||||
<pre><code>import { faUser } from '@fortawesome/free-solid-svg-icons';</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
<h4>2. Add to Component</h4>
|
||||
<pre><code>export class MyComponent {
|
||||
faUser = faUser;
|
||||
faHome = faHome;
|
||||
faCog = faCog;
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
<h4>3. Basic Icon Usage</h4>
|
||||
<pre><code><fa-icon [icon]="faUser"></fa-icon>
|
||||
<fa-icon [icon]="faHome" size="lg"></fa-icon>
|
||||
<fa-icon [icon]="faCog" size="2x"></fa-icon></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
<h4>4. Styled Icons</h4>
|
||||
<pre><code><fa-icon
|
||||
[icon]="faHeart"
|
||||
size="2x"
|
||||
style="color: #dc3545;">
|
||||
</fa-icon></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
<h4>5. Icons in Buttons</h4>
|
||||
<pre><code><ui-button
|
||||
variant="filled"
|
||||
[icon]="faPlus"
|
||||
iconPosition="left">
|
||||
Add Item
|
||||
</ui-button></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
<h4>6. Navigation with Icons</h4>
|
||||
<pre><code><nav class="nav-menu">
|
||||
<a href="/home">
|
||||
<fa-icon [icon]="faHome"></fa-icon> Home
|
||||
</a>
|
||||
<a href="/profile">
|
||||
<fa-icon [icon]="faUser"></fa-icon> Profile
|
||||
</a>
|
||||
</nav></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
<h4>7. Status Messages</h4>
|
||||
<pre><code><div class="status-success">
|
||||
<fa-icon [icon]="faCheckCircle"></fa-icon>
|
||||
Operation completed successfully!
|
||||
</div>
|
||||
|
||||
<div class="status-error">
|
||||
<fa-icon [icon]="faTimesCircle"></fa-icon>
|
||||
Something went wrong.
|
||||
</div></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
<h4>8. Loading States</h4>
|
||||
<pre><code><div class="loading-container">
|
||||
<fa-icon [icon]="faSpinner"></fa-icon>
|
||||
Loading...
|
||||
</div>
|
||||
|
||||
<ui-button
|
||||
[disabled]="isLoading"
|
||||
[icon]="isLoading ? faSpinner : faSave">
|
||||
{{ isLoading ? 'Saving...' : 'Save' }}
|
||||
</ui-button></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Best Practices -->
|
||||
<section class="demo-section">
|
||||
<h3>Best Practices</h3>
|
||||
<div class="best-practices">
|
||||
<div class="practice-item">
|
||||
<fa-icon [icon]="faCheckCircle" class="practice-icon success"></fa-icon>
|
||||
<div class="practice-content">
|
||||
<h4>Import Only What You Need</h4>
|
||||
<p>Only import the specific icons you're using to keep bundle size small.</p>
|
||||
<code>import { faUser, faHome } from '@fortawesome/free-solid-svg-icons';</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="practice-item">
|
||||
<fa-icon [icon]="faCheckCircle" class="practice-icon success"></fa-icon>
|
||||
<div class="practice-content">
|
||||
<h4>Use Semantic Naming</h4>
|
||||
<p>Choose icons that clearly represent their function or meaning.</p>
|
||||
<code>faTrash for delete, faEdit for edit, faPlus for add</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="practice-item">
|
||||
<fa-icon [icon]="faCheckCircle" class="practice-icon success"></fa-icon>
|
||||
<div class="practice-content">
|
||||
<h4>Consistent Sizing</h4>
|
||||
<p>Use consistent icon sizes throughout your application.</p>
|
||||
<code>size="sm" for inline text, size="lg" for buttons</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="practice-item">
|
||||
<fa-icon [icon]="faExclamationTriangle" class="practice-icon warning"></fa-icon>
|
||||
<div class="practice-content">
|
||||
<h4>Accessibility</h4>
|
||||
<p>Add aria-labels for screen readers when icons convey important information.</p>
|
||||
<code><fa-icon [icon]="faUser" aria-label="User profile"></fa-icon></code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Available Icon Reference -->
|
||||
<section class="demo-section">
|
||||
<h3>Quick Reference</h3>
|
||||
<div class="reference-grid">
|
||||
<div class="reference-category">
|
||||
<h4>Actions</h4>
|
||||
<div class="reference-list">
|
||||
<code>faPlus</code> - Add/Create
|
||||
<code>faEdit</code> - Edit/Modify
|
||||
<code>faTrash</code> - Delete/Remove
|
||||
<code>faSave</code> - Save/Confirm
|
||||
<code>faCancel</code> - Cancel/Dismiss
|
||||
<code>faRefresh</code> - Reload/Update
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="reference-category">
|
||||
<h4>Navigation</h4>
|
||||
<div class="reference-list">
|
||||
<code>faHome</code> - Home page
|
||||
<code>faArrowLeft</code> - Go back
|
||||
<code>faArrowRight</code> - Go forward
|
||||
<code>faChevronUp</code> - Expand up
|
||||
<code>faChevronDown</code> - Expand down
|
||||
<code>faBars</code> - Menu toggle
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="reference-category">
|
||||
<h4>Status</h4>
|
||||
<div class="reference-list">
|
||||
<code>faCheckCircle</code> - Success
|
||||
<code>faTimesCircle</code> - Error
|
||||
<code>faExclamationTriangle</code> - Warning
|
||||
<code>faInfoCircle</code> - Information
|
||||
<code>faSpinner</code> - Loading
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="reference-category">
|
||||
<h4>User Interface</h4>
|
||||
<div class="reference-list">
|
||||
<code>faUser</code> - User/Profile
|
||||
<code>faCog</code> - Settings
|
||||
<code>faSearch</code> - Search
|
||||
<code>faEye</code> - View/Show
|
||||
<code>faEyeSlash</code> - Hide
|
||||
<code>faDownload</code> - Download
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Demo -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Demo</h3>
|
||||
<div class="interactive-buttons">
|
||||
<ui-button
|
||||
variant="filled"
|
||||
[icon]="faRefresh"
|
||||
iconPosition="left"
|
||||
(clicked)="refreshDemo()">
|
||||
Refresh Demo
|
||||
</ui-button>
|
||||
<ui-button
|
||||
variant="tonal"
|
||||
[icon]="faDownload"
|
||||
iconPosition="left"
|
||||
(clicked)="downloadDemo()">
|
||||
Download Examples
|
||||
</ui-button>
|
||||
<ui-button
|
||||
variant="outlined"
|
||||
[icon]="faGithub"
|
||||
iconPosition="left"
|
||||
(clicked)="viewSource()">
|
||||
View Source
|
||||
</ui-button>
|
||||
</div>
|
||||
|
||||
@if (lastAction) {
|
||||
<div class="action-feedback">
|
||||
<fa-icon [icon]="faInfoCircle"></fa-icon>
|
||||
<span>{{ lastAction }}</span>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './fontawesome-demo.component.scss'
|
||||
})
|
||||
export class FontAwesomeDemoComponent {
|
||||
lastAction: string = '';
|
||||
isLoading: boolean = false;
|
||||
|
||||
// Export icons for template use
|
||||
faInfoCircle = faInfoCircle;
|
||||
faUser = faUser;
|
||||
faHome = faHome;
|
||||
faCog = faCog;
|
||||
faSearch = faSearch;
|
||||
faPlus = faPlus;
|
||||
faEdit = faEdit;
|
||||
faTrash = faTrash;
|
||||
faSave = faSave;
|
||||
faCancel = faCancel;
|
||||
faCheck = faCheck;
|
||||
faTimes = faTimes;
|
||||
faArrowLeft = faArrowLeft;
|
||||
faArrowRight = faArrowRight;
|
||||
faChevronLeft = faChevronLeft;
|
||||
faChevronRight = faChevronRight;
|
||||
faChevronUp = faChevronUp;
|
||||
faChevronDown = faChevronDown;
|
||||
faBars = faBars;
|
||||
faEllipsisV = faEllipsisV;
|
||||
faEye = faEye;
|
||||
faEyeSlash = faEyeSlash;
|
||||
faDownload = faDownload;
|
||||
faUpload = faUpload;
|
||||
faRefresh = faRefresh;
|
||||
faSpinner = faSpinner;
|
||||
faExclamationTriangle = faExclamationTriangle;
|
||||
faCheckCircle = faCheckCircle;
|
||||
faTimesCircle = faTimesCircle;
|
||||
faEnvelope = faEnvelope;
|
||||
|
||||
// Regular icons
|
||||
faHeart = faHeart;
|
||||
faBookmark = faBookmark;
|
||||
faComment = faComment;
|
||||
faThumbsUp = faThumbsUp;
|
||||
|
||||
// Brand icons
|
||||
faGithub = faGithub;
|
||||
faTwitter = faTwitter;
|
||||
faLinkedin = faLinkedin;
|
||||
faGoogle = faGoogle;
|
||||
|
||||
refreshDemo(): void {
|
||||
this.lastAction = 'Demo refreshed - all selections cleared';
|
||||
console.log('Demo refreshed');
|
||||
}
|
||||
|
||||
downloadDemo(): void {
|
||||
this.lastAction = 'Demo download initiated (simulated)';
|
||||
console.log('Download demo');
|
||||
}
|
||||
|
||||
viewSource(): void {
|
||||
this.lastAction = 'Opening source code (simulated)';
|
||||
console.log('View source');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
font-size: $semantic-typography-heading-h2-size;
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
font-weight: $semantic-typography-font-weight-bold;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: $semantic-typography-heading-h3-size;
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-lg;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-layout-md;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
h4 {
|
||||
font-size: $semantic-typography-heading-h4-size;
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-list-item;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-actions {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-content-paragraph;
|
||||
align-items: center;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-variant;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border: none;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-primary-hover;
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
color: $semantic-color-text-tertiary;
|
||||
margin-top: $semantic-spacing-content-list-item;
|
||||
}
|
||||
|
||||
.form-demo {
|
||||
display: grid;
|
||||
gap: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
.demo-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
font-size: $semantic-typography-font-size-md;
|
||||
color: $semantic-color-text-primary;
|
||||
background: $semantic-color-surface-primary;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-in-out;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $semantic-color-primary;
|
||||
box-shadow: 0 0 0 2px $semantic-color-primary-container;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: $semantic-color-surface-disabled;
|
||||
color: $semantic-color-text-disabled;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&--error {
|
||||
border-color: $semantic-color-error;
|
||||
background: $semantic-color-error-container;
|
||||
}
|
||||
|
||||
&--success {
|
||||
border-color: $semantic-color-success;
|
||||
background: $semantic-color-tertiary-container;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-textarea {
|
||||
@extend .demo-input;
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.demo-select {
|
||||
@extend .demo-input;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.demo-checkbox,
|
||||
.demo-radio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-content-line-normal;
|
||||
cursor: pointer;
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.validation-demo {
|
||||
display: grid;
|
||||
gap: $semantic-spacing-content-paragraph;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-variant;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
}
|
||||
|
||||
.form-status {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
font-family: 'JetBrains Mono', 'Consolas', monospace;
|
||||
font-size: $semantic-typography-font-size-xs;
|
||||
}
|
||||
|
||||
.code-demo {
|
||||
background: $semantic-color-surface-variant;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
margin-top: $semantic-spacing-content-paragraph;
|
||||
|
||||
pre {
|
||||
font-family: 'JetBrains Mono', 'Consolas', monospace;
|
||||
font-size: $semantic-typography-font-size-xs;
|
||||
color: $semantic-color-text-secondary;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-demo {
|
||||
display: grid;
|
||||
gap: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
// Ensure form fields are properly contained
|
||||
ui-form-field {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: $semantic-spacing-layout-sm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,669 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from '@angular/forms';
|
||||
import { FormFieldComponent } from '../../../../../ui-essentials/src/lib/components/forms/form-field';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-form-field-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, FormFieldComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Form Field Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-grid">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ size.toUpperCase() }} Size</h4>
|
||||
<p>Form field with {{ size }} sizing.</p>
|
||||
<ui-form-field
|
||||
[size]="size"
|
||||
label="Label ({{ size }})"
|
||||
helperText="This is helper text"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="text"
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Layout Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Layout Options</h3>
|
||||
<div class="layout-demo">
|
||||
<div class="demo-item">
|
||||
<h4>Vertical Layout (Default)</h4>
|
||||
<p>Standard vertical layout with label above the control.</p>
|
||||
<ui-form-field
|
||||
label="Vertical Label"
|
||||
helperText="Helper text appears below the control"
|
||||
layout="vertical"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="text"
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Horizontal Layout</h4>
|
||||
<p>Horizontal layout with label beside the control.</p>
|
||||
<ui-form-field
|
||||
label="Horizontal Label"
|
||||
helperText="Helper text appears below the control"
|
||||
layout="horizontal"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="text"
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Compact Layout</h4>
|
||||
<p>Compact spacing for tighter layouts.</p>
|
||||
<ui-form-field
|
||||
label="Compact Label"
|
||||
helperText="Reduced spacing"
|
||||
[compact]="true"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="text"
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- State Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>Default State</h4>
|
||||
<p>Normal state with no validation.</p>
|
||||
<ui-form-field
|
||||
label="Default Field"
|
||||
helperText="This is in default state"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="text"
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Error State</h4>
|
||||
<p>Error state with error message.</p>
|
||||
<ui-form-field
|
||||
label="Error Field"
|
||||
state="error"
|
||||
errorMessage="This field has an error"
|
||||
>
|
||||
<input
|
||||
class="demo-input demo-input--error"
|
||||
type="text"
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Success State</h4>
|
||||
<p>Success state with success message.</p>
|
||||
<ui-form-field
|
||||
label="Success Field"
|
||||
state="success"
|
||||
successMessage="This field is valid"
|
||||
>
|
||||
<input
|
||||
class="demo-input demo-input--success"
|
||||
type="text"
|
||||
value="Valid input"
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Warning State</h4>
|
||||
<p>Warning state with warning message.</p>
|
||||
<ui-form-field
|
||||
label="Warning Field"
|
||||
state="warning"
|
||||
warningMessage="This is a warning message"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="text"
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Info State</h4>
|
||||
<p>Info state with informational message.</p>
|
||||
<ui-form-field
|
||||
label="Info Field"
|
||||
state="info"
|
||||
infoMessage="This is informational text"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="text"
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Disabled State</h4>
|
||||
<p>Disabled field with disabled styling.</p>
|
||||
<ui-form-field
|
||||
label="Disabled Field"
|
||||
helperText="This field is disabled"
|
||||
[disabled]="true"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="text"
|
||||
placeholder="Disabled input"
|
||||
disabled
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Required and Optional -->
|
||||
<section class="demo-section">
|
||||
<h3>Required and Optional Fields</h3>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>Required Field</h4>
|
||||
<p>Field marked as required with asterisk.</p>
|
||||
<ui-form-field
|
||||
label="Required Field"
|
||||
[required]="true"
|
||||
helperText="This field is required"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="text"
|
||||
placeholder="Required input"
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Optional Field</h4>
|
||||
<p>Field marked as optional with (optional) text.</p>
|
||||
<ui-form-field
|
||||
label="Optional Field"
|
||||
[showOptional]="true"
|
||||
helperText="This field is optional"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="text"
|
||||
placeholder="Optional input"
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Different Input Types -->
|
||||
<section class="demo-section">
|
||||
<h3>Input Types</h3>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>Text Input</h4>
|
||||
<ui-form-field
|
||||
label="Text Input"
|
||||
helperText="Enter any text"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="text"
|
||||
placeholder="Text input"
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Email Input</h4>
|
||||
<ui-form-field
|
||||
label="Email Input"
|
||||
helperText="Enter a valid email address"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Number Input</h4>
|
||||
<ui-form-field
|
||||
label="Number Input"
|
||||
helperText="Enter a number"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="number"
|
||||
placeholder="123"
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Textarea</h4>
|
||||
<ui-form-field
|
||||
label="Textarea"
|
||||
helperText="Enter multiple lines of text"
|
||||
>
|
||||
<textarea
|
||||
class="demo-textarea"
|
||||
placeholder="Multi-line text input"
|
||||
></textarea>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Select</h4>
|
||||
<ui-form-field
|
||||
label="Select"
|
||||
helperText="Choose an option"
|
||||
>
|
||||
<select class="demo-select">
|
||||
<option value="">Choose an option</option>
|
||||
<option value="option1">Option 1</option>
|
||||
<option value="option2">Option 2</option>
|
||||
<option value="option3">Option 3</option>
|
||||
</select>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Checkbox Group</h4>
|
||||
<ui-form-field
|
||||
label="Preferences"
|
||||
helperText="Select your preferences"
|
||||
[group]="true"
|
||||
>
|
||||
<div class="demo-checkbox">
|
||||
<input type="checkbox" id="pref1" />
|
||||
<label for="pref1">Option 1</label>
|
||||
</div>
|
||||
<div class="demo-checkbox">
|
||||
<input type="checkbox" id="pref2" />
|
||||
<label for="pref2">Option 2</label>
|
||||
</div>
|
||||
<div class="demo-checkbox">
|
||||
<input type="checkbox" id="pref3" />
|
||||
<label for="pref3">Option 3</label>
|
||||
</div>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Character Count -->
|
||||
<section class="demo-section">
|
||||
<h3>Character Count</h3>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>With Character Counter</h4>
|
||||
<p>Shows character count with maximum limit.</p>
|
||||
<ui-form-field
|
||||
label="Limited Text"
|
||||
helperText="Maximum 50 characters"
|
||||
[showCharacterCount]="true"
|
||||
[maxLength]="50"
|
||||
[currentLength]="characterCountText.length"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="text"
|
||||
placeholder="Type here..."
|
||||
[value]="characterCountText"
|
||||
(input)="updateCharacterCount($event)"
|
||||
maxlength="50"
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Hint Text -->
|
||||
<section class="demo-section">
|
||||
<h3>Hint Text</h3>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>With Hint</h4>
|
||||
<p>Additional helpful information with icon.</p>
|
||||
<ui-form-field
|
||||
label="Password"
|
||||
helperText="Enter a secure password"
|
||||
hintText="Password should be at least 8 characters long and contain uppercase, lowercase, numbers, and special characters"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="password"
|
||||
placeholder="Enter password"
|
||||
/>
|
||||
</ui-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Reactive Form Integration -->
|
||||
<section class="demo-section">
|
||||
<h3>Reactive Form Integration</h3>
|
||||
<div class="demo-item">
|
||||
<h4>Form with Validation</h4>
|
||||
<p>Form fields with Angular reactive form validation.</p>
|
||||
|
||||
<form [formGroup]="validationForm" class="form-demo">
|
||||
<!-- Required Field -->
|
||||
<ui-form-field
|
||||
label="Full Name"
|
||||
[required]="true"
|
||||
helperText="Enter your full name"
|
||||
[validationMessages]="validationMessages"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="text"
|
||||
placeholder="John Doe"
|
||||
formControlName="name"
|
||||
[class.demo-input--error]="isFieldInvalid('name')"
|
||||
[class.demo-input--success]="isFieldValid('name')"
|
||||
/>
|
||||
</ui-form-field>
|
||||
|
||||
<!-- Email with validation -->
|
||||
<ui-form-field
|
||||
label="Email Address"
|
||||
[required]="true"
|
||||
helperText="We'll never share your email"
|
||||
[validationMessages]="validationMessages"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="email"
|
||||
placeholder="john@example.com"
|
||||
formControlName="email"
|
||||
[class.demo-input--error]="isFieldInvalid('email')"
|
||||
[class.demo-input--success]="isFieldValid('email')"
|
||||
/>
|
||||
</ui-form-field>
|
||||
|
||||
<!-- Password with length validation -->
|
||||
<ui-form-field
|
||||
label="Password"
|
||||
[required]="true"
|
||||
helperText="Must be at least 8 characters"
|
||||
[validationMessages]="validationMessages"
|
||||
[showCharacterCount]="true"
|
||||
[maxLength]="50"
|
||||
[currentLength]="getFieldValue('password')?.length || 0"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="password"
|
||||
placeholder="Enter password"
|
||||
formControlName="password"
|
||||
[class.demo-input--error]="isFieldInvalid('password')"
|
||||
[class.demo-input--success]="isFieldValid('password')"
|
||||
/>
|
||||
</ui-form-field>
|
||||
|
||||
<!-- Age with number validation -->
|
||||
<ui-form-field
|
||||
label="Age"
|
||||
[showOptional]="true"
|
||||
helperText="Must be between 18 and 100"
|
||||
[validationMessages]="validationMessages"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="number"
|
||||
placeholder="25"
|
||||
formControlName="age"
|
||||
[class.demo-input--error]="isFieldInvalid('age')"
|
||||
[class.demo-input--success]="isFieldValid('age')"
|
||||
/>
|
||||
</ui-form-field>
|
||||
|
||||
<!-- Website with URL validation -->
|
||||
<ui-form-field
|
||||
label="Website"
|
||||
[showOptional]="true"
|
||||
helperText="Enter a valid URL"
|
||||
hintText="Include http:// or https://"
|
||||
[validationMessages]="validationMessages"
|
||||
>
|
||||
<input
|
||||
class="demo-input"
|
||||
type="url"
|
||||
placeholder="https://example.com"
|
||||
formControlName="website"
|
||||
[class.demo-input--error]="isFieldInvalid('website')"
|
||||
[class.demo-input--success]="isFieldValid('website')"
|
||||
/>
|
||||
</ui-form-field>
|
||||
|
||||
<!-- Bio with custom validation -->
|
||||
<ui-form-field
|
||||
label="Bio"
|
||||
[showOptional]="true"
|
||||
helperText="Tell us about yourself"
|
||||
[showCharacterCount]="true"
|
||||
[maxLength]="200"
|
||||
[currentLength]="getFieldValue('bio')?.length || 0"
|
||||
[validationMessages]="validationMessages"
|
||||
>
|
||||
<textarea
|
||||
class="demo-textarea"
|
||||
placeholder="Write a short bio..."
|
||||
formControlName="bio"
|
||||
[class.demo-input--error]="isFieldInvalid('bio')"
|
||||
[class.demo-input--success]="isFieldValid('bio')"
|
||||
maxlength="200"
|
||||
></textarea>
|
||||
</ui-form-field>
|
||||
</form>
|
||||
|
||||
<div class="demo-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="demo-button"
|
||||
(click)="markAllTouched()"
|
||||
>
|
||||
Validate All
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="demo-button demo-button--secondary"
|
||||
(click)="resetValidationForm()"
|
||||
>
|
||||
Reset Form
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="demo-button demo-button--secondary"
|
||||
(click)="fillSampleData()"
|
||||
>
|
||||
Fill Sample Data
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-status">
|
||||
<div><strong>Form Status:</strong> {{ validationForm.status }}</div>
|
||||
<div><strong>Form Valid:</strong> {{ validationForm.valid }}</div>
|
||||
<div><strong>Form Touched:</strong> {{ validationForm.touched }}</div>
|
||||
<div><strong>Form Dirty:</strong> {{ validationForm.dirty }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Code Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Usage Example</h3>
|
||||
<div class="code-demo">
|
||||
<pre><code>{{ codeExample }}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './form-field-demo.component.scss'
|
||||
})
|
||||
export class FormFieldDemoComponent implements OnInit {
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
characterCountText = '';
|
||||
|
||||
validationForm = new FormGroup({
|
||||
name: new FormControl('', [Validators.required, Validators.minLength(2)]),
|
||||
email: new FormControl('', [Validators.required, Validators.email]),
|
||||
password: new FormControl('', [Validators.required, Validators.minLength(8)]),
|
||||
age: new FormControl('', [Validators.min(18), Validators.max(100)]),
|
||||
website: new FormControl('', [this.urlValidator]),
|
||||
bio: new FormControl('', [Validators.maxLength(200)])
|
||||
});
|
||||
|
||||
validationMessages = {
|
||||
required: 'This field is required',
|
||||
email: 'Please enter a valid email address',
|
||||
minlength: 'This field is too short',
|
||||
maxlength: 'This field is too long',
|
||||
min: 'Value is too low',
|
||||
max: 'Value is too high',
|
||||
url: 'Please enter a valid URL',
|
||||
custom: 'Custom validation error'
|
||||
};
|
||||
|
||||
readonly codeExample = `import { FormFieldComponent } from 'ui-essentials';
|
||||
|
||||
// Basic usage
|
||||
<ui-form-field
|
||||
label="Email Address"
|
||||
helperText="We'll never share your email"
|
||||
[required]="true"
|
||||
>
|
||||
<input type="email" formControlName="email" />
|
||||
</ui-form-field>
|
||||
|
||||
// With validation messages
|
||||
<ui-form-field
|
||||
label="Password"
|
||||
[required]="true"
|
||||
helperText="Must be at least 8 characters"
|
||||
[validationMessages]="validationMessages"
|
||||
[showCharacterCount]="true"
|
||||
[maxLength]="50"
|
||||
>
|
||||
<input type="password" formControlName="password" />
|
||||
</ui-form-field>
|
||||
|
||||
// Horizontal layout
|
||||
<ui-form-field
|
||||
label="Settings"
|
||||
layout="horizontal"
|
||||
helperText="Choose your preferences"
|
||||
>
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="option1" />
|
||||
<label for="option1">Option 1</label>
|
||||
</div>
|
||||
</ui-form-field>
|
||||
|
||||
// Custom states
|
||||
<ui-form-field
|
||||
label="Status Field"
|
||||
state="success"
|
||||
successMessage="Data saved successfully"
|
||||
>
|
||||
<input type="text" readonly value="Saved" />
|
||||
</ui-form-field>`;
|
||||
|
||||
ngOnInit(): void {
|
||||
// Watch form changes for demo purposes
|
||||
this.validationForm.valueChanges.subscribe(() => {
|
||||
// Update demo display if needed
|
||||
});
|
||||
}
|
||||
|
||||
updateCharacterCount(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
this.characterCountText = input.value;
|
||||
}
|
||||
|
||||
isFieldInvalid(fieldName: string): boolean {
|
||||
const field = this.validationForm.get(fieldName);
|
||||
return field ? field.invalid && (field.dirty || field.touched) : false;
|
||||
}
|
||||
|
||||
isFieldValid(fieldName: string): boolean {
|
||||
const field = this.validationForm.get(fieldName);
|
||||
return field ? field.valid && field.value && (field.dirty || field.touched) : false;
|
||||
}
|
||||
|
||||
getFieldValue(fieldName: string): any {
|
||||
return this.validationForm.get(fieldName)?.value;
|
||||
}
|
||||
|
||||
markAllTouched(): void {
|
||||
this.validationForm.markAllAsTouched();
|
||||
}
|
||||
|
||||
resetValidationForm(): void {
|
||||
this.validationForm.reset();
|
||||
Object.keys(this.validationForm.controls).forEach(key => {
|
||||
this.validationForm.get(key)?.setErrors(null);
|
||||
});
|
||||
}
|
||||
|
||||
fillSampleData(): void {
|
||||
this.validationForm.patchValue({
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
password: 'SecurePass123!',
|
||||
age: "30",
|
||||
website: 'https://johndoe.com',
|
||||
bio: 'Software developer with 10 years of experience in web development.'
|
||||
});
|
||||
this.validationForm.markAllAsTouched();
|
||||
}
|
||||
|
||||
// Custom URL validator
|
||||
private urlValidator(control: AbstractControl): ValidationErrors | null {
|
||||
if (!control.value) return null;
|
||||
|
||||
try {
|
||||
new URL(control.value);
|
||||
return null;
|
||||
} catch {
|
||||
return { url: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic' as tokens;
|
||||
|
||||
.demo-container {
|
||||
padding: tokens.$semantic-spacing-layout-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: tokens.$semantic-spacing-layout-lg;
|
||||
|
||||
h3 {
|
||||
margin-bottom: tokens.$semantic-spacing-component-md;
|
||||
color: tokens.$semantic-color-text-primary;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: tokens.$semantic-spacing-component-sm;
|
||||
color: tokens.$semantic-color-text-secondary;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
color: tokens.$semantic-color-text-secondary;
|
||||
margin-bottom: tokens.$semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: tokens.$semantic-spacing-layout-md;
|
||||
}
|
||||
|
||||
.demo-grid-container {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
margin-bottom: tokens.$semantic-spacing-layout-sm;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
border: 2px dashed tokens.$semantic-color-border-subtle;
|
||||
border-radius: tokens.$semantic-border-radius-md;
|
||||
padding: tokens.$semantic-spacing-component-md;
|
||||
|
||||
&--fixed-height {
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-grid-item {
|
||||
background: tokens.$semantic-color-container-primary;
|
||||
color: tokens.$semantic-color-on-container-primary;
|
||||
padding: tokens.$semantic-spacing-component-sm;
|
||||
border-radius: tokens.$semantic-border-radius-sm;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&--small {
|
||||
width: 80px;
|
||||
height: 40px;
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-grid-container {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { GridSystemComponent } from '../../../../../ui-essentials/src/lib/components/layout';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-grid-system-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, GridSystemComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Grid System Demo</h2>
|
||||
|
||||
<!-- Column Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Column Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (cols of columnVariants; track cols) {
|
||||
<div class="demo-grid-container">
|
||||
<h4>{{ cols }} Columns</h4>
|
||||
<ui-grid-system [columns]="cols" class="demo-grid">
|
||||
@for (item of getItems(getItemCount(cols)); track $index) {
|
||||
<div class="demo-grid-item">{{ $index + 1 }}</div>
|
||||
}
|
||||
</ui-grid-system>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gap Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Gap Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (gap of gapSizes; track gap) {
|
||||
<div class="demo-grid-container">
|
||||
<h4>Gap: {{ gap }}</h4>
|
||||
<ui-grid-system [columns]="3" [gap]="gap" class="demo-grid">
|
||||
@for (item of getItems(6); track $index) {
|
||||
<div class="demo-grid-item">{{ $index + 1 }}</div>
|
||||
}
|
||||
</ui-grid-system>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Auto-fit and Auto-fill -->
|
||||
<section class="demo-section">
|
||||
<h3>Auto Layout</h3>
|
||||
<div class="demo-grid-container">
|
||||
<h4>Auto-fit (items stretch to fill)</h4>
|
||||
<ui-grid-system columns="auto-fit" class="demo-grid">
|
||||
@for (item of getItems(4); track $index) {
|
||||
<div class="demo-grid-item">Auto-fit {{ $index + 1 }}</div>
|
||||
}
|
||||
</ui-grid-system>
|
||||
</div>
|
||||
<div class="demo-grid-container">
|
||||
<h4>Auto-fill (creates empty columns)</h4>
|
||||
<ui-grid-system columns="auto-fill" class="demo-grid">
|
||||
@for (item of getItems(4); track $index) {
|
||||
<div class="demo-grid-item">Auto-fill {{ $index + 1 }}</div>
|
||||
}
|
||||
</ui-grid-system>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Alignment -->
|
||||
<section class="demo-section">
|
||||
<h3>Alignment Options</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-grid-container">
|
||||
<h4>Justify Items: Center</h4>
|
||||
<ui-grid-system [columns]="3" justifyItems="center" class="demo-grid demo-grid--fixed-height">
|
||||
@for (item of getItems(6); track $index) {
|
||||
<div class="demo-grid-item demo-grid-item--small">{{ $index + 1 }}</div>
|
||||
}
|
||||
</ui-grid-system>
|
||||
</div>
|
||||
<div class="demo-grid-container">
|
||||
<h4>Align Items: Center</h4>
|
||||
<ui-grid-system [columns]="3" alignItems="center" class="demo-grid demo-grid--fixed-height">
|
||||
@for (item of getItems(6); track $index) {
|
||||
<div class="demo-grid-item demo-grid-item--small">{{ $index + 1 }}</div>
|
||||
}
|
||||
</ui-grid-system>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Custom Grid -->
|
||||
<section class="demo-section">
|
||||
<h3>Custom Grid Template</h3>
|
||||
<div class="demo-grid-container">
|
||||
<h4>Custom Columns: 200px 1fr 100px</h4>
|
||||
<ui-grid-system customColumns="200px 1fr 100px" class="demo-grid">
|
||||
<div class="demo-grid-item">Fixed 200px</div>
|
||||
<div class="demo-grid-item">Flexible 1fr</div>
|
||||
<div class="demo-grid-item">Fixed 100px</div>
|
||||
</ui-grid-system>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Responsive Grid -->
|
||||
<section class="demo-section">
|
||||
<h3>Responsive Grid</h3>
|
||||
<p>Resize the window to see responsive behavior</p>
|
||||
<ui-grid-system [columns]="12" [responsive]="true" class="demo-grid">
|
||||
@for (item of getItems(24); track $index) {
|
||||
<div class="demo-grid-item">{{ $index + 1 }}</div>
|
||||
}
|
||||
</ui-grid-system>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './grid-system-demo.component.scss'
|
||||
})
|
||||
export class GridSystemDemoComponent {
|
||||
columnVariants: (1 | 2 | 3 | 4 | 6 | 12)[] = [1, 2, 3, 4, 6, 12];
|
||||
gapSizes: ('xs' | 'sm' | 'md' | 'lg' | 'xl')[] = ['xs', 'sm', 'md', 'lg', 'xl'];
|
||||
|
||||
getItems(count: number): number[] {
|
||||
return Array.from({ length: count }, (_, i) => i + 1);
|
||||
}
|
||||
|
||||
getItemCount(cols: number | string): number {
|
||||
if (typeof cols === 'number') {
|
||||
return Math.min(cols * 2, 12);
|
||||
}
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
@import "../../../../../shared-ui/src/styles/semantic";
|
||||
|
||||
// Tokens available globally via main application styles
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-md;
|
||||
|
||||
h2 {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: $base-typography-font-size-xl;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: $semantic-spacing-layout-lg;
|
||||
color: $semantic-color-text-secondary;
|
||||
font-size: $base-typography-font-size-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-xl;
|
||||
|
||||
h3 {
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: $base-typography-font-size-lg;
|
||||
border-bottom: $semantic-border-divider-width solid $semantic-color-border-subtle;
|
||||
padding-bottom: $semantic-spacing-component-xs;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: $semantic-spacing-component-md;
|
||||
margin-bottom: $semantic-spacing-layout-lg;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
|
||||
.ui-image-container {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: $semantic-shadow-elevation-3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-label {
|
||||
font-size: $base-typography-font-size-sm;
|
||||
color: $semantic-color-text-secondary;
|
||||
font-weight: $base-typography-font-weight-medium;
|
||||
text-align: center;
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
border: $semantic-border-divider-width solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
// Custom styling for hero image demo
|
||||
.hero-image {
|
||||
position: relative;
|
||||
|
||||
[slot='caption'] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: $base-typography-font-size-md;
|
||||
font-weight: $base-typography-font-weight-semibold;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: $base-typography-font-size-sm;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retry button styling
|
||||
.retry-btn {
|
||||
margin-top: $semantic-spacing-component-xs;
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
background: $semantic-color-interactive-primary;
|
||||
color: $semantic-color-text-inverse;
|
||||
border: none;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
font-size: $base-typography-font-size-xs;
|
||||
cursor: pointer;
|
||||
transition: background-color $semantic-duration-fast $semantic-easing-standard;
|
||||
|
||||
&:hover {
|
||||
background-color: $semantic-color-surface-interactive;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus-ring;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Event log styling
|
||||
.event-log {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-divider-width solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
font-family: monospace;
|
||||
font-size: $base-typography-font-size-xs;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.event-item {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.event-time {
|
||||
color: $semantic-color-text-tertiary;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.event-type {
|
||||
color: $semantic-color-interactive-primary;
|
||||
font-weight: $base-typography-font-weight-medium;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.event-message {
|
||||
color: $semantic-color-text-secondary;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: $semantic-sizing-breakpoint-tablet - 1) {
|
||||
.demo-row {
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-gallery {
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-sizing-breakpoint-mobile - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-sm;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-lg;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-gallery {
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
}
|
||||
|
||||
.event-log {
|
||||
font-size: $base-typography-font-size-xs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ImageContainerComponent, ImageContainerSize, ImageContainerAspectRatio, ImageContainerObjectFit, ImageContainerShape } from '../../../../../ui-essentials/src/lib/components/data-display/image-container';
|
||||
import { BadgeComponent } from '../../../../../ui-essentials/src/lib/components/data-display/badge';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { faRefresh, faHeart, faPlay } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-image-container-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ImageContainerComponent, BadgeComponent, FontAwesomeModule],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Image Container Demo</h2>
|
||||
<p>A flexible image container with lazy loading, aspect ratios, and various display options.</p>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-item">
|
||||
<ui-image-container
|
||||
[size]="size"
|
||||
[src]="sampleImages.landscape"
|
||||
[alt]="'Sample image - ' + size + ' size'">
|
||||
</ui-image-container>
|
||||
<span class="demo-label">{{ size }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Aspect Ratios -->
|
||||
<section class="demo-section">
|
||||
<h3>Aspect Ratios</h3>
|
||||
<div class="demo-grid">
|
||||
@for (ratio of aspectRatios; track ratio) {
|
||||
<div class="demo-item">
|
||||
<ui-image-container
|
||||
size="md"
|
||||
[aspectRatio]="ratio"
|
||||
[src]="sampleImages.landscape"
|
||||
[alt]="'Sample image - ' + ratio + ' aspect ratio'">
|
||||
</ui-image-container>
|
||||
<span class="demo-label">{{ ratio }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Object Fit Options -->
|
||||
<section class="demo-section">
|
||||
<h3>Object Fit</h3>
|
||||
<div class="demo-row">
|
||||
@for (fit of objectFits; track fit) {
|
||||
<div class="demo-item">
|
||||
<ui-image-container
|
||||
size="md"
|
||||
aspectRatio="1/1"
|
||||
[objectFit]="fit"
|
||||
[src]="sampleImages.landscape"
|
||||
[alt]="'Sample image with ' + fit + ' object fit'">
|
||||
</ui-image-container>
|
||||
<span class="demo-label">{{ fit }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Shape Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Shapes</h3>
|
||||
<div class="demo-row">
|
||||
@for (shape of shapes; track shape) {
|
||||
<div class="demo-item">
|
||||
<ui-image-container
|
||||
size="md"
|
||||
aspectRatio="1/1"
|
||||
[shape]="shape"
|
||||
[src]="sampleImages.portrait"
|
||||
[alt]="'Sample image with ' + shape + ' shape'">
|
||||
</ui-image-container>
|
||||
<span class="demo-label">{{ shape }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Loading States -->
|
||||
<section class="demo-section">
|
||||
<h3>Loading States</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<ui-image-container
|
||||
size="md"
|
||||
[loading]="true"
|
||||
src="placeholder"
|
||||
alt="Loading image">
|
||||
</ui-image-container>
|
||||
<span class="demo-label">Loading</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<ui-image-container
|
||||
size="md"
|
||||
src="invalid-url.jpg"
|
||||
alt="Failed to load image">
|
||||
<div slot="error">
|
||||
<fa-icon [icon]="faRefresh" size="lg"></fa-icon>
|
||||
<span>Custom Error</span>
|
||||
<button class="retry-btn" (click)="retryImage($event)">Retry</button>
|
||||
</div>
|
||||
</ui-image-container>
|
||||
<span class="demo-label">Error State</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<ui-image-container
|
||||
size="md"
|
||||
[src]="sampleImages.landscape"
|
||||
[lazy]="false"
|
||||
alt="Eager loaded image">
|
||||
</ui-image-container>
|
||||
<span class="demo-label">Eager Load</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Features -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Features</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<ui-image-container
|
||||
size="lg"
|
||||
aspectRatio="16/9"
|
||||
[src]="sampleImages.landscape"
|
||||
[overlay]="true"
|
||||
alt="Image with overlay"
|
||||
(imageClick)="handleImageClick('overlay')">
|
||||
<div slot="overlay">
|
||||
<fa-icon [icon]="faPlay" size="2x"></fa-icon>
|
||||
<span>Play Video</span>
|
||||
</div>
|
||||
</ui-image-container>
|
||||
<span class="demo-label">With Overlay</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<ui-image-container
|
||||
size="lg"
|
||||
aspectRatio="4/3"
|
||||
[src]="sampleImages.landscape"
|
||||
caption="Beautiful landscape scene"
|
||||
alt="Image with caption"
|
||||
(imageClick)="handleImageClick('caption')">
|
||||
</ui-image-container>
|
||||
<span class="demo-label">With Caption</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Advanced Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Advanced Examples</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<ui-image-container
|
||||
size="lg"
|
||||
aspectRatio="1/1"
|
||||
shape="circle"
|
||||
[src]="sampleImages.portrait"
|
||||
[overlay]="true"
|
||||
alt="Profile picture with status"
|
||||
(imageClick)="handleImageClick('profile')">
|
||||
<div slot="overlay">
|
||||
<fa-icon [icon]="faHeart" style="color: red;"></fa-icon>
|
||||
</div>
|
||||
</ui-image-container>
|
||||
<span class="demo-label">Profile Avatar</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<ui-image-container
|
||||
size="xl"
|
||||
aspectRatio="16/9"
|
||||
[src]="sampleImages.landscape"
|
||||
caption="Hero image with custom styling"
|
||||
alt="Hero banner image"
|
||||
class="hero-image">
|
||||
<div slot="caption">
|
||||
<h4>Custom Caption Content</h4>
|
||||
<p>With additional styled elements</p>
|
||||
<ui-badge variant="primary" size="xs">Featured</ui-badge>
|
||||
</div>
|
||||
</ui-image-container>
|
||||
<span class="demo-label">Hero Banner</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Responsive Gallery -->
|
||||
<section class="demo-section">
|
||||
<h3>Responsive Gallery</h3>
|
||||
<div class="demo-gallery">
|
||||
@for (image of galleryImages; track image.id; let i = $index) {
|
||||
<ui-image-container
|
||||
size="md"
|
||||
[aspectRatio]="i % 3 === 0 ? '16/9' : '1/1'"
|
||||
[src]="image.url"
|
||||
[alt]="image.alt"
|
||||
[overlay]="true"
|
||||
(imageClick)="handleGalleryClick(image, i)"
|
||||
(imageLoaded)="handleImageLoad(image.id)"
|
||||
(imageError)="handleImageError(image.id)">
|
||||
<div slot="overlay">
|
||||
<span>{{ image.title }}</span>
|
||||
</div>
|
||||
</ui-image-container>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Event Log -->
|
||||
@if (eventLog.length > 0) {
|
||||
<section class="demo-section">
|
||||
<h3>Event Log</h3>
|
||||
<div class="event-log">
|
||||
@for (event of eventLog.slice(-5); track event.id) {
|
||||
<div class="event-item">
|
||||
<span class="event-time">{{ event.time | date:'HH:mm:ss' }}</span>
|
||||
<span class="event-type">{{ event.type }}</span>
|
||||
<span class="event-message">{{ event.message }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './image-container-demo.component.scss'
|
||||
})
|
||||
export class ImageContainerDemoComponent {
|
||||
// Icons
|
||||
faRefresh = faRefresh;
|
||||
faHeart = faHeart;
|
||||
faPlay = faPlay;
|
||||
|
||||
// Demo data
|
||||
sizes: ImageContainerSize[] = ['sm', 'md', 'lg', 'xl'];
|
||||
aspectRatios: ImageContainerAspectRatio[] = ['1/1', '4/3', '16/9', '3/2', '2/1', '3/4', '9/16'];
|
||||
objectFits: ImageContainerObjectFit[] = ['contain', 'cover', 'fill', 'scale-down'];
|
||||
shapes: ImageContainerShape[] = ['square', 'rounded', 'circle'];
|
||||
|
||||
// Sample images (using placeholder service)
|
||||
sampleImages = {
|
||||
landscape: 'https://picsum.photos/800/600?random=1',
|
||||
portrait: 'https://picsum.photos/600/800?random=2',
|
||||
square: 'https://picsum.photos/600/600?random=3'
|
||||
};
|
||||
|
||||
galleryImages = [
|
||||
{ id: 1, url: 'https://picsum.photos/400/400?random=10', title: 'Nature', alt: 'Beautiful nature scene' },
|
||||
{ id: 2, url: 'https://picsum.photos/400/300?random=11', title: 'Architecture', alt: 'Modern building' },
|
||||
{ id: 3, url: 'https://picsum.photos/400/500?random=12', title: 'Portrait', alt: 'Person portrait' },
|
||||
{ id: 4, url: 'https://picsum.photos/400/400?random=13', title: 'Urban', alt: 'City landscape' },
|
||||
{ id: 5, url: 'https://picsum.photos/400/300?random=14', title: 'Technology', alt: 'Tech equipment' },
|
||||
{ id: 6, url: 'https://picsum.photos/400/400?random=15', title: 'Abstract', alt: 'Abstract art' }
|
||||
];
|
||||
|
||||
// Event tracking
|
||||
eventLog: Array<{id: number, type: string, message: string, time: Date}> = [];
|
||||
private eventId = 1;
|
||||
|
||||
handleImageClick(context: string): void {
|
||||
this.logEvent('click', `Image clicked in ${context} context`);
|
||||
}
|
||||
|
||||
handleGalleryClick(image: any, index: number): void {
|
||||
this.logEvent('gallery-click', `Gallery image "${image.title}" clicked (index ${index})`);
|
||||
}
|
||||
|
||||
handleImageLoad(imageId: number): void {
|
||||
this.logEvent('load', `Image ${imageId} loaded successfully`);
|
||||
}
|
||||
|
||||
handleImageError(imageId: number): void {
|
||||
this.logEvent('error', `Image ${imageId} failed to load`);
|
||||
}
|
||||
|
||||
retryImage(event: Event): void {
|
||||
event.stopPropagation();
|
||||
this.logEvent('retry', 'Retry button clicked for failed image');
|
||||
// In a real implementation, this would retry loading the image
|
||||
}
|
||||
|
||||
private logEvent(type: string, message: string): void {
|
||||
this.eventLog.push({
|
||||
id: this.eventId++,
|
||||
type,
|
||||
message,
|
||||
time: new Date()
|
||||
});
|
||||
|
||||
// Keep only last 20 events
|
||||
if (this.eventLog.length > 20) {
|
||||
this.eventLog.splice(0, this.eventLog.length - 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './image-container-demo.component';
|
||||
17
projects/demo-ui-essentials/src/app/demos/index.ts
Normal file
17
projects/demo-ui-essentials/src/app/demos/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export * from './appbar-demo/appbar-demo.component';
|
||||
export * from './avatar-demo/avatar-demo.component';
|
||||
export * from './badge-demo/badge-demo.component';
|
||||
export * from './button-demo/button-demo.component';
|
||||
export * from './card-demo/card-demo.component';
|
||||
export * from './checkbox-demo/checkbox-demo.component';
|
||||
export * from './chip-demo/chip-demo.component';
|
||||
export * from './fontawesome-demo/fontawesome-demo.component';
|
||||
export * from './input-demo/input-demo.component';
|
||||
export * from './layout-demo/layout-demo.component';
|
||||
export * from './menu-demo/menu-demo.component';
|
||||
export * from './progress-demo/progress-demo.component';
|
||||
export * from './radio-demo/radio-demo.component';
|
||||
export * from './search-demo/search-demo.component';
|
||||
export * from './switch-demo/switch-demo.component';
|
||||
export * from './table-demo/table-demo.component';
|
||||
export * from './demos.routes';
|
||||
@@ -0,0 +1,680 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TextInputComponent } from '../../../../../ui-essentials/src/lib/components/forms/input';
|
||||
import { TextareaComponent } from '../../../../../ui-essentials/src/lib/components/forms/input';
|
||||
import { InputWrapperComponent } from '../../../../../ui-essentials/src/lib/components/forms/input';
|
||||
import { faSearch, faEnvelope, faEdit } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-input-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
TextInputComponent,
|
||||
TextareaComponent,
|
||||
InputWrapperComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Input Component Showcase</h2>
|
||||
|
||||
<!-- Text Input Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Text Input Variants</h3>
|
||||
|
||||
<!-- Outlined Variant -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Outlined (Default)</h4>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
|
||||
<ui-text-input
|
||||
label="Small Outlined"
|
||||
placeholder="Enter text..."
|
||||
size="sm"
|
||||
variant="outlined"
|
||||
[(ngModel)]="textValues['outlined-sm']"
|
||||
(inputChange)="handleInputChange('outlined-sm', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Medium Outlined"
|
||||
placeholder="Enter text..."
|
||||
size="md"
|
||||
variant="outlined"
|
||||
[(ngModel)]="textValues['outlined-md']"
|
||||
(inputChange)="handleInputChange('outlined-md', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Large Outlined"
|
||||
placeholder="Enter text..."
|
||||
size="lg"
|
||||
variant="outlined"
|
||||
[(ngModel)]="textValues['outlined-lg']"
|
||||
(inputChange)="handleInputChange('outlined-lg', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filled Variant -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Filled</h4>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
|
||||
<ui-text-input
|
||||
label="Small Filled"
|
||||
placeholder="Enter text..."
|
||||
size="sm"
|
||||
variant="filled"
|
||||
[(ngModel)]="textValues['filled-sm']"
|
||||
(inputChange)="handleInputChange('filled-sm', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Medium Filled"
|
||||
placeholder="Enter text..."
|
||||
size="md"
|
||||
variant="filled"
|
||||
[(ngModel)]="textValues['filled-md']"
|
||||
(inputChange)="handleInputChange('filled-md', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Large Filled"
|
||||
placeholder="Enter text..."
|
||||
size="lg"
|
||||
variant="filled"
|
||||
[(ngModel)]="textValues['filled-lg']"
|
||||
(inputChange)="handleInputChange('filled-lg', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Underlined Variant -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Underlined</h4>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
|
||||
<ui-text-input
|
||||
label="Small Underlined"
|
||||
placeholder="Enter text..."
|
||||
size="sm"
|
||||
variant="underlined"
|
||||
[(ngModel)]="textValues['underlined-sm']"
|
||||
(inputChange)="handleInputChange('underlined-sm', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Medium Underlined"
|
||||
placeholder="Enter text..."
|
||||
size="md"
|
||||
variant="underlined"
|
||||
[(ngModel)]="textValues['underlined-md']"
|
||||
(inputChange)="handleInputChange('underlined-md', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Large Underlined"
|
||||
placeholder="Enter text..."
|
||||
size="lg"
|
||||
variant="underlined"
|
||||
[(ngModel)]="textValues['underlined-lg']"
|
||||
(inputChange)="handleInputChange('underlined-lg', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Input Types -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Input Types</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
|
||||
<ui-text-input
|
||||
label="Email Input"
|
||||
placeholder="email@example.com"
|
||||
type="email"
|
||||
variant="outlined"
|
||||
[(ngModel)]="textValues['email']"
|
||||
(inputChange)="handleInputChange('email', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Password Input"
|
||||
placeholder="Enter password"
|
||||
type="password"
|
||||
variant="outlined"
|
||||
[(ngModel)]="textValues['password']"
|
||||
(inputChange)="handleInputChange('password', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Search Input"
|
||||
placeholder="Search..."
|
||||
type="search"
|
||||
variant="outlined"
|
||||
[(ngModel)]="textValues['search']"
|
||||
(inputChange)="handleInputChange('search', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Number Input"
|
||||
placeholder="Enter number"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
[(ngModel)]="textValues['number']"
|
||||
(inputChange)="handleInputChange('number', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Tel Input"
|
||||
placeholder="+1 (555) 000-0000"
|
||||
type="tel"
|
||||
variant="outlined"
|
||||
[(ngModel)]="textValues['tel']"
|
||||
(inputChange)="handleInputChange('tel', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="URL Input"
|
||||
placeholder="https://example.com"
|
||||
type="url"
|
||||
variant="outlined"
|
||||
[(ngModel)]="textValues['url']"
|
||||
(inputChange)="handleInputChange('url', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Input States -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Input States</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
|
||||
<ui-text-input
|
||||
label="Default State"
|
||||
placeholder="Enter text..."
|
||||
variant="outlined"
|
||||
state="default"
|
||||
helperText="This is helper text"
|
||||
[(ngModel)]="textValues['state-default']"
|
||||
(inputChange)="handleInputChange('state-default', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Error State"
|
||||
placeholder="Enter text..."
|
||||
variant="outlined"
|
||||
state="error"
|
||||
errorMessage="This field has an error"
|
||||
[(ngModel)]="textValues['state-error']"
|
||||
(inputChange)="handleInputChange('state-error', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Success State"
|
||||
placeholder="Enter text..."
|
||||
variant="outlined"
|
||||
state="success"
|
||||
helperText="Input is valid!"
|
||||
[(ngModel)]="textValues['state-success']"
|
||||
(inputChange)="handleInputChange('state-success', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Warning State"
|
||||
placeholder="Enter text..."
|
||||
variant="outlined"
|
||||
state="warning"
|
||||
helperText="Please double-check this value"
|
||||
[(ngModel)]="textValues['state-warning']"
|
||||
(inputChange)="handleInputChange('state-warning', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Inputs with Icons and Features -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Inputs with Icons and Features</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
|
||||
<ui-text-input
|
||||
label="Prefix Icon"
|
||||
placeholder="Search..."
|
||||
variant="outlined"
|
||||
[prefixIcon]="faSearch"
|
||||
[clearable]="true"
|
||||
[(ngModel)]="textValues['prefix-icon']"
|
||||
(inputChange)="handleInputChange('prefix-icon', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Prefix Text"
|
||||
placeholder="0.00"
|
||||
variant="outlined"
|
||||
prefixText="$"
|
||||
[(ngModel)]="textValues['prefix-text']"
|
||||
(inputChange)="handleInputChange('prefix-text', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Suffix Icon"
|
||||
placeholder="Enter email"
|
||||
variant="outlined"
|
||||
[suffixIcon]="faEnvelope"
|
||||
[(ngModel)]="textValues['suffix-icon']"
|
||||
(inputChange)="handleInputChange('suffix-icon', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="With Clear Button"
|
||||
placeholder="Type to see clear button"
|
||||
variant="outlined"
|
||||
[clearable]="true"
|
||||
[(ngModel)]="textValues['clearable']"
|
||||
(inputChange)="handleInputChange('clearable', $event)"
|
||||
(clear)="handleClear('clearable')"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Special States -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Special States</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
|
||||
<ui-text-input
|
||||
label="Disabled Input"
|
||||
placeholder="This is disabled"
|
||||
variant="outlined"
|
||||
[disabled]="true"
|
||||
value="Disabled value"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Readonly Input"
|
||||
placeholder="This is readonly"
|
||||
variant="outlined"
|
||||
[readonly]="true"
|
||||
value="Readonly value"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Loading Input"
|
||||
placeholder="Loading..."
|
||||
variant="outlined"
|
||||
[loading]="isLoading()"
|
||||
[(ngModel)]="textValues['loading']"
|
||||
(inputChange)="handleInputChange('loading', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Required Input"
|
||||
placeholder="This field is required"
|
||||
variant="outlined"
|
||||
[required]="true"
|
||||
helperText="Required field"
|
||||
[(ngModel)]="textValues['required']"
|
||||
(inputChange)="handleInputChange('required', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Character Limit -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Character Limit</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
|
||||
<ui-text-input
|
||||
label="Tweet (280 chars)"
|
||||
placeholder="What's happening?"
|
||||
variant="outlined"
|
||||
[maxLength]="280"
|
||||
[showCharacterCount]="true"
|
||||
[(ngModel)]="textValues['tweet']"
|
||||
(inputChange)="handleInputChange('tweet', $event)"
|
||||
/>
|
||||
<ui-text-input
|
||||
label="Short Bio (100 chars)"
|
||||
placeholder="Tell us about yourself"
|
||||
variant="filled"
|
||||
[maxLength]="100"
|
||||
[showCharacterCount]="true"
|
||||
[(ngModel)]="textValues['bio']"
|
||||
(inputChange)="handleInputChange('bio', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Textarea Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Textarea Variants</h3>
|
||||
|
||||
<!-- Basic Textareas -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Basic Textareas</h4>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 1.5rem;">
|
||||
<ui-textarea
|
||||
label="Small Textarea"
|
||||
placeholder="Enter your message..."
|
||||
size="sm"
|
||||
variant="outlined"
|
||||
[rows]="3"
|
||||
[(ngModel)]="textValues['textarea-sm']"
|
||||
(textareaChange)="handleInputChange('textarea-sm', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Medium Textarea"
|
||||
placeholder="Enter your message..."
|
||||
size="md"
|
||||
variant="outlined"
|
||||
[rows]="4"
|
||||
[(ngModel)]="textValues['textarea-md']"
|
||||
(textareaChange)="handleInputChange('textarea-md', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Large Textarea"
|
||||
placeholder="Enter your message..."
|
||||
size="lg"
|
||||
variant="outlined"
|
||||
[rows]="5"
|
||||
[(ngModel)]="textValues['textarea-lg']"
|
||||
(textareaChange)="handleInputChange('textarea-lg', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Textarea Variants -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Textarea Variants</h4>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 1.5rem;">
|
||||
<ui-textarea
|
||||
label="Filled Textarea"
|
||||
placeholder="Enter your message..."
|
||||
variant="filled"
|
||||
[(ngModel)]="textValues['textarea-filled']"
|
||||
(textareaChange)="handleInputChange('textarea-filled', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Underlined Textarea"
|
||||
placeholder="Enter your message..."
|
||||
variant="underlined"
|
||||
[(ngModel)]="textValues['textarea-underlined']"
|
||||
(textareaChange)="handleInputChange('textarea-underlined', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Textarea with Features -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Textarea with Features</h4>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 1.5rem;">
|
||||
<ui-textarea
|
||||
label="Auto-resize Textarea"
|
||||
placeholder="Type to see auto-resize in action..."
|
||||
variant="outlined"
|
||||
[autoResize]="true"
|
||||
[rows]="2"
|
||||
[(ngModel)]="textValues['textarea-auto']"
|
||||
(textareaChange)="handleInputChange('textarea-auto', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Character Limited"
|
||||
placeholder="Maximum 500 characters..."
|
||||
variant="outlined"
|
||||
[maxLength]="500"
|
||||
[showCharacterCount]="true"
|
||||
[clearable]="true"
|
||||
[(ngModel)]="textValues['textarea-limited']"
|
||||
(textareaChange)="handleInputChange('textarea-limited', $event)"
|
||||
(clear)="handleClear('textarea-limited')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Textarea States -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Textarea States</h4>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 1.5rem;">
|
||||
<ui-textarea
|
||||
label="Error Textarea"
|
||||
placeholder="This has an error..."
|
||||
variant="outlined"
|
||||
state="error"
|
||||
errorMessage="Please provide valid feedback"
|
||||
[(ngModel)]="textValues['textarea-error']"
|
||||
(textareaChange)="handleInputChange('textarea-error', $event)"
|
||||
/>
|
||||
<ui-textarea
|
||||
label="Success Textarea"
|
||||
placeholder="This looks good..."
|
||||
variant="outlined"
|
||||
state="success"
|
||||
helperText="Thank you for your feedback!"
|
||||
[(ngModel)]="textValues['textarea-success']"
|
||||
(textareaChange)="handleInputChange('textarea-success', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Input Wrapper -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Input Wrapper (Dynamic)</h3>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="useTextarea()"
|
||||
(change)="toggleInputMode()"
|
||||
/>
|
||||
Switch to {{ useTextarea() ? 'Text Input' : 'Textarea' }}
|
||||
</label>
|
||||
</div>
|
||||
<div style="max-width: 500px;">
|
||||
<ui-input-wrapper
|
||||
[mode]="useTextarea() ? 'textarea' : 'input'"
|
||||
label="Dynamic Input"
|
||||
placeholder="This switches between input and textarea..."
|
||||
variant="outlined"
|
||||
[clearable]="true"
|
||||
[prefixIcon]="faEdit"
|
||||
helperText="Switch the checkbox above to change input type"
|
||||
[(ngModel)]="textValues['wrapper']"
|
||||
(inputChange)="handleInputChange('wrapper', $event)"
|
||||
(clear)="handleClear('wrapper')"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Usage Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Basic Text Input:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-text-input
|
||||
label="Email"
|
||||
placeholder="Enter your email"
|
||||
type="email"
|
||||
variant="outlined"
|
||||
size="md"
|
||||
[(ngModel)]="email"
|
||||
(inputChange)="handleEmailChange($event)">
|
||||
</ui-text-input></code></pre>
|
||||
|
||||
<h4>Text Input with Icons:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-text-input
|
||||
label="Search"
|
||||
placeholder="Search products..."
|
||||
type="search"
|
||||
variant="filled"
|
||||
[prefixIcon]="faSearch"
|
||||
[clearable]="true"
|
||||
[(ngModel)]="searchTerm"
|
||||
(inputChange)="handleSearch($event)">
|
||||
</ui-text-input></code></pre>
|
||||
|
||||
<h4>Textarea:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-textarea
|
||||
label="Message"
|
||||
placeholder="Enter your message..."
|
||||
variant="outlined"
|
||||
[rows]="4"
|
||||
[maxLength]="500"
|
||||
[showCharacterCount]="true"
|
||||
[(ngModel)]="message"
|
||||
(textareaChange)="handleMessageChange($event)">
|
||||
</ui-textarea></code></pre>
|
||||
|
||||
<h4>Input Wrapper:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-input-wrapper
|
||||
[mode]="inputMode"
|
||||
label="Dynamic Input"
|
||||
placeholder="Switches between input and textarea"
|
||||
variant="outlined"
|
||||
[prefixIcon]="faEdit"
|
||||
[(ngModel)]="content"
|
||||
(inputChange)="handleContentChange($event)">
|
||||
</ui-input-wrapper></code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Actions</h3>
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<button
|
||||
type="button"
|
||||
style="padding: 0.5rem 1rem; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;"
|
||||
(click)="toggleLoading()"
|
||||
>
|
||||
{{ isLoading() ? 'Stop Loading' : 'Start Loading' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style="padding: 0.5rem 1rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;"
|
||||
(click)="fillSampleData()"
|
||||
>
|
||||
Fill Sample Data
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style="padding: 0.5rem 1rem; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer;"
|
||||
(click)="clearAllInputs()"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (lastChangedInput()) {
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #e3f2fd; border-radius: 4px;">
|
||||
<strong>Last changed:</strong> {{ lastChangedInput() }} = "{{ lastChangedValue() }}"
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Values Display -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Current Values</h3>
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; max-height: 300px; overflow-y: auto;">
|
||||
<pre>{{ getValuesAsJson() }}</pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(287, 12%, 35%);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid hsl(289, 14%, 90%);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: hsl(286, 20%, 99%);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
button {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class InputDemoComponent {
|
||||
textValues: Record<string, string> = {};
|
||||
lastChangedInput = signal<string>('');
|
||||
lastChangedValue = signal<string>('');
|
||||
isLoading = signal<boolean>(false);
|
||||
useTextarea = signal<boolean>(false);
|
||||
|
||||
// FontAwesome icons
|
||||
readonly faSearch = faSearch;
|
||||
readonly faEnvelope = faEnvelope;
|
||||
readonly faEdit = faEdit;
|
||||
|
||||
handleInputChange(inputName: string, value: string): void {
|
||||
this.textValues[inputName] = value;
|
||||
this.lastChangedInput.set(inputName);
|
||||
this.lastChangedValue.set(value);
|
||||
console.log(`Input changed: ${inputName} = "${value}"`);
|
||||
}
|
||||
|
||||
handleClear(inputName: string): void {
|
||||
this.textValues[inputName] = '';
|
||||
this.lastChangedInput.set(inputName);
|
||||
this.lastChangedValue.set('(cleared)');
|
||||
console.log(`Input cleared: ${inputName}`);
|
||||
}
|
||||
|
||||
toggleLoading(): void {
|
||||
this.isLoading.set(!this.isLoading());
|
||||
console.log(`Loading state: ${this.isLoading() ? 'started' : 'stopped'}`);
|
||||
}
|
||||
|
||||
toggleInputMode(): void {
|
||||
this.useTextarea.set(!this.useTextarea());
|
||||
console.log(`Input mode: ${this.useTextarea() ? 'textarea' : 'input'}`);
|
||||
}
|
||||
|
||||
fillSampleData(): void {
|
||||
this.textValues = {
|
||||
'outlined-md': 'Sample outlined text',
|
||||
'email': 'user@example.com',
|
||||
'search': 'sample search query',
|
||||
'prefix-icon': 'search with icon',
|
||||
'textarea-md': 'This is a sample message\nin a textarea component\nwith multiple lines.',
|
||||
'wrapper': 'Dynamic wrapper content'
|
||||
};
|
||||
this.lastChangedInput.set('multiple');
|
||||
this.lastChangedValue.set('(sample data filled)');
|
||||
console.log('Sample data filled');
|
||||
}
|
||||
|
||||
clearAllInputs(): void {
|
||||
this.textValues = {};
|
||||
this.lastChangedInput.set('all');
|
||||
this.lastChangedValue.set('(cleared)');
|
||||
console.log('All inputs cleared');
|
||||
}
|
||||
|
||||
getValuesAsJson(): string {
|
||||
return JSON.stringify(this.textValues, null, 2);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,622 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ListItemComponent,
|
||||
ListContainerComponent,
|
||||
ListItemData
|
||||
} from '../../../../../ui-essentials/src/lib/components/data-display/list';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import {
|
||||
faInbox,
|
||||
faStar,
|
||||
faPaperPlane,
|
||||
faEdit,
|
||||
faArchive,
|
||||
faTrash,
|
||||
faPhone,
|
||||
faComment,
|
||||
faPlay,
|
||||
faDownload,
|
||||
faCheck,
|
||||
faChevronRight,
|
||||
faPlus,
|
||||
faMinus
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-list-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ListItemComponent,
|
||||
ListContainerComponent,
|
||||
FontAwesomeModule
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>List Component Showcase</h2>
|
||||
|
||||
<!-- Basic Lists with Size Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Basic Lists - Size Variants</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; margin-bottom: 2rem;">
|
||||
<div>
|
||||
<h4>Small (sm)</h4>
|
||||
<ui-list-container
|
||||
elevation="sm"
|
||||
spacing="xs"
|
||||
[rounded]="true"
|
||||
ariaLabel="Small navigation list">
|
||||
@for (item of navigationItems; track item.primary) {
|
||||
<ui-list-item
|
||||
[data]="item"
|
||||
size="sm"
|
||||
lines="one"
|
||||
variant="text"
|
||||
[divider]="true"
|
||||
>
|
||||
<fa-icon [icon]="getIconForItem(item.primary)" slot="trailing"></fa-icon>
|
||||
</ui-list-item>
|
||||
}
|
||||
</ui-list-container>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Medium (md)</h4>
|
||||
<ui-list-container
|
||||
elevation="sm"
|
||||
spacing="xs"
|
||||
[rounded]="true"
|
||||
ariaLabel="Medium navigation list">
|
||||
@for (item of navigationItems; track item.primary) {
|
||||
<ui-list-item
|
||||
[data]="item"
|
||||
size="md"
|
||||
lines="one"
|
||||
variant="text"
|
||||
[divider]="true"
|
||||
>
|
||||
<fa-icon [icon]="getIconForItem(item.primary)" slot="trailing"></fa-icon>
|
||||
</ui-list-item>
|
||||
}
|
||||
</ui-list-container>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Large (lg)</h4>
|
||||
<ui-list-container
|
||||
elevation="sm"
|
||||
spacing="xs"
|
||||
[rounded]="true"
|
||||
ariaLabel="Large navigation list">
|
||||
@for (item of navigationItems; track item.primary) {
|
||||
<ui-list-item
|
||||
[data]="item"
|
||||
size="lg"
|
||||
lines="one"
|
||||
variant="text"
|
||||
[divider]="true"
|
||||
>
|
||||
<fa-icon [icon]="getIconForItem(item.primary)" slot="trailing"></fa-icon>
|
||||
</ui-list-item>
|
||||
}
|
||||
</ui-list-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Multi-line Lists -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Multi-line Lists</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 2rem;">
|
||||
<div>
|
||||
<h4>Two Lines</h4>
|
||||
<ui-list-container
|
||||
elevation="md"
|
||||
spacing="sm"
|
||||
[rounded]="true"
|
||||
ariaLabel="Two line task list">
|
||||
@for (item of twoLineItems; track item.primary) {
|
||||
<ui-list-item
|
||||
[data]="item"
|
||||
size="md"
|
||||
lines="two"
|
||||
variant="text"
|
||||
>
|
||||
<button
|
||||
slot="trailing"
|
||||
style="background: none; border: none; padding: 8px; cursor: pointer; color: #6c757d;"
|
||||
(click)="handleAction('complete', item.primary)"
|
||||
[attr.aria-label]="'Complete ' + item.primary">
|
||||
<fa-icon [icon]="faCheck"></fa-icon>
|
||||
</button>
|
||||
</ui-list-item>
|
||||
}
|
||||
</ui-list-container>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Three Lines</h4>
|
||||
<ui-list-container
|
||||
elevation="md"
|
||||
spacing="sm"
|
||||
[rounded]="true"
|
||||
ariaLabel="Three line task list">
|
||||
@for (item of threeLineItems; track item.primary) {
|
||||
<ui-list-item
|
||||
[data]="item"
|
||||
size="lg"
|
||||
lines="three"
|
||||
variant="text"
|
||||
>
|
||||
<div slot="trailing" style="display: flex; gap: 0.5rem;">
|
||||
<button
|
||||
style="background: none; border: none; padding: 4px; cursor: pointer; color: #28a745;"
|
||||
(click)="handleAction('approve', item.primary)"
|
||||
[attr.aria-label]="'Approve ' + item.primary">
|
||||
<fa-icon [icon]="faCheck"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ui-list-item>
|
||||
}
|
||||
</ui-list-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Avatar Lists -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Lists with Avatars</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 2rem;">
|
||||
<div>
|
||||
<h4>Contacts - Avatar + One Line</h4>
|
||||
<ui-list-container
|
||||
elevation="sm"
|
||||
spacing="xs"
|
||||
[rounded]="true"
|
||||
ariaLabel="Contact list">
|
||||
@for (item of contactItems; track item.primary) {
|
||||
<ui-list-item
|
||||
[data]="item"
|
||||
size="md"
|
||||
lines="one"
|
||||
variant="avatar"
|
||||
>
|
||||
<div slot="trailing" style="display: flex; gap: 0.75rem;">
|
||||
<button
|
||||
style="background: none; border: none; padding: 4px; cursor: pointer; color: #007bff;"
|
||||
(click)="handleAction('call', item.primary)"
|
||||
[attr.aria-label]="'Call ' + item.primary">
|
||||
<fa-icon [icon]="faPhone"></fa-icon>
|
||||
</button>
|
||||
<button
|
||||
style="background: none; border: none; padding: 4px; cursor: pointer; color: #28a745;"
|
||||
(click)="handleAction('message', item.primary)"
|
||||
[attr.aria-label]="'Message ' + item.primary">
|
||||
<fa-icon [icon]="faComment"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ui-list-item>
|
||||
}
|
||||
</ui-list-container>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Messages - Avatar + Two Lines</h4>
|
||||
<ui-list-container
|
||||
elevation="sm"
|
||||
spacing="xs"
|
||||
[rounded]="true"
|
||||
ariaLabel="Message list">
|
||||
@for (item of messageItems; track item.primary) {
|
||||
<ui-list-item
|
||||
[data]="item"
|
||||
size="lg"
|
||||
lines="two"
|
||||
variant="avatar"
|
||||
>
|
||||
<span
|
||||
slot="trailing"
|
||||
style="font-size: 0.75rem; color: #6c757d;">
|
||||
2h
|
||||
</span>
|
||||
</ui-list-item>
|
||||
}
|
||||
</ui-list-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Media Lists -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Lists with Media</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); gap: 2rem;">
|
||||
<div>
|
||||
<h4>Music - Media + Two Lines</h4>
|
||||
<ui-list-container
|
||||
elevation="md"
|
||||
spacing="sm"
|
||||
[rounded]="true"
|
||||
ariaLabel="Music playlist">
|
||||
@for (item of musicItems; track item.primary) {
|
||||
<ui-list-item
|
||||
[data]="item"
|
||||
size="lg"
|
||||
lines="two"
|
||||
variant="media"
|
||||
>
|
||||
<div slot="trailing" style="display: flex; gap: 0.75rem; align-items: center;">
|
||||
<span style="font-size: 0.875rem; color: #6c757d;">3:42</span>
|
||||
<button
|
||||
style="background: none; border: none; padding: 8px; cursor: pointer; color: #007bff;"
|
||||
(click)="handleAction('play', item.primary)"
|
||||
[attr.aria-label]="'Play ' + item.primary">
|
||||
<fa-icon [icon]="faPlay"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ui-list-item>
|
||||
}
|
||||
</ui-list-container>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Videos - Media + Three Lines</h4>
|
||||
<ui-list-container
|
||||
elevation="md"
|
||||
spacing="sm"
|
||||
[rounded]="true"
|
||||
ariaLabel="Video library">
|
||||
@for (item of videoItems; track item.primary) {
|
||||
<ui-list-item
|
||||
[data]="item"
|
||||
size="lg"
|
||||
lines="three"
|
||||
variant="media"
|
||||
>
|
||||
<div slot="trailing" style="display: flex; flex-direction: column; align-items: center; gap: 0.5rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.25rem;">
|
||||
<fa-icon [icon]="faStar" style="color: #ffc107; font-size: 0.75rem;"></fa-icon>
|
||||
<span style="font-size: 0.75rem; color: #6c757d;">4.8</span>
|
||||
</div>
|
||||
<button
|
||||
style="background: none; border: none; padding: 4px; cursor: pointer; color: #007bff;"
|
||||
(click)="handleAction('download', item.primary)"
|
||||
[attr.aria-label]="'Download ' + item.primary">
|
||||
<fa-icon [icon]="faDownload"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ui-list-item>
|
||||
}
|
||||
</ui-list-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Demo</h3>
|
||||
<p style="margin-bottom: 1rem; color: #6c757d;">
|
||||
Click items to interact with them:
|
||||
</p>
|
||||
|
||||
<div style="max-width: 600px;">
|
||||
<ui-list-container
|
||||
elevation="sm"
|
||||
spacing="sm"
|
||||
[rounded]="true"
|
||||
ariaLabel="Interactive demo list">
|
||||
@for (item of interactiveItems(); track item.primary) {
|
||||
<ui-list-item
|
||||
[data]="item"
|
||||
size="md"
|
||||
lines="two"
|
||||
variant="avatar"
|
||||
style="cursor: pointer;"
|
||||
(click)="toggleInteractiveItem(item.primary)"
|
||||
>
|
||||
<div slot="trailing" style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
@if (item.selected) {
|
||||
<span style="background: #28a745; color: white; padding: 4px 8px; border-radius: 12px; font-size: 0.75rem;">
|
||||
Enabled
|
||||
</span>
|
||||
} @else if (item.disabled) {
|
||||
<span style="background: #dc3545; color: white; padding: 4px 8px; border-radius: 12px; font-size: 0.75rem;">
|
||||
Disabled
|
||||
</span>
|
||||
} @else {
|
||||
<span style="background: #6c757d; color: white; padding: 4px 8px; border-radius: 12px; font-size: 0.75rem;">
|
||||
Click to enable
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</ui-list-item>
|
||||
}
|
||||
</ui-list-container>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Code Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Usage Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Basic List with Icons:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto; font-size: 0.875rem;"><code><ui-list-container elevation="sm" spacing="xs" [rounded]="true">
|
||||
<ui-list-item
|
||||
[data]="{primary: 'Inbox'}"
|
||||
size="md"
|
||||
lines="one"
|
||||
variant="text">
|
||||
<fa-icon [icon]="faInbox" slot="trailing"></fa-icon>
|
||||
</ui-list-item>
|
||||
</ui-list-container></code></pre>
|
||||
|
||||
<h4>List with Avatar and Actions:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto; font-size: 0.875rem;"><code><ui-list-container elevation="md" spacing="sm" [rounded]="true">
|
||||
<ui-list-item
|
||||
[data]="{
|
||||
primary: 'John Doe',
|
||||
secondary: 'john@example.com',
|
||||
avatarSrc: 'avatar.jpg'
|
||||
}"
|
||||
size="lg"
|
||||
lines="two"
|
||||
variant="avatar">
|
||||
<button slot="trailing" (click)="call()">
|
||||
<fa-icon [icon]="faPhone"></fa-icon>
|
||||
</button>
|
||||
</ui-list-item>
|
||||
</ui-list-container></code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (lastAction()) {
|
||||
<div style="position: fixed; bottom: 20px; right: 20px; background: #007bff; color: white; padding: 1rem; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-width: 300px;">
|
||||
<strong>Action:</strong> {{ lastAction() }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(287, 12%, 35%);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid hsl(289, 14%, 90%);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
background: hsl(286, 20%, 99%);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0.5rem 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Courier New', monospace;
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
button {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class ListDemoComponent {
|
||||
// State signals
|
||||
lastAction = signal('');
|
||||
|
||||
// Demo data
|
||||
navigationItems: ListItemData[] = [
|
||||
{ primary: 'Inbox' },
|
||||
{ primary: 'Starred' },
|
||||
{ primary: 'Sent' },
|
||||
{ primary: 'Drafts' },
|
||||
{ primary: 'Archive' },
|
||||
{ primary: 'Trash' }
|
||||
];
|
||||
|
||||
twoLineItems: ListItemData[] = [
|
||||
{
|
||||
primary: 'Review Design System Updates',
|
||||
secondary: 'Check new component specifications and design tokens'
|
||||
},
|
||||
{
|
||||
primary: 'Team Standup Meeting',
|
||||
secondary: 'Daily sync with development and design teams'
|
||||
},
|
||||
{
|
||||
primary: 'Code Review - Authentication',
|
||||
secondary: 'Review pull request #247 for OAuth integration'
|
||||
}
|
||||
];
|
||||
|
||||
threeLineItems: ListItemData[] = [
|
||||
{
|
||||
primary: 'Angular 19 Migration',
|
||||
secondary: 'Upgrade project to latest Angular version with control flow',
|
||||
tertiary: 'Estimated completion: Next Sprint • Priority: High'
|
||||
},
|
||||
{
|
||||
primary: 'Design System Documentation',
|
||||
secondary: 'Create comprehensive documentation for all components',
|
||||
tertiary: 'Status: In Progress • Assigned: Design Team'
|
||||
}
|
||||
];
|
||||
|
||||
contactItems: ListItemData[] = [
|
||||
{
|
||||
primary: 'Sarah Wilson',
|
||||
avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=sarah',
|
||||
avatarAlt: 'Sarah Wilson avatar'
|
||||
},
|
||||
{
|
||||
primary: 'Alex Chen',
|
||||
avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=alex',
|
||||
avatarAlt: 'Alex Chen avatar'
|
||||
},
|
||||
{
|
||||
primary: 'Mike Rodriguez',
|
||||
avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=mike',
|
||||
avatarAlt: 'Mike Rodriguez avatar'
|
||||
}
|
||||
];
|
||||
|
||||
messageItems: ListItemData[] = [
|
||||
{
|
||||
primary: 'John Doe',
|
||||
secondary: 'Hey! How\'s the new component library coming along?',
|
||||
avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=john',
|
||||
avatarAlt: 'John Doe avatar'
|
||||
},
|
||||
{
|
||||
primary: 'Jane Smith',
|
||||
secondary: 'The design tokens look great. Ready for review when you are.',
|
||||
avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=jane',
|
||||
avatarAlt: 'Jane Smith avatar'
|
||||
}
|
||||
];
|
||||
|
||||
musicItems: ListItemData[] = [
|
||||
{
|
||||
primary: 'Bohemian Rhapsody',
|
||||
secondary: 'Queen • A Night at the Opera',
|
||||
mediaSrc: 'https://picsum.photos/80/80?random=1',
|
||||
mediaAlt: 'Album cover'
|
||||
},
|
||||
{
|
||||
primary: 'Hotel California',
|
||||
secondary: 'Eagles • Hotel California',
|
||||
mediaSrc: 'https://picsum.photos/80/80?random=2',
|
||||
mediaAlt: 'Album cover'
|
||||
}
|
||||
];
|
||||
|
||||
videoItems: ListItemData[] = [
|
||||
{
|
||||
primary: 'Angular Fundamentals',
|
||||
secondary: 'Complete guide to modern Angular development',
|
||||
tertiary: 'Duration: 2h 30m • Updated: Today • 1.2M views',
|
||||
mediaSrc: 'https://picsum.photos/80/80?random=5',
|
||||
mediaAlt: 'Video thumbnail'
|
||||
}
|
||||
];
|
||||
|
||||
interactiveItems = signal<ListItemData[]>([
|
||||
{
|
||||
primary: 'Email Notifications',
|
||||
secondary: 'Receive emails about important updates',
|
||||
avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=email',
|
||||
selected: false,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
primary: 'SMS Alerts',
|
||||
secondary: 'Get text messages for urgent notifications',
|
||||
avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=sms',
|
||||
selected: true,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
primary: 'Marketing Communications',
|
||||
secondary: 'Promotional offers and product updates',
|
||||
avatarSrc: 'https://api.dicebear.com/7.x/avataaars/svg?seed=marketing',
|
||||
selected: false,
|
||||
disabled: true
|
||||
}
|
||||
]);
|
||||
|
||||
// Font Awesome icons
|
||||
faInbox = faInbox;
|
||||
faStar = faStar;
|
||||
faPaperPlane = faPaperPlane;
|
||||
faEdit = faEdit;
|
||||
faArchive = faArchive;
|
||||
faTrash = faTrash;
|
||||
faPhone = faPhone;
|
||||
faComment = faComment;
|
||||
faPlay = faPlay;
|
||||
faDownload = faDownload;
|
||||
faCheck = faCheck;
|
||||
faChevronRight = faChevronRight;
|
||||
faPlus = faPlus;
|
||||
faMinus = faMinus;
|
||||
|
||||
// Helper methods
|
||||
getIconForItem(itemName: string): any {
|
||||
const iconMap: { [key: string]: any } = {
|
||||
'Inbox': faInbox,
|
||||
'Starred': faStar,
|
||||
'Sent': faPaperPlane,
|
||||
'Drafts': faEdit,
|
||||
'Archive': faArchive,
|
||||
'Trash': faTrash
|
||||
};
|
||||
return iconMap[itemName] || faInbox;
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
handleAction(action: string, item?: string): void {
|
||||
const message = item
|
||||
? `${action} action on "${item}" at ${new Date().toLocaleTimeString()}`
|
||||
: `${action} action at ${new Date().toLocaleTimeString()}`;
|
||||
|
||||
this.lastAction.set(message);
|
||||
console.log(`List action: ${action}`, item);
|
||||
|
||||
// Clear the message after 3 seconds
|
||||
setTimeout(() => this.lastAction.set(''), 3000);
|
||||
}
|
||||
|
||||
toggleInteractiveItem(itemName: string): void {
|
||||
this.interactiveItems.update(items =>
|
||||
items.map(item => {
|
||||
if (item.primary === itemName && !item.disabled) {
|
||||
return { ...item, selected: !item.selected };
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
|
||||
this.handleAction('toggle', itemName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
.demo-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h2 {
|
||||
font-size: 1.875rem;
|
||||
color: #1f2937;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
color: #1f2937;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.25rem;
|
||||
color: #1f2937;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.125rem;
|
||||
color: #1f2937;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.375rem;
|
||||
background: #ffffff;
|
||||
min-width: 120px;
|
||||
|
||||
p {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-interactive {
|
||||
padding: 1rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
background: #f9fafb;
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
color: #1f2937;
|
||||
font-weight: 500;
|
||||
|
||||
select {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.25rem;
|
||||
background: #ffffff;
|
||||
color: #1f2937;
|
||||
font-size: 0.875rem;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-result {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 120px;
|
||||
background: #ffffff;
|
||||
border: 1px dashed #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-usage {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.usage-example {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
background: #ffffff;
|
||||
|
||||
.demo-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: #6366f1;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #5855eb;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-loading-example,
|
||||
.processing-example {
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.25rem;
|
||||
background: #f9fafb;
|
||||
|
||||
.page-content,
|
||||
.process-result {
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #64748b;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: background 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive breakpoints
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
min-width: 100px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.demo-interactive .demo-controls {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
label {
|
||||
width: 100%;
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-usage {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { LoadingSpinnerComponent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-loading-spinner-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, LoadingSpinnerComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Loading Spinner Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-item">
|
||||
<ui-loading-spinner [size]="size"></ui-loading-spinner>
|
||||
<p>{{ size }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Type Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Types</h3>
|
||||
<div class="demo-row">
|
||||
@for (type of types; track type) {
|
||||
<div class="demo-item">
|
||||
<ui-loading-spinner [type]="type"></ui-loading-spinner>
|
||||
<p>{{ type }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Color Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Colors</h3>
|
||||
<div class="demo-row">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-item">
|
||||
<ui-loading-spinner [variant]="variant"></ui-loading-spinner>
|
||||
<p>{{ variant }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Speed Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Speeds</h3>
|
||||
<div class="demo-row">
|
||||
@for (speed of speeds; track speed) {
|
||||
<div class="demo-item">
|
||||
<ui-loading-spinner [speed]="speed" type="spin"></ui-loading-spinner>
|
||||
<p>{{ speed }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- With Labels -->
|
||||
<section class="demo-section">
|
||||
<h3>With Labels</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<ui-loading-spinner
|
||||
label="Loading data"
|
||||
[showLabel]="true"
|
||||
type="spin">
|
||||
</ui-loading-spinner>
|
||||
<p>With Label</p>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<ui-loading-spinner
|
||||
label="Processing"
|
||||
[showLabel]="true"
|
||||
type="dots"
|
||||
variant="success">
|
||||
</ui-loading-spinner>
|
||||
<p>Processing</p>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<ui-loading-spinner
|
||||
label="Uploading"
|
||||
[showLabel]="true"
|
||||
type="bars"
|
||||
variant="info">
|
||||
</ui-loading-spinner>
|
||||
<p>Uploading</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<ui-loading-spinner></ui-loading-spinner>
|
||||
<p>Normal</p>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<ui-loading-spinner [disabled]="true"></ui-loading-spinner>
|
||||
<p>Disabled</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive</h3>
|
||||
<div class="demo-interactive">
|
||||
<div class="demo-controls">
|
||||
<label>
|
||||
Size:
|
||||
<select [(ngModel)]="selectedSize">
|
||||
@for (size of sizes; track size) {
|
||||
<option [value]="size">{{ size }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Type:
|
||||
<select [(ngModel)]="selectedType">
|
||||
@for (type of types; track type) {
|
||||
<option [value]="type">{{ type }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Variant:
|
||||
<select [(ngModel)]="selectedVariant">
|
||||
@for (variant of variants; track variant) {
|
||||
<option [value]="variant">{{ variant }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Speed:
|
||||
<select [(ngModel)]="selectedSpeed">
|
||||
@for (speed of speeds; track speed) {
|
||||
<option [value]="speed">{{ speed }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="showInteractiveLabel">
|
||||
Show Label
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="isDisabled">
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="demo-result">
|
||||
<ui-loading-spinner
|
||||
[size]="selectedSize"
|
||||
[type]="selectedType"
|
||||
[variant]="selectedVariant"
|
||||
[speed]="selectedSpeed"
|
||||
[showLabel]="showInteractiveLabel"
|
||||
[disabled]="isDisabled"
|
||||
[label]="interactiveLabel"
|
||||
(clicked)="handleSpinnerClick($event)">
|
||||
</ui-loading-spinner>
|
||||
</div>
|
||||
|
||||
@if (clickCount > 0) {
|
||||
<p>Spinner clicked: {{ clickCount }} times</p>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Common Usage Examples</h3>
|
||||
<div class="demo-usage">
|
||||
<div class="usage-example">
|
||||
<h4>Loading Button</h4>
|
||||
<button class="demo-button" [disabled]="isLoading" (click)="simulateLoading()">
|
||||
@if (isLoading) {
|
||||
<ui-loading-spinner size="sm" variant="secondary" type="spin"></ui-loading-spinner>
|
||||
<span>Loading...</span>
|
||||
} @else {
|
||||
<span>Click to Load</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="usage-example">
|
||||
<h4>Page Loading</h4>
|
||||
<div class="page-loading-example">
|
||||
@if (pageLoading) {
|
||||
<ui-loading-spinner
|
||||
size="lg"
|
||||
variant="primary"
|
||||
type="pulse"
|
||||
label="Loading page content"
|
||||
[showLabel]="true">
|
||||
</ui-loading-spinner>
|
||||
} @else {
|
||||
<div class="page-content">
|
||||
<h5>Page Content</h5>
|
||||
<p>This is the loaded content.</p>
|
||||
<button (click)="simulatePageLoad()">Reload Page</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="usage-example">
|
||||
<h4>Data Processing</h4>
|
||||
<div class="processing-example">
|
||||
@if (processing) {
|
||||
<ui-loading-spinner
|
||||
type="bars"
|
||||
variant="success"
|
||||
label="Processing data"
|
||||
[showLabel]="true">
|
||||
</ui-loading-spinner>
|
||||
} @else {
|
||||
<div class="process-result">
|
||||
<p>✅ Data processed successfully!</p>
|
||||
<button (click)="simulateProcessing()">Process Data</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './loading-spinner-demo.component.scss'
|
||||
})
|
||||
export class LoadingSpinnerDemoComponent {
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
types = ['spin', 'dots', 'pulse', 'bars'] as const;
|
||||
variants = ['primary', 'secondary', 'accent', 'success', 'warning', 'danger', 'info'] as const;
|
||||
speeds = ['slow', 'normal', 'fast'] as const;
|
||||
|
||||
// Interactive demo state
|
||||
selectedSize = 'md' as const;
|
||||
selectedType = 'spin' as const;
|
||||
selectedVariant = 'primary' as const;
|
||||
selectedSpeed = 'normal' as const;
|
||||
showInteractiveLabel = false;
|
||||
isDisabled = false;
|
||||
interactiveLabel = 'Loading';
|
||||
clickCount = 0;
|
||||
|
||||
// Usage examples state
|
||||
isLoading = false;
|
||||
pageLoading = false;
|
||||
processing = false;
|
||||
|
||||
handleSpinnerClick(event: MouseEvent): void {
|
||||
if (!this.isDisabled) {
|
||||
this.clickCount++;
|
||||
console.log('Spinner clicked', event);
|
||||
}
|
||||
}
|
||||
|
||||
simulateLoading(): void {
|
||||
this.isLoading = true;
|
||||
setTimeout(() => {
|
||||
this.isLoading = false;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
simulatePageLoad(): void {
|
||||
this.pageLoading = true;
|
||||
setTimeout(() => {
|
||||
this.pageLoading = false;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
simulateProcessing(): void {
|
||||
this.processing = true;
|
||||
setTimeout(() => {
|
||||
this.processing = false;
|
||||
}, 2500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
@use "../../../../../shared-ui/src/styles/tokens" as *;
|
||||
|
||||
// Tokens available globally via main application styles
|
||||
|
||||
// ==========================================================================
|
||||
// MENU DEMO COMPONENT
|
||||
// ==========================================================================
|
||||
// Demonstration component showcasing all menu variants and capabilities
|
||||
// ==========================================================================
|
||||
|
||||
.menu-demo {
|
||||
padding: $semantic-spacing-component-xl;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-layout-lg;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid $semantic-color-interactive-primary;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-xl;
|
||||
border: 1px solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
padding: $semantic-spacing-component-xl;
|
||||
background: $semantic-color-surface-elevated;
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-xl;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
border-bottom: 2px solid $semantic-color-border-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: $semantic-spacing-component-xl;
|
||||
margin-bottom: $semantic-spacing-layout-md;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
text-align: center;
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
background-color: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-single {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
|
||||
h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
text-align: center;
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
background-color: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// USAGE EXAMPLES SECTION
|
||||
// ==========================================================================
|
||||
|
||||
.usage-examples {
|
||||
background: $semantic-color-surface-secondary;
|
||||
padding: $semantic-spacing-component-xl;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border-left: 4px solid $semantic-color-interactive-primary;
|
||||
|
||||
h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
margin-top: $semantic-spacing-component-lg;
|
||||
text-align: left;
|
||||
padding: 0;
|
||||
background: none;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
background: $semantic-color-surface-primary;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
overflow-x: auto;
|
||||
margin: $semantic-spacing-component-sm 0 $semantic-spacing-component-lg 0;
|
||||
border: 1px solid $semantic-color-border-secondary;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// INTERACTIVE DEMO SECTION
|
||||
// ==========================================================================
|
||||
|
||||
.interactive-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-feedback {
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-container-primary;
|
||||
color: $semantic-color-on-container-primary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: 1px solid $semantic-color-border-secondary;
|
||||
text-align: center;
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// RESPONSIVE DESIGN
|
||||
// ==========================================================================
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.menu-demo {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.usage-examples {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
|
||||
pre {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.menu-demo {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// ACCESSIBILITY ENHANCEMENTS
|
||||
// ==========================================================================
|
||||
|
||||
// Focus indicators for better keyboard navigation
|
||||
.menu-demo {
|
||||
.demo-section:focus-within {
|
||||
outline: 2px solid $semantic-color-border-focus;
|
||||
outline-offset: 4px;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
}
|
||||
}
|
||||
|
||||
// High contrast mode support
|
||||
@media (prefers-contrast: high) {
|
||||
.demo-section {
|
||||
border: 2px solid $semantic-color-border-primary;
|
||||
}
|
||||
|
||||
.demo-feedback {
|
||||
border: 2px solid $semantic-color-border-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Reduced motion support
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.menu-demo * {
|
||||
transition: none;
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,697 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
faUser, faHome, faCog, faEnvelope, faDownload, faShare, faFileExport,
|
||||
faChartBar, faChartLine, faFileAlt, faShieldAlt, faDatabase, faTrash,
|
||||
faStar, faBolt, faInfoCircle, faExclamationTriangle, faFolder, faCirclePlus,
|
||||
faFolderOpen, faEdit, faSave, faTachometerAlt, faProjectDiagram, faPlayCircle,
|
||||
faCheckCircle, faArchive, faCalendarWeek, faCalendarAlt, faCalendar, faSlidersH,
|
||||
faPalette, faBell, faQuestionCircle, faSignOutAlt, faBox, faConciergeBell,
|
||||
faCircle, faRefresh, faToggleOn
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { faAngular, faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
import { MenuItemComponent, MenuContainerComponent, MenuSubmenuComponent, MenuItemData } from '../../../../../ui-essentials/src/lib/components/navigation/menu';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-menu-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MenuItemComponent,
|
||||
MenuContainerComponent,
|
||||
MenuSubmenuComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="menu-demo">
|
||||
<h2>Menu Component Showcase</h2>
|
||||
|
||||
<!-- Basic Menu Items -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Menu Items</h3>
|
||||
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>Text Only - Different Sizes</h4>
|
||||
<ui-menu-container elevation="sm" spacing="sm" [rounded]="true">
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Small Menu Item' }"
|
||||
size="sm"
|
||||
(itemClick)="handleMenuClick('small item', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Medium Menu Item' }"
|
||||
size="md"
|
||||
(itemClick)="handleMenuClick('medium item', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Large Menu Item' }"
|
||||
size="lg"
|
||||
(itemClick)="handleMenuClick('large item', $event)"
|
||||
/>
|
||||
</ui-menu-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Menu States</h4>
|
||||
<ui-menu-container elevation="sm" spacing="sm" [rounded]="true">
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Normal Item', active: false }"
|
||||
(itemClick)="handleMenuClick('normal', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Active Item', active: true }"
|
||||
(itemClick)="handleMenuClick('active', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Disabled Item', disabled: true }"
|
||||
(itemClick)="handleMenuClick('disabled', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'With Badge', badge: '3' }"
|
||||
(itemClick)="handleMenuClick('badge', $event)"
|
||||
/>
|
||||
</ui-menu-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Icons and Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Icons and Variants</h3>
|
||||
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>Left Icons</h4>
|
||||
<ui-menu-container elevation="sm" spacing="sm" [rounded]="true">
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Home', icon: faUser }"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('home', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Settings', icon: faCog }"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('settings', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Profile', icon: faUser }"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('profile', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Messages', icon: faEnvelope, badge: '12' }"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('messages', $event)"
|
||||
/>
|
||||
</ui-menu-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Right Icons</h4>
|
||||
<ui-menu-container elevation="sm" spacing="sm" [rounded]="true">
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Download', icon: faDownload }"
|
||||
iconPosition="right"
|
||||
(itemClick)="handleMenuClick('download', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Share', icon: faShare }"
|
||||
iconPosition="right"
|
||||
(itemClick)="handleMenuClick('share', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Export', icon: faFileExport }"
|
||||
iconPosition="right"
|
||||
(itemClick)="handleMenuClick('export', $event)"
|
||||
/>
|
||||
</ui-menu-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Circular Icons -->
|
||||
<section class="demo-section">
|
||||
<h3>Circular Icons</h3>
|
||||
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>Circle Icons - Left</h4>
|
||||
<ui-menu-container elevation="md" spacing="md" [rounded]="true">
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Dashboard', icon: faChartBar, iconCircle: true }"
|
||||
iconPosition="left"
|
||||
variant="primary"
|
||||
(itemClick)="handleMenuClick('dashboard', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Analytics', icon: faChartLine, iconCircle: true }"
|
||||
iconPosition="left"
|
||||
variant="secondary"
|
||||
(itemClick)="handleMenuClick('analytics', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Reports', icon: faFileAlt, iconCircle: true }"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('reports', $event)"
|
||||
/>
|
||||
</ui-menu-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Circle Icons - Right</h4>
|
||||
<ui-menu-container elevation="md" spacing="md" [rounded]="true">
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Security', icon: faShieldAlt, iconCircle: true }"
|
||||
iconPosition="right"
|
||||
variant="primary"
|
||||
(itemClick)="handleMenuClick('security', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Backup', icon: faDatabase, iconCircle: true }"
|
||||
iconPosition="right"
|
||||
variant="secondary"
|
||||
(itemClick)="handleMenuClick('backup', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Delete', icon: faTrash, iconCircle: true }"
|
||||
iconPosition="right"
|
||||
variant="danger"
|
||||
(itemClick)="handleMenuClick('delete', $event)"
|
||||
/>
|
||||
</ui-menu-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Menu Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Menu Variants</h3>
|
||||
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>Color Variants</h4>
|
||||
<ui-menu-container elevation="sm" spacing="sm" [rounded]="true">
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Default Action', icon: faStar }"
|
||||
variant="default"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('default action', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Primary Action', icon: faBolt }"
|
||||
variant="primary"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('primary action', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Secondary Action', icon: faInfoCircle }"
|
||||
variant="secondary"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('secondary action', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Danger Action', icon: faExclamationTriangle }"
|
||||
variant="danger"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('danger action', $event)"
|
||||
/>
|
||||
</ui-menu-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>With Dividers</h4>
|
||||
<ui-menu-container elevation="sm" spacing="sm" [rounded]="true">
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'File Operations', icon: faFolder }"
|
||||
iconPosition="left"
|
||||
[showDivider]="true"
|
||||
(itemClick)="handleMenuClick('file ops', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'New File', icon: faCirclePlus }"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('new file', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Open File', icon: faFolderOpen }"
|
||||
iconPosition="left"
|
||||
[showDivider]="true"
|
||||
(itemClick)="handleMenuClick('open file', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Edit Operations', icon: faEdit }"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('edit ops', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Save', icon: faSave }"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('save', $event)"
|
||||
/>
|
||||
</ui-menu-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Submenu Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Collapsible Submenus</h3>
|
||||
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>Navigation Menu</h4>
|
||||
<ui-menu-container elevation="md" spacing="sm" [rounded]="true">
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Dashboard', icon: faTachometerAlt }"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('dashboard', $event)"
|
||||
/>
|
||||
|
||||
<ui-menu-submenu
|
||||
[parentItem]="{ label: 'Projects', icon: faProjectDiagram, hasSubmenu: true }"
|
||||
iconPosition="left"
|
||||
[isOpen]="submenuStates['projects']"
|
||||
(submenuToggled)="toggleSubmenu('projects', $event)"
|
||||
(parentItemClick)="handleMenuClick('projects parent', $event)"
|
||||
>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Active Projects', icon: faPlayCircle }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
(itemClick)="handleMenuClick('active projects', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Completed Projects', icon: faCheckCircle }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
(itemClick)="handleMenuClick('completed projects', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Archived Projects', icon: faArchive }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
(itemClick)="handleMenuClick('archived projects', $event)"
|
||||
/>
|
||||
</ui-menu-submenu>
|
||||
|
||||
<ui-menu-submenu
|
||||
[parentItem]="{ label: 'Reports', icon: faChartBar, hasSubmenu: true }"
|
||||
iconPosition="left"
|
||||
[isOpen]="submenuStates['reports']"
|
||||
(submenuToggled)="toggleSubmenu('reports', $event)"
|
||||
(parentItemClick)="handleMenuClick('reports parent', $event)"
|
||||
>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Weekly Report', icon: faCalendarWeek }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
(itemClick)="handleMenuClick('weekly report', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Monthly Report', icon: faCalendarAlt }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
(itemClick)="handleMenuClick('monthly report', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Annual Report', icon: faCalendar }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
(itemClick)="handleMenuClick('annual report', $event)"
|
||||
/>
|
||||
</ui-menu-submenu>
|
||||
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Settings', icon: faCog }"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('settings', $event)"
|
||||
/>
|
||||
</ui-menu-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>User Menu</h4>
|
||||
<ui-menu-container elevation="md" spacing="sm" [rounded]="true">
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'My Profile', icon: faUser }"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('profile', $event)"
|
||||
/>
|
||||
|
||||
<ui-menu-submenu
|
||||
[parentItem]="{ label: 'Preferences', icon: faSlidersH, hasSubmenu: true }"
|
||||
iconPosition="left"
|
||||
[isOpen]="submenuStates['preferences']"
|
||||
(submenuToggled)="toggleSubmenu('preferences', $event)"
|
||||
(parentItemClick)="handleMenuClick('preferences parent', $event)"
|
||||
>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Theme Settings', icon: faPalette }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
(itemClick)="handleMenuClick('theme', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Notifications', icon: faBell }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
[showDivider]="true"
|
||||
(itemClick)="handleMenuClick('notifications', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Privacy', icon: faShieldAlt }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
(itemClick)="handleMenuClick('privacy', $event)"
|
||||
/>
|
||||
</ui-menu-submenu>
|
||||
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Help & Support', icon: faQuestionCircle }"
|
||||
iconPosition="left"
|
||||
[showDivider]="true"
|
||||
(itemClick)="handleMenuClick('help', $event)"
|
||||
/>
|
||||
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Sign Out', icon: faSignOutAlt }"
|
||||
iconPosition="left"
|
||||
variant="danger"
|
||||
(itemClick)="handleMenuClick('sign out', $event)"
|
||||
/>
|
||||
</ui-menu-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Horizontal Menu -->
|
||||
<section class="demo-section">
|
||||
<h3>Horizontal Menu</h3>
|
||||
|
||||
<div class="demo-single">
|
||||
<h4>Navigation Bar Style</h4>
|
||||
<ui-menu-container
|
||||
elevation="sm"
|
||||
spacing="md"
|
||||
orientation="horizontal"
|
||||
[rounded]="true"
|
||||
>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Home', icon: faHome }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
(itemClick)="handleMenuClick('home nav', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Products', icon: faBox }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
(itemClick)="handleMenuClick('products nav', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Services', icon: faConciergeBell }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
(itemClick)="handleMenuClick('services nav', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'About', icon: faInfoCircle }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
(itemClick)="handleMenuClick('about nav', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Contact', icon: faEnvelope }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
(itemClick)="handleMenuClick('contact nav', $event)"
|
||||
/>
|
||||
</ui-menu-container>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Dense and Scrollable -->
|
||||
<section class="demo-section">
|
||||
<h3>Layout Options</h3>
|
||||
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>Dense Menu</h4>
|
||||
<ui-menu-container elevation="sm" spacing="xs" [rounded]="true" [dense]="true">
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Compact Item 1', icon: faCircle }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
[dense]="true"
|
||||
(itemClick)="handleMenuClick('compact 1', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Compact Item 2', icon: faCircle }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
[dense]="true"
|
||||
(itemClick)="handleMenuClick('compact 2', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Compact Item 3', icon: faCircle }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
[dense]="true"
|
||||
(itemClick)="handleMenuClick('compact 3', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Compact Item 4', icon: faCircle }"
|
||||
iconPosition="left"
|
||||
size="sm"
|
||||
[dense]="true"
|
||||
(itemClick)="handleMenuClick('compact 4', $event)"
|
||||
/>
|
||||
</ui-menu-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Scrollable Menu</h4>
|
||||
<ui-menu-container
|
||||
elevation="md"
|
||||
spacing="sm"
|
||||
[rounded]="true"
|
||||
[scrollable]="true"
|
||||
maxHeight="200px"
|
||||
>
|
||||
@for (item of scrollableItems; track item.id) {
|
||||
<ui-menu-item
|
||||
[data]="item"
|
||||
iconPosition="left"
|
||||
size="md"
|
||||
(itemClick)="handleMenuClick('scrollable ' + item.id, item)"
|
||||
/>
|
||||
}
|
||||
</ui-menu-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Menu with Links -->
|
||||
<section class="demo-section">
|
||||
<h3>Menu with Links</h3>
|
||||
|
||||
<div class="demo-single">
|
||||
<h4>External Links</h4>
|
||||
<ui-menu-container elevation="sm" spacing="sm" [rounded]="true">
|
||||
<ui-menu-item
|
||||
[data]="{
|
||||
label: 'Angular Documentation',
|
||||
icon: faAngular,
|
||||
href: 'https://angular.io',
|
||||
target: '_blank'
|
||||
}"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('angular docs', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{
|
||||
label: 'GitHub Repository',
|
||||
icon: faGithub,
|
||||
href: 'https://github.com',
|
||||
target: '_blank'
|
||||
}"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('github', $event)"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{
|
||||
label: 'Material Design',
|
||||
icon: faPalette,
|
||||
href: 'https://material.io',
|
||||
target: '_blank'
|
||||
}"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick('material', $event)"
|
||||
/>
|
||||
</ui-menu-container>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Usage Examples</h3>
|
||||
<div class="usage-examples">
|
||||
<h4>Basic Menu Item:</h4>
|
||||
<pre><code>{{ '<' }}ui-menu-item
|
||||
[data]="{{ '{' }} label: 'Menu Item', icon: faHome {{ '}' }}"
|
||||
iconPosition="left"
|
||||
variant="primary"
|
||||
(itemClick)="handleClick($event)"{{ '>' }}
|
||||
{{ '<' }}/ui-menu-item{{ '>' }}</code></pre>
|
||||
|
||||
<h4>Menu with Submenu:</h4>
|
||||
<pre><code>{{ '<' }}ui-menu-submenu
|
||||
[parentItem]="{{ '{' }} label: 'Parent', hasSubmenu: true {{ '}' }}"
|
||||
[isOpen]="submenuOpen"
|
||||
(submenuToggled)="toggleSubmenu($event)"{{ '>' }}
|
||||
|
||||
{{ '<' }}ui-menu-item [data]="childItem1"{{ '>' }}{{ '<' }}/ui-menu-item{{ '>' }}
|
||||
{{ '<' }}ui-menu-item [data]="childItem2"{{ '>' }}{{ '<' }}/ui-menu-item{{ '>' }}
|
||||
{{ '<' }}/ui-menu-submenu{{ '>' }}</code></pre>
|
||||
|
||||
<h4>Menu Container:</h4>
|
||||
<pre><code>{{ '<' }}ui-menu-container
|
||||
elevation="md"
|
||||
spacing="sm"
|
||||
[rounded]="true"
|
||||
[scrollable]="true"
|
||||
maxHeight="300px"{{ '>' }}
|
||||
|
||||
{{ '<' }}!-- Menu items here --{{ '>' }}
|
||||
{{ '<' }}/ui-menu-container{{ '>' }}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Actions</h3>
|
||||
<div class="interactive-demo">
|
||||
<ui-menu-container elevation="sm" spacing="sm" [rounded]="true">
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Show Alert', icon: faBell }"
|
||||
iconPosition="left"
|
||||
variant="primary"
|
||||
(itemClick)="showAlert('Menu Alert!')"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Toggle Demo State', icon: faToggleOn }"
|
||||
iconPosition="left"
|
||||
(itemClick)="toggleDemoState()"
|
||||
/>
|
||||
<ui-menu-item
|
||||
[data]="{ label: 'Reset Demo', icon: faRefresh }"
|
||||
iconPosition="left"
|
||||
variant="secondary"
|
||||
(itemClick)="resetDemo()"
|
||||
/>
|
||||
</ui-menu-container>
|
||||
|
||||
@if (lastClickedItem) {
|
||||
<div class="demo-feedback">
|
||||
<strong>Last clicked:</strong> {{ lastClickedItem }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./menu-demo.component.scss']
|
||||
})
|
||||
export class MenuDemoComponent {
|
||||
// FontAwesome icons
|
||||
faUser = faUser;
|
||||
faHome = faHome;
|
||||
faCog = faCog;
|
||||
faEnvelope = faEnvelope;
|
||||
faDownload = faDownload;
|
||||
faShare = faShare;
|
||||
faFileExport = faFileExport;
|
||||
faChartBar = faChartBar;
|
||||
faChartLine = faChartLine;
|
||||
faFileAlt = faFileAlt;
|
||||
faShieldAlt = faShieldAlt;
|
||||
faDatabase = faDatabase;
|
||||
faTrash = faTrash;
|
||||
faStar = faStar;
|
||||
faBolt = faBolt;
|
||||
faInfoCircle = faInfoCircle;
|
||||
faExclamationTriangle = faExclamationTriangle;
|
||||
faFolder = faFolder;
|
||||
faCirclePlus = faCirclePlus;
|
||||
faFolderOpen = faFolderOpen;
|
||||
faEdit = faEdit;
|
||||
faSave = faSave;
|
||||
faTachometerAlt = faTachometerAlt;
|
||||
faProjectDiagram = faProjectDiagram;
|
||||
faPlayCircle = faPlayCircle;
|
||||
faCheckCircle = faCheckCircle;
|
||||
faArchive = faArchive;
|
||||
faCalendarWeek = faCalendarWeek;
|
||||
faCalendarAlt = faCalendarAlt;
|
||||
faCalendar = faCalendar;
|
||||
faSlidersH = faSlidersH;
|
||||
faPalette = faPalette;
|
||||
faBell = faBell;
|
||||
faQuestionCircle = faQuestionCircle;
|
||||
faSignOutAlt = faSignOutAlt;
|
||||
faBox = faBox;
|
||||
faConciergeBell = faConciergeBell;
|
||||
faCircle = faCircle;
|
||||
faRefresh = faRefresh;
|
||||
faToggleOn = faToggleOn;
|
||||
faAngular = faAngular;
|
||||
faGithub = faGithub;
|
||||
|
||||
lastClickedItem: string = '';
|
||||
submenuStates: Record<string, boolean> = {
|
||||
projects: false,
|
||||
reports: false,
|
||||
preferences: false
|
||||
};
|
||||
|
||||
scrollableItems: MenuItemData[] = Array.from({ length: 15 }, (_, i) => ({
|
||||
id: `item-${i + 1}`,
|
||||
label: `Scrollable Item ${i + 1}`,
|
||||
icon: this.faCircle
|
||||
}));
|
||||
|
||||
handleMenuClick(action: string, item: MenuItemData): void {
|
||||
this.lastClickedItem = `${action} (${item.label})`;
|
||||
console.log(`Menu clicked: ${action}`, item);
|
||||
}
|
||||
|
||||
toggleSubmenu(key: string, event: { item: MenuItemData; isOpen: boolean }): void {
|
||||
this.submenuStates[key] = event.isOpen;
|
||||
console.log(`Submenu ${key} ${event.isOpen ? 'opened' : 'closed'}`);
|
||||
}
|
||||
|
||||
showAlert(message: string): void {
|
||||
alert(message);
|
||||
this.lastClickedItem = 'Alert shown';
|
||||
}
|
||||
|
||||
toggleDemoState(): void {
|
||||
// Toggle all submenus
|
||||
Object.keys(this.submenuStates).forEach(key => {
|
||||
this.submenuStates[key] = !this.submenuStates[key];
|
||||
});
|
||||
this.lastClickedItem = 'Demo state toggled';
|
||||
}
|
||||
|
||||
resetDemo(): void {
|
||||
this.lastClickedItem = '';
|
||||
this.submenuStates = {
|
||||
projects: false,
|
||||
reports: false,
|
||||
preferences: false
|
||||
};
|
||||
console.log('Demo reset');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-xl;
|
||||
|
||||
h2 {
|
||||
color: $semantic-color-on-surface;
|
||||
font-size: $semantic-typography-heading-h2-size;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
border-bottom: 1px solid $semantic-color-outline;
|
||||
padding-bottom: $semantic-spacing-content-line-normal;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: $semantic-color-on-surface;
|
||||
font-size: $semantic-typography-heading-h4-size;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
align-items: flex-start;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.config-panel {
|
||||
background: $semantic-color-surface-variant;
|
||||
border: 1px solid $semantic-color-outline;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
margin-top: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.config-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-md;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
margin-top: $semantic-spacing-component-lg;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
color: $semantic-color-on-surface;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
white-space: nowrap;
|
||||
|
||||
&:first-child {
|
||||
min-width: 80px;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
border: 1px solid $semantic-color-outline;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-on-surface;
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
min-width: 120px;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $semantic-color-primary;
|
||||
outline-offset: 2px;
|
||||
border-color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
accent-color: $semantic-color-primary;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
label:first-child {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.event-log {
|
||||
background: $semantic-color-surface-primary;
|
||||
border: 1px solid $semantic-color-outline;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
}
|
||||
|
||||
.event-item {
|
||||
padding: $semantic-spacing-component-xs 0;
|
||||
border-bottom: 1px solid $semantic-color-outline-variant;
|
||||
color: $semantic-color-on-surface-variant;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
color: $semantic-color-on-surface;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { faExclamationTriangle, faCheckCircle, faInfoCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { ButtonComponent, ModalComponent } from "../../../../../ui-essentials/src/public-api";
|
||||
|
||||
@Component({
|
||||
selector: 'ui-modal-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, FontAwesomeModule, ButtonComponent, ModalComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Modal Demo</h2>
|
||||
|
||||
<!-- Basic Usage -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Usage</h3>
|
||||
<div class="demo-row">
|
||||
<ui-button (clicked)="basicModal.set(true)">
|
||||
Open Basic Modal
|
||||
</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
<ui-button (clicked)="smallModal.set(true)">Small Modal</ui-button>
|
||||
<ui-button (clicked)="mediumModal.set(true)">Medium Modal</ui-button>
|
||||
<ui-button (clicked)="largeModal.set(true)">Large Modal</ui-button>
|
||||
<ui-button (clicked)="xlModal.set(true)">XL Modal</ui-button>
|
||||
<ui-button (clicked)="fullscreenModal.set(true)">Fullscreen Modal</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Types -->
|
||||
<section class="demo-section">
|
||||
<h3>Variants</h3>
|
||||
<div class="demo-row">
|
||||
<ui-button (clicked)="defaultModal.set(true)" variant="filled">Default</ui-button>
|
||||
<ui-button (clicked)="warningModal.set(true)" variant="tonal">Warning</ui-button>
|
||||
<ui-button (clicked)="dangerModal.set(true)" variant="outlined">Danger</ui-button>
|
||||
<ui-button (clicked)="successModal.set(true)" variant="filled">Success</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Configuration Options -->
|
||||
<section class="demo-section">
|
||||
<h3>Configuration Options</h3>
|
||||
<div class="demo-row">
|
||||
<ui-button (clicked)="noHeaderModal.set(true)">No Header</ui-button>
|
||||
<ui-button (clicked)="noFooterModal.set(true)">With Footer</ui-button>
|
||||
<ui-button (clicked)="noPaddingModal.set(true)">No Padding</ui-button>
|
||||
<ui-button (clicked)="notClosableModal.set(true)">Not Closable</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Features -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Features</h3>
|
||||
<div class="demo-row">
|
||||
<ui-button (clicked)="loadingModal.set(true)">Loading Modal</ui-button>
|
||||
<ui-button (clicked)="scrollableModal.set(true)">Scrollable Content</ui-button>
|
||||
<ui-button (clicked)="formModal.set(true)">Form Modal</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Configuration Panel -->
|
||||
<section class="demo-section">
|
||||
<h3>Try It Yourself</h3>
|
||||
<div class="config-panel">
|
||||
<div class="config-row">
|
||||
<label>Size:</label>
|
||||
<select [(ngModel)]="customConfig.size">
|
||||
<option value="sm">Small</option>
|
||||
<option value="md">Medium</option>
|
||||
<option value="lg">Large</option>
|
||||
<option value="xl">Extra Large</option>
|
||||
<option value="fullscreen">Fullscreen</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="config-row">
|
||||
<label>Variant:</label>
|
||||
<select [(ngModel)]="customConfig.variant">
|
||||
<option value="default">Default</option>
|
||||
<option value="danger">Danger</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="success">Success</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="config-row">
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="customConfig.closable">
|
||||
Closable
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="customConfig.backdropClosable">
|
||||
Backdrop Closable
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="config-row">
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="customConfig.showHeader">
|
||||
Show Header
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="customConfig.showFooter">
|
||||
Show Footer
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ui-button (clicked)="customModal.set(true)">Open Custom Modal</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Event Log -->
|
||||
<section class="demo-section">
|
||||
<h3>Event Log</h3>
|
||||
<div class="event-log">
|
||||
@for (event of eventLog; track $index) {
|
||||
<div class="event-item">{{ event }}</div>
|
||||
}
|
||||
@if (eventLog.length === 0) {
|
||||
<div class="event-item">No events yet...</div>
|
||||
}
|
||||
</div>
|
||||
<ui-button (clicked)="clearEventLog()" variant="outlined">Clear Log</ui-button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Basic Modal -->
|
||||
<ui-modal
|
||||
[open]="basicModal()"
|
||||
(openChange)="basicModal.set($event); logEvent('Basic modal: ' + ($event ? 'opened' : 'closed'))"
|
||||
title="Basic Modal"
|
||||
(opened)="logEvent('Basic modal opened')"
|
||||
(closed)="logEvent('Basic modal closed')"
|
||||
>
|
||||
<p>This is a basic modal with default settings.</p>
|
||||
<p>You can close it by clicking the X button, pressing Escape, or clicking outside the modal.</p>
|
||||
</ui-modal>
|
||||
|
||||
<!-- Size Modals -->
|
||||
<ui-modal
|
||||
[open]="smallModal()"
|
||||
(openChange)="smallModal.set($event)"
|
||||
size="sm"
|
||||
title="Small Modal"
|
||||
>
|
||||
<p>This is a small modal (400px width).</p>
|
||||
</ui-modal>
|
||||
|
||||
<ui-modal
|
||||
[open]="mediumModal()"
|
||||
(openChange)="mediumModal.set($event)"
|
||||
size="md"
|
||||
title="Medium Modal"
|
||||
>
|
||||
<p>This is a medium modal (600px width) - the default size.</p>
|
||||
</ui-modal>
|
||||
|
||||
<ui-modal
|
||||
[open]="largeModal()"
|
||||
(openChange)="largeModal.set($event)"
|
||||
size="lg"
|
||||
title="Large Modal"
|
||||
>
|
||||
<p>This is a large modal (800px width).</p>
|
||||
<p>Perfect for forms, detailed content, or complex interfaces.</p>
|
||||
</ui-modal>
|
||||
|
||||
<ui-modal
|
||||
[open]="xlModal()"
|
||||
(openChange)="xlModal.set($event)"
|
||||
size="xl"
|
||||
title="Extra Large Modal"
|
||||
>
|
||||
<p>This is an extra large modal (1000px width).</p>
|
||||
<p>Use this for very complex interfaces or wide content.</p>
|
||||
</ui-modal>
|
||||
|
||||
<ui-modal
|
||||
[open]="fullscreenModal()"
|
||||
(openChange)="fullscreenModal.set($event)"
|
||||
size="fullscreen"
|
||||
title="Fullscreen Modal"
|
||||
>
|
||||
<p>This modal takes up the entire screen.</p>
|
||||
<p>Perfect for complex applications or immersive experiences.</p>
|
||||
</ui-modal>
|
||||
|
||||
<!-- Variant Modals -->
|
||||
<ui-modal
|
||||
[open]="defaultModal()"
|
||||
(openChange)="defaultModal.set($event)"
|
||||
variant="default"
|
||||
title="Default Modal"
|
||||
>
|
||||
<p>This is the default modal variant.</p>
|
||||
</ui-modal>
|
||||
|
||||
<ui-modal
|
||||
[open]="warningModal()"
|
||||
(openChange)="warningModal.set($event)"
|
||||
variant="warning"
|
||||
title="Warning Modal"
|
||||
[showFooter]="true"
|
||||
>
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
|
||||
<fa-icon [icon]="faExclamationTriangle" style="color: #f59e0b; font-size: 24px;"></fa-icon>
|
||||
<p style="margin: 0;">Are you sure you want to continue? This action cannot be undone.</p>
|
||||
</div>
|
||||
|
||||
<ng-container slot="footer">
|
||||
<ui-button (clicked)="warningModal.set(false)" variant="outlined">Cancel</ui-button>
|
||||
<ui-button (clicked)="warningModal.set(false)" variant="filled">Continue</ui-button>
|
||||
</ng-container>
|
||||
</ui-modal>
|
||||
|
||||
<ui-modal
|
||||
[open]="dangerModal()"
|
||||
(openChange)="dangerModal.set($event)"
|
||||
variant="danger"
|
||||
title="Delete Item"
|
||||
[showFooter]="true"
|
||||
>
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
|
||||
<fa-icon [icon]="faExclamationTriangle" style="color: #ef4444; font-size: 24px;"></fa-icon>
|
||||
<p style="margin: 0;">This will permanently delete the item. This action cannot be undone.</p>
|
||||
</div>
|
||||
|
||||
<ng-container slot="footer">
|
||||
<ui-button (clicked)="dangerModal.set(false)" variant="outlined">Cancel</ui-button>
|
||||
<ui-button (clicked)="dangerModal.set(false)" variant="filled">Delete</ui-button>
|
||||
</ng-container>
|
||||
</ui-modal>
|
||||
|
||||
<ui-modal
|
||||
[open]="successModal()"
|
||||
(openChange)="successModal.set($event)"
|
||||
variant="success"
|
||||
title="Success!"
|
||||
[showFooter]="true"
|
||||
>
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
|
||||
<fa-icon [icon]="faCheckCircle" style="color: #10b981; font-size: 24px;"></fa-icon>
|
||||
<p style="margin: 0;">Your changes have been saved successfully!</p>
|
||||
</div>
|
||||
|
||||
<ng-container slot="footer">
|
||||
<ui-button (clicked)="successModal.set(false)" variant="filled">Close</ui-button>
|
||||
</ng-container>
|
||||
</ui-modal>
|
||||
|
||||
<!-- Configuration Modals -->
|
||||
<ui-modal
|
||||
[open]="noHeaderModal()"
|
||||
(openChange)="noHeaderModal.set($event)"
|
||||
[showHeader]="false"
|
||||
>
|
||||
<div style="padding: 16px; text-align: center;">
|
||||
<fa-icon [icon]="faInfoCircle" style="font-size: 48px; color: #6750a4; margin-bottom: 16px;"></fa-icon>
|
||||
<h3 style="margin: 0 0 16px 0;">Modal without header</h3>
|
||||
<p>This modal doesn't have a header section.</p>
|
||||
<ui-button (clicked)="noHeaderModal.set(false)" style="margin-top: 16px;">Close</ui-button>
|
||||
</div>
|
||||
</ui-modal>
|
||||
|
||||
<ui-modal
|
||||
[open]="noFooterModal()"
|
||||
(openChange)="noFooterModal.set($event)"
|
||||
title="Modal with Footer"
|
||||
[showFooter]="true"
|
||||
footerAlignment="between"
|
||||
>
|
||||
<p>This modal has a footer with buttons aligned between start and end.</p>
|
||||
|
||||
<ng-container slot="footer">
|
||||
<div>
|
||||
<ui-button (clicked)="logEvent('Help clicked')" variant="outlined">Help</ui-button>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<ui-button (clicked)="noFooterModal.set(false)" variant="outlined">Cancel</ui-button>
|
||||
<ui-button (clicked)="noFooterModal.set(false)" variant="filled">Save</ui-button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ui-modal>
|
||||
|
||||
<ui-modal
|
||||
[open]="noPaddingModal()"
|
||||
(openChange)="noPaddingModal.set($event)"
|
||||
title="No Padding Modal"
|
||||
[bodyPadding]="false"
|
||||
>
|
||||
<div style="padding: 0; background: linear-gradient(45deg, #6750a4, #7c3aed); height: 200px; display: flex; align-items: center; justify-content: center; color: white;">
|
||||
<p style="margin: 0; text-align: center;">This content spans the full width<br>with no padding constraints.</p>
|
||||
</div>
|
||||
</ui-modal>
|
||||
|
||||
<ui-modal
|
||||
[open]="notClosableModal()"
|
||||
(openChange)="notClosableModal.set($event)"
|
||||
title="Not Closable Modal"
|
||||
[closable]="false"
|
||||
[backdropClosable]="false"
|
||||
[escapeClosable]="false"
|
||||
[showFooter]="true"
|
||||
>
|
||||
<p>This modal cannot be closed by clicking the X, pressing Escape, or clicking the backdrop.</p>
|
||||
<p>You must use the Close button below.</p>
|
||||
|
||||
<ng-container slot="footer">
|
||||
<ui-button (clicked)="notClosableModal.set(false)" variant="filled">Close</ui-button>
|
||||
</ng-container>
|
||||
</ui-modal>
|
||||
|
||||
<!-- Interactive Modals -->
|
||||
<ui-modal
|
||||
[open]="loadingModal()"
|
||||
(openChange)="loadingModal.set($event)"
|
||||
title="Loading Data"
|
||||
[loading]="true"
|
||||
>
|
||||
<!-- Content will be replaced by loading spinner -->
|
||||
</ui-modal>
|
||||
|
||||
<ui-modal
|
||||
[open]="scrollableModal()"
|
||||
(openChange)="scrollableModal.set($event)"
|
||||
title="Scrollable Content"
|
||||
size="md"
|
||||
>
|
||||
<div style="height: 800px;">
|
||||
<h4>Long Content</h4>
|
||||
@for (item of longContent; track $index) {
|
||||
<p>{{ item }}</p>
|
||||
}
|
||||
</div>
|
||||
</ui-modal>
|
||||
|
||||
<ui-modal
|
||||
[open]="formModal()"
|
||||
(openChange)="formModal.set($event)"
|
||||
title="User Registration"
|
||||
[showFooter]="true"
|
||||
size="md"
|
||||
>
|
||||
<form (ngSubmit)="handleFormSubmit()" #userForm="ngForm">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<label style="display: block; margin-bottom: 4px;">Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="formData.name"
|
||||
name="name"
|
||||
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 16px;">
|
||||
<label style="display: block; margin-bottom: 4px;">Email:</label>
|
||||
<input
|
||||
type="email"
|
||||
[(ngModel)]="formData.email"
|
||||
name="email"
|
||||
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 16px;">
|
||||
<label style="display: block; margin-bottom: 4px;">Message:</label>
|
||||
<textarea
|
||||
[(ngModel)]="formData.message"
|
||||
name="message"
|
||||
rows="4"
|
||||
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; resize: vertical;"
|
||||
></textarea>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-container slot="footer">
|
||||
<ui-button (clicked)="formModal.set(false)" variant="outlined">Cancel</ui-button>
|
||||
<ui-button (clicked)="handleFormSubmit()" variant="filled">Submit</ui-button>
|
||||
</ng-container>
|
||||
</ui-modal>
|
||||
|
||||
<!-- Custom Configuration Modal -->
|
||||
<ui-modal
|
||||
[open]="customModal()"
|
||||
(openChange)="customModal.set($event)"
|
||||
[size]="customConfig.size"
|
||||
[variant]="customConfig.variant"
|
||||
[closable]="customConfig.closable"
|
||||
[backdropClosable]="customConfig.backdropClosable"
|
||||
[showHeader]="customConfig.showHeader"
|
||||
[showFooter]="customConfig.showFooter"
|
||||
title="Custom Configured Modal"
|
||||
>
|
||||
<p>This modal is configured based on your selections:</p>
|
||||
<ul>
|
||||
<li>Size: {{ customConfig.size }}</li>
|
||||
<li>Variant: {{ customConfig.variant }}</li>
|
||||
<li>Closable: {{ customConfig.closable ? 'Yes' : 'No' }}</li>
|
||||
<li>Backdrop Closable: {{ customConfig.backdropClosable ? 'Yes' : 'No' }}</li>
|
||||
<li>Show Header: {{ customConfig.showHeader ? 'Yes' : 'No' }}</li>
|
||||
<li>Show Footer: {{ customConfig.showFooter ? 'Yes' : 'No' }}</li>
|
||||
</ul>
|
||||
|
||||
@if (customConfig.showFooter) {
|
||||
<ng-container slot="footer">
|
||||
<ui-button (clicked)="customModal.set(false)" variant="filled">Close</ui-button>
|
||||
</ng-container>
|
||||
}
|
||||
</ui-modal>
|
||||
`,
|
||||
styleUrl: './modal-demo.component.scss'
|
||||
})
|
||||
export class ModalDemoComponent {
|
||||
// Icons
|
||||
readonly faExclamationTriangle = faExclamationTriangle;
|
||||
readonly faCheckCircle = faCheckCircle;
|
||||
readonly faInfoCircle = faInfoCircle;
|
||||
|
||||
// Modal states
|
||||
basicModal = signal(false);
|
||||
smallModal = signal(false);
|
||||
mediumModal = signal(false);
|
||||
largeModal = signal(false);
|
||||
xlModal = signal(false);
|
||||
fullscreenModal = signal(false);
|
||||
|
||||
defaultModal = signal(false);
|
||||
warningModal = signal(false);
|
||||
dangerModal = signal(false);
|
||||
successModal = signal(false);
|
||||
|
||||
noHeaderModal = signal(false);
|
||||
noFooterModal = signal(false);
|
||||
noPaddingModal = signal(false);
|
||||
notClosableModal = signal(false);
|
||||
|
||||
loadingModal = signal(false);
|
||||
scrollableModal = signal(false);
|
||||
formModal = signal(false);
|
||||
customModal = signal(false);
|
||||
|
||||
// Demo data
|
||||
eventLog: string[] = [];
|
||||
|
||||
customConfig = {
|
||||
size: 'md' as any,
|
||||
variant: 'default' as any,
|
||||
closable: true,
|
||||
backdropClosable: true,
|
||||
showHeader: true,
|
||||
showFooter: false
|
||||
};
|
||||
|
||||
formData = {
|
||||
name: '',
|
||||
email: '',
|
||||
message: ''
|
||||
};
|
||||
|
||||
longContent = Array.from({ length: 50 }, (_, i) =>
|
||||
`This is paragraph ${i + 1}. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
|
||||
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.`
|
||||
);
|
||||
|
||||
// Simulate loading
|
||||
constructor() {
|
||||
setTimeout(() => {
|
||||
if (this.loadingModal()) {
|
||||
this.loadingModal.set(false);
|
||||
this.logEvent('Loading completed');
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
logEvent(event: string): void {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
this.eventLog.unshift(`[${timestamp}] ${event}`);
|
||||
if (this.eventLog.length > 10) {
|
||||
this.eventLog = this.eventLog.slice(0, 10);
|
||||
}
|
||||
}
|
||||
|
||||
clearEventLog(): void {
|
||||
this.eventLog = [];
|
||||
}
|
||||
|
||||
handleFormSubmit(): void {
|
||||
this.logEvent(`Form submitted: ${JSON.stringify(this.formData)}`);
|
||||
this.formModal.set(false);
|
||||
|
||||
// Reset form
|
||||
this.formData = {
|
||||
name: '',
|
||||
email: '',
|
||||
message: ''
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic/index' as tokens;
|
||||
|
||||
.demo-container {
|
||||
padding: tokens.$semantic-spacing-layout-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
font-size: tokens.$semantic-typography-heading-h2-size;
|
||||
color: tokens.$semantic-color-text-primary;
|
||||
margin-bottom: tokens.$semantic-spacing-content-heading;
|
||||
border-bottom: tokens.$semantic-border-width-1 solid tokens.$semantic-color-border-subtle;
|
||||
padding-bottom: tokens.$semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: tokens.$semantic-typography-heading-h3-size;
|
||||
color: tokens.$semantic-color-text-primary;
|
||||
margin-bottom: tokens.$semantic-spacing-content-paragraph;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: tokens.$semantic-spacing-layout-lg;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: tokens.$semantic-spacing-component-sm;
|
||||
margin-bottom: tokens.$semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: tokens.$semantic-sizing-button-height-md;
|
||||
padding: tokens.$semantic-spacing-component-sm tokens.$semantic-spacing-component-md;
|
||||
|
||||
// Visual design
|
||||
background: tokens.$semantic-color-primary;
|
||||
color: tokens.$semantic-color-on-primary;
|
||||
border: none;
|
||||
border-radius: tokens.$semantic-border-radius-md;
|
||||
box-shadow: tokens.$semantic-shadow-button-rest;
|
||||
|
||||
// Typography
|
||||
font-size: tokens.$semantic-typography-font-size-md;
|
||||
font-weight: tokens.$semantic-typography-font-weight-medium;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
|
||||
// Transitions
|
||||
transition: background-color tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease,
|
||||
box-shadow tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease,
|
||||
transform tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease;
|
||||
|
||||
// Cursor
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
// Interactive states
|
||||
&:hover {
|
||||
background: tokens.$semantic-color-primary-hover;
|
||||
box-shadow: tokens.$semantic-shadow-button-hover;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid tokens.$semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: tokens.$semantic-color-primary-pressed;
|
||||
box-shadow: tokens.$semantic-shadow-button-active;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: tokens.$semantic-color-surface-disabled;
|
||||
color: tokens.$semantic-color-text-disabled;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: tokens.$semantic-spacing-component-xs;
|
||||
|
||||
p {
|
||||
font-size: tokens.$semantic-typography-font-size-md;
|
||||
color: tokens.$semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
|
||||
&:before {
|
||||
content: "📊 ";
|
||||
margin-right: tokens.$semantic-spacing-component-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overlay-content {
|
||||
padding: tokens.$semantic-spacing-component-lg;
|
||||
text-align: center;
|
||||
|
||||
h3 {
|
||||
font-size: tokens.$semantic-typography-heading-h3-size;
|
||||
color: tokens.$semantic-color-text-primary;
|
||||
margin: 0 0 tokens.$semantic-spacing-content-paragraph 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: tokens.$semantic-typography-font-size-md;
|
||||
color: tokens.$semantic-color-text-secondary;
|
||||
margin: 0 0 tokens.$semantic-spacing-content-heading 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: tokens.$semantic-sizing-button-height-md;
|
||||
padding: tokens.$semantic-spacing-component-sm tokens.$semantic-spacing-component-lg;
|
||||
|
||||
// Visual design
|
||||
background: tokens.$semantic-color-secondary;
|
||||
color: tokens.$semantic-color-on-secondary;
|
||||
border: tokens.$semantic-border-width-1 solid tokens.$semantic-color-border-primary;
|
||||
border-radius: tokens.$semantic-border-radius-md;
|
||||
box-shadow: tokens.$semantic-shadow-button-rest;
|
||||
|
||||
// Typography
|
||||
font-size: tokens.$semantic-typography-font-size-md;
|
||||
font-weight: tokens.$semantic-typography-font-weight-medium;
|
||||
|
||||
// Transitions
|
||||
transition: background-color tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease,
|
||||
border-color tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease,
|
||||
box-shadow tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease,
|
||||
transform tokens.$semantic-motion-duration-fast tokens.$semantic-motion-easing-ease;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: tokens.$semantic-color-secondary-hover;
|
||||
border-color: tokens.$semantic-color-border-primary;
|
||||
box-shadow: tokens.$semantic-shadow-button-hover;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid tokens.$semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: tokens.$semantic-color-secondary-pressed;
|
||||
box-shadow: tokens.$semantic-shadow-button-active;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.focus-trap-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: tokens.$semantic-spacing-component-sm;
|
||||
align-items: center;
|
||||
|
||||
button,
|
||||
input {
|
||||
min-height: tokens.$semantic-sizing-input-height-md;
|
||||
padding: tokens.$semantic-spacing-component-sm tokens.$semantic-spacing-component-md;
|
||||
border: tokens.$semantic-border-width-1 solid tokens.$semantic-color-border-primary;
|
||||
border-radius: tokens.$semantic-border-radius-md;
|
||||
font-size: tokens.$semantic-typography-font-size-md;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid tokens.$semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
background: tokens.$semantic-color-surface-secondary;
|
||||
color: tokens.$semantic-color-text-primary;
|
||||
|
||||
&::placeholder {
|
||||
color: tokens.$semantic-color-text-tertiary;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
background: tokens.$semantic-color-tertiary;
|
||||
color: tokens.$semantic-color-on-tertiary;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: tokens.$semantic-color-secondary-hover;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
background: tokens.$semantic-color-danger;
|
||||
color: tokens.$semantic-color-on-danger;
|
||||
|
||||
&:hover {
|
||||
background: tokens.$semantic-color-primary-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: #{tokens.$semantic-breakpoint-md - 1px}) {
|
||||
.demo-container {
|
||||
padding: tokens.$semantic-spacing-layout-sm;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: #{tokens.$semantic-breakpoint-sm - 1px}) {
|
||||
.overlay-content {
|
||||
padding: tokens.$semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.focus-trap-demo {
|
||||
button,
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { OverlayContainerComponent } from '../../../../../ui-essentials/src/lib/components/overlays/overlay-container/overlay-container.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-overlay-container-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, OverlayContainerComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Overlay Container Demo</h2>
|
||||
|
||||
<!-- Position Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Positions</h3>
|
||||
<div class="demo-row">
|
||||
@for (position of positions; track position) {
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="showOverlay(position, 'auto', 'default')"
|
||||
>
|
||||
{{ position }} position
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="showOverlay('center', size, 'default')"
|
||||
>
|
||||
{{ size }} size
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Types -->
|
||||
<section class="demo-section">
|
||||
<h3>Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (variant of variants; track variant) {
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="showOverlay('center', 'md', variant)"
|
||||
>
|
||||
{{ variant }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features -->
|
||||
<section class="demo-section">
|
||||
<h3>Features</h3>
|
||||
<div class="demo-row">
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="showFeatureOverlay('no-backdrop')"
|
||||
>
|
||||
No Backdrop
|
||||
</button>
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="showFeatureOverlay('backdrop-blur')"
|
||||
>
|
||||
Backdrop Blur
|
||||
</button>
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="showFeatureOverlay('focus-trap')"
|
||||
>
|
||||
Focus Trap
|
||||
</button>
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="showFeatureOverlay('no-escape')"
|
||||
>
|
||||
No ESC Close
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Offset Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Offset Positioning</h3>
|
||||
<div class="demo-row">
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="showOffsetOverlay()"
|
||||
>
|
||||
Offset Overlay
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Stats -->
|
||||
<section class="demo-section">
|
||||
<h3>Stats</h3>
|
||||
<div class="demo-stats">
|
||||
<p>Overlays shown: {{ overlayCount }}</p>
|
||||
<p>Backdrop clicks: {{ backdropClickCount }}</p>
|
||||
<p>ESC key presses: {{ escapeCount }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Overlay Instances -->
|
||||
<ui-overlay-container
|
||||
[visible]="currentOverlay.visible"
|
||||
[position]="currentOverlay.position"
|
||||
[size]="currentOverlay.size"
|
||||
[variant]="currentOverlay.variant"
|
||||
[showBackdrop]="currentOverlay.showBackdrop"
|
||||
[backdropBlur]="currentOverlay.backdropBlur"
|
||||
[focusTrap]="currentOverlay.focusTrap"
|
||||
[escapeClosable]="currentOverlay.escapeClosable"
|
||||
[offsetX]="currentOverlay.offsetX"
|
||||
[offsetY]="currentOverlay.offsetY"
|
||||
(visibleChange)="handleVisibilityChange($event)"
|
||||
(backdropClicked)="handleBackdropClick()"
|
||||
(escapePressed)="handleEscapePress()"
|
||||
>
|
||||
<div class="overlay-content">
|
||||
<h3>{{ currentOverlay.title }}</h3>
|
||||
<p>{{ currentOverlay.description }}</p>
|
||||
@if (currentOverlay.focusTrap) {
|
||||
<div class="focus-trap-demo">
|
||||
<button>First Button</button>
|
||||
<input type="text" placeholder="Input field">
|
||||
<button>Second Button</button>
|
||||
<button (click)="closeOverlay()">Close</button>
|
||||
</div>
|
||||
} @else {
|
||||
<button (click)="closeOverlay()">Close Overlay</button>
|
||||
}
|
||||
</div>
|
||||
</ui-overlay-container>
|
||||
`,
|
||||
styleUrl: './overlay-container-demo.component.scss'
|
||||
})
|
||||
export class OverlayContainerDemoComponent {
|
||||
positions = ['center', 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'] as const;
|
||||
sizes = ['auto', 'sm', 'md', 'lg', 'xl', 'fullscreen'] as const;
|
||||
variants = ['default', 'elevated', 'floating', 'modal', 'popover'] as const;
|
||||
|
||||
// Stats
|
||||
overlayCount = 0;
|
||||
backdropClickCount = 0;
|
||||
escapeCount = 0;
|
||||
|
||||
// Current overlay state
|
||||
currentOverlay = {
|
||||
visible: false,
|
||||
position: 'center' as typeof this.positions[number],
|
||||
size: 'auto' as typeof this.sizes[number],
|
||||
variant: 'default' as typeof this.variants[number],
|
||||
showBackdrop: true,
|
||||
backdropBlur: false,
|
||||
focusTrap: false,
|
||||
escapeClosable: true,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
title: 'Overlay Title',
|
||||
description: 'This is an overlay container demonstration.'
|
||||
};
|
||||
|
||||
showOverlay(position: typeof this.positions[number], size: typeof this.sizes[number], variant: typeof this.variants[number]): void {
|
||||
this.currentOverlay = {
|
||||
...this.currentOverlay,
|
||||
visible: true,
|
||||
position,
|
||||
size,
|
||||
variant,
|
||||
showBackdrop: true,
|
||||
backdropBlur: false,
|
||||
focusTrap: false,
|
||||
escapeClosable: true,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
title: `${variant} Overlay`,
|
||||
description: `Position: ${position}, Size: ${size}, Variant: ${variant}`
|
||||
};
|
||||
this.overlayCount++;
|
||||
}
|
||||
|
||||
showFeatureOverlay(feature: string): void {
|
||||
let config = {
|
||||
visible: true,
|
||||
position: 'center' as const,
|
||||
size: 'md' as const,
|
||||
variant: 'default' as const,
|
||||
showBackdrop: true,
|
||||
backdropBlur: false,
|
||||
focusTrap: false,
|
||||
escapeClosable: true,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
title: 'Feature Demo',
|
||||
description: 'Demonstrating overlay features.'
|
||||
};
|
||||
|
||||
switch (feature) {
|
||||
case 'no-backdrop':
|
||||
config.showBackdrop = false;
|
||||
config.title = 'No Backdrop';
|
||||
config.description = 'This overlay has no backdrop.';
|
||||
break;
|
||||
case 'backdrop-blur':
|
||||
config.backdropBlur = true;
|
||||
config.title = 'Backdrop Blur';
|
||||
config.description = 'This overlay has a blurred backdrop effect.';
|
||||
break;
|
||||
case 'focus-trap':
|
||||
config.focusTrap = true;
|
||||
config.title = 'Focus Trap';
|
||||
config.description = 'Try pressing Tab - focus is trapped within this overlay.';
|
||||
break;
|
||||
case 'no-escape':
|
||||
config.escapeClosable = false;
|
||||
config.title = 'No ESC Close';
|
||||
config.description = 'ESC key will not close this overlay.';
|
||||
break;
|
||||
}
|
||||
|
||||
this.currentOverlay = { ...this.currentOverlay, ...config };
|
||||
this.overlayCount++;
|
||||
}
|
||||
|
||||
showOffsetOverlay(): void {
|
||||
this.currentOverlay = {
|
||||
...this.currentOverlay,
|
||||
visible: true,
|
||||
position: 'top-right',
|
||||
size: 'sm',
|
||||
variant: 'floating',
|
||||
showBackdrop: false,
|
||||
backdropBlur: false,
|
||||
focusTrap: false,
|
||||
escapeClosable: true,
|
||||
offsetX: -20,
|
||||
offsetY: 20,
|
||||
title: 'Offset Overlay',
|
||||
description: 'This overlay is positioned with X and Y offsets.'
|
||||
};
|
||||
this.overlayCount++;
|
||||
}
|
||||
|
||||
closeOverlay(): void {
|
||||
this.currentOverlay.visible = false;
|
||||
}
|
||||
|
||||
handleVisibilityChange(visible: boolean): void {
|
||||
this.currentOverlay.visible = visible;
|
||||
}
|
||||
|
||||
handleBackdropClick(): void {
|
||||
this.backdropClickCount++;
|
||||
console.log('Backdrop clicked');
|
||||
}
|
||||
|
||||
handleEscapePress(): void {
|
||||
this.escapeCount++;
|
||||
console.log('ESC key pressed');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { PaginationComponent } from '../../../../../ui-essentials/src/lib/components/navigation/pagination';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-pagination-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, PaginationComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Pagination Component Showcase</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Sizes</h3>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Small</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageSmall"
|
||||
[totalPages]="totalPagesDemo"
|
||||
size="sm"
|
||||
(pageChange)="onPageChange($event, 'small')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Medium (Default)</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageMedium"
|
||||
[totalPages]="totalPagesDemo"
|
||||
size="md"
|
||||
(pageChange)="onPageChange($event, 'medium')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Large</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageLarge"
|
||||
[totalPages]="totalPagesDemo"
|
||||
size="lg"
|
||||
(pageChange)="onPageChange($event, 'large')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Alignment Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Alignment</h3>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Left Aligned</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageLeft"
|
||||
[totalPages]="totalPagesDemo"
|
||||
alignment="left"
|
||||
(pageChange)="onPageChange($event, 'left')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Center Aligned (Default)</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageCenter"
|
||||
[totalPages]="totalPagesDemo"
|
||||
alignment="center"
|
||||
(pageChange)="onPageChange($event, 'center')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Right Aligned</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageRight"
|
||||
[totalPages]="totalPagesDemo"
|
||||
alignment="right"
|
||||
(pageChange)="onPageChange($event, 'right')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Page Info Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>With Page Information</h3>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Basic Page Info</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageInfo"
|
||||
[totalPages]="totalPagesDemo"
|
||||
[showInfo]="true"
|
||||
(pageChange)="onPageChange($event, 'info')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>With Items Count</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageItems"
|
||||
[totalPages]="totalPagesItems"
|
||||
[totalItems]="totalItemsDemo"
|
||||
[itemsPerPage]="itemsPerPageDemo"
|
||||
[showInfo]="true"
|
||||
(pageChange)="onPageChange($event, 'items')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Without Info</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageNoInfo"
|
||||
[totalPages]="totalPagesDemo"
|
||||
[showInfo]="false"
|
||||
(pageChange)="onPageChange($event, 'no-info')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Compact Variant -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Compact Style</h3>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Compact Pagination</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageCompact"
|
||||
[totalPages]="totalPagesDemo"
|
||||
[compact]="true"
|
||||
(pageChange)="onPageChange($event, 'compact')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Compact with Labels</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageCompactLabels"
|
||||
[totalPages]="totalPagesDemo"
|
||||
[compact]="true"
|
||||
[showLabels]="true"
|
||||
(pageChange)="onPageChange($event, 'compact-labels')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Different Page Counts -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Different Page Counts</h3>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Few Pages (5 total)</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageFew"
|
||||
[totalPages]="totalPagesFew"
|
||||
(pageChange)="onPageChange($event, 'few')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Many Pages (50 total)</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageMany"
|
||||
[totalPages]="totalPagesMany"
|
||||
(pageChange)="onPageChange($event, 'many')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Limited Visible Pages (maxVisible=5)</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageLimited"
|
||||
[totalPages]="totalPagesMany"
|
||||
[maxVisible]="5"
|
||||
(pageChange)="onPageChange($event, 'limited')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Labels and States -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Labels and States</h3>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>With Navigation Labels</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageLabels"
|
||||
[totalPages]="totalPagesDemo"
|
||||
[showLabels]="true"
|
||||
previousLabel="Back"
|
||||
nextLabel="Forward"
|
||||
(pageChange)="onPageChange($event, 'labels')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Custom ARIA Labels</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageAria"
|
||||
[totalPages]="totalPagesDemo"
|
||||
ariaLabel="Results pagination"
|
||||
previousAriaLabel="Go to previous results page"
|
||||
nextAriaLabel="Go to next results page"
|
||||
(pageChange)="onPageChange($event, 'aria')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Disabled State</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageDisabled"
|
||||
[totalPages]="totalPagesDemo"
|
||||
[disabled]="true"
|
||||
(pageChange)="onPageChange($event, 'disabled')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Pagination</h3>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Controlled Pagination</h4>
|
||||
<div style="margin-bottom: 1rem; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;">
|
||||
<button
|
||||
style="padding: 0.5rem 1rem; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;"
|
||||
(click)="goToFirstPage()">
|
||||
First Page
|
||||
</button>
|
||||
<button
|
||||
style="padding: 0.5rem 1rem; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;"
|
||||
(click)="goToLastPage()">
|
||||
Last Page
|
||||
</button>
|
||||
<button
|
||||
style="padding: 0.5rem 1rem; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;"
|
||||
(click)="jumpToMiddle()">
|
||||
Jump to Middle
|
||||
</button>
|
||||
<button
|
||||
style="padding: 0.5rem 1rem; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;"
|
||||
(click)="resetInteractive()">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
<ui-pagination
|
||||
#interactivePagination
|
||||
[currentPage]="currentPageInteractive"
|
||||
[totalPages]="totalPagesInteractive"
|
||||
[totalItems]="totalItemsInteractive()"
|
||||
[itemsPerPage]="itemsPerPageDemo"
|
||||
[showInfo]="true"
|
||||
size="lg"
|
||||
(pageChange)="onInteractivePageChange($event)">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
|
||||
@if (lastPageChange) {
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #e3f2fd; border-radius: 4px;">
|
||||
<strong>Last page change:</strong> {{ lastPageChange }}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Usage Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Basic Pagination:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-pagination
|
||||
[currentPage]="currentPage"
|
||||
[totalPages]="totalPages"
|
||||
(pageChange)="onPageChange($event)">
|
||||
</ui-pagination></code></pre>
|
||||
|
||||
<h4>With Item Information:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-pagination
|
||||
[currentPage]="currentPage"
|
||||
[totalPages]="totalPages"
|
||||
[totalItems]="totalItems"
|
||||
[itemsPerPage]="itemsPerPage"
|
||||
[showInfo]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
</ui-pagination></code></pre>
|
||||
|
||||
<h4>Compact with Labels:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-pagination
|
||||
[currentPage]="currentPage"
|
||||
[totalPages]="totalPages"
|
||||
[compact]="true"
|
||||
[showLabels]="true"
|
||||
size="sm"
|
||||
alignment="left"
|
||||
previousLabel="Back"
|
||||
nextLabel="Forward"
|
||||
(pageChange)="onPageChange($event)">
|
||||
</ui-pagination></code></pre>
|
||||
|
||||
<h4>Customized Pagination:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-pagination
|
||||
[currentPage]="currentPage"
|
||||
[totalPages]="totalPages"
|
||||
[maxVisible]="5"
|
||||
size="lg"
|
||||
alignment="center"
|
||||
ariaLabel="Search results pagination"
|
||||
[showInfo]="true"
|
||||
(pageChange)="handlePageChange($event)">
|
||||
</ui-pagination></code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Edge Cases -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Edge Cases</h3>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Single Page</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageOne"
|
||||
[totalPages]="totalPagesOne"
|
||||
(pageChange)="onPageChange($event, 'single')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Two Pages</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageTwo"
|
||||
[totalPages]="totalPagesTwo"
|
||||
(pageChange)="onPageChange($event, 'two')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Large Dataset (1000+ pages)</h4>
|
||||
<ui-pagination
|
||||
[currentPage]="currentPageLargeDataset"
|
||||
[totalPages]="totalPagesLargeDataset"
|
||||
[totalItems]="24934"
|
||||
[itemsPerPage]="20"
|
||||
[showInfo]="true"
|
||||
[maxVisible]="7"
|
||||
(pageChange)="onPageChange($event, 'large-dataset')">
|
||||
</ui-pagination>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(287, 12%, 35%);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid hsl(289, 14%, 90%);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: hsl(286, 20%, 99%);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #f0f0f0 !important;
|
||||
}
|
||||
|
||||
button:active {
|
||||
background: #e0e0e0 !important;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class PaginationDemoComponent {
|
||||
// Size variants
|
||||
currentPageSmall = signal(2);
|
||||
currentPageMedium = signal(3);
|
||||
currentPageLarge = signal(4);
|
||||
|
||||
// Alignment variants
|
||||
currentPageLeft = signal(1);
|
||||
currentPageCenter = signal(5);
|
||||
currentPageRight = signal(8);
|
||||
|
||||
// Info variants
|
||||
currentPageInfo = signal(3);
|
||||
currentPageItems = signal(2);
|
||||
currentPageNoInfo = signal(6);
|
||||
|
||||
// Compact variants
|
||||
currentPageCompact = signal(4);
|
||||
currentPageCompactLabels = signal(2);
|
||||
|
||||
// Different counts
|
||||
currentPageFew = signal(3);
|
||||
currentPageMany = signal(25);
|
||||
currentPageLimited = signal(15);
|
||||
|
||||
// Labels and states
|
||||
currentPageLabels = signal(1);
|
||||
currentPageAria = signal(1);
|
||||
currentPageDisabled = signal(5);
|
||||
|
||||
// Interactive
|
||||
currentPageInteractive = signal(1);
|
||||
totalPagesInteractive = signal(20);
|
||||
totalItemsInteractive = signal(387);
|
||||
|
||||
// Edge cases
|
||||
currentPageTwo = signal(1);
|
||||
currentPageLargeDataset = signal(234);
|
||||
|
||||
// Constants for edge cases
|
||||
totalPagesOne = signal(1);
|
||||
currentPageOne = signal(1);
|
||||
totalPagesTwo = signal(2);
|
||||
totalPagesLargeDataset = signal(1247);
|
||||
|
||||
// Demo data
|
||||
totalPagesDemo = signal(12);
|
||||
totalPagesFew = signal(5);
|
||||
totalPagesMany = signal(50);
|
||||
totalItemsDemo = 234;
|
||||
itemsPerPageDemo = 20;
|
||||
totalPagesItems = signal(Math.ceil(this.totalItemsDemo / this.itemsPerPageDemo));
|
||||
|
||||
// Interactive state
|
||||
lastPageChange: string = '';
|
||||
|
||||
onPageChange(event: any, type: string): void {
|
||||
this.lastPageChange = `${type}: Page ${event.previousPage} → ${event.page}`;
|
||||
console.log(`Page changed in ${type}:`, event);
|
||||
|
||||
// Update the appropriate signal based on type
|
||||
switch (type) {
|
||||
case 'small':
|
||||
this.currentPageSmall.set(event.page);
|
||||
break;
|
||||
case 'medium':
|
||||
this.currentPageMedium.set(event.page);
|
||||
break;
|
||||
case 'large':
|
||||
this.currentPageLarge.set(event.page);
|
||||
break;
|
||||
case 'left':
|
||||
this.currentPageLeft.set(event.page);
|
||||
break;
|
||||
case 'center':
|
||||
this.currentPageCenter.set(event.page);
|
||||
break;
|
||||
case 'right':
|
||||
this.currentPageRight.set(event.page);
|
||||
break;
|
||||
case 'info':
|
||||
this.currentPageInfo.set(event.page);
|
||||
break;
|
||||
case 'items':
|
||||
this.currentPageItems.set(event.page);
|
||||
break;
|
||||
case 'no-info':
|
||||
this.currentPageNoInfo.set(event.page);
|
||||
break;
|
||||
case 'compact':
|
||||
this.currentPageCompact.set(event.page);
|
||||
break;
|
||||
case 'compact-labels':
|
||||
this.currentPageCompactLabels.set(event.page);
|
||||
break;
|
||||
case 'few':
|
||||
this.currentPageFew.set(event.page);
|
||||
break;
|
||||
case 'many':
|
||||
this.currentPageMany.set(event.page);
|
||||
break;
|
||||
case 'limited':
|
||||
this.currentPageLimited.set(event.page);
|
||||
break;
|
||||
case 'labels':
|
||||
this.currentPageLabels.set(event.page);
|
||||
break;
|
||||
case 'aria':
|
||||
this.currentPageAria.set(event.page);
|
||||
break;
|
||||
case 'two':
|
||||
this.currentPageTwo.set(event.page);
|
||||
break;
|
||||
case 'large-dataset':
|
||||
this.currentPageLargeDataset.set(event.page);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onInteractivePageChange(event: any): void {
|
||||
this.currentPageInteractive.set(event.page);
|
||||
this.lastPageChange = `Interactive: Page ${event.previousPage} → ${event.page}`;
|
||||
}
|
||||
|
||||
// Interactive controls
|
||||
goToFirstPage(): void {
|
||||
this.currentPageInteractive.set(1);
|
||||
this.lastPageChange = 'Programmatically navigated to first page';
|
||||
}
|
||||
|
||||
goToLastPage(): void {
|
||||
this.currentPageInteractive.set(this.totalPagesInteractive());
|
||||
this.lastPageChange = 'Programmatically navigated to last page';
|
||||
}
|
||||
|
||||
jumpToMiddle(): void {
|
||||
const middle = Math.ceil(this.totalPagesInteractive() / 2);
|
||||
this.currentPageInteractive.set(middle);
|
||||
this.lastPageChange = `Programmatically jumped to middle page (${middle})`;
|
||||
}
|
||||
|
||||
resetInteractive(): void {
|
||||
this.currentPageInteractive.set(1);
|
||||
this.lastPageChange = 'Interactive pagination reset';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,584 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ProgressBarComponent } from '../../../../../ui-essentials/src/lib/components/data-display/progress';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-progress-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ProgressBarComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Progress Bar Component Showcase</h2>
|
||||
|
||||
<!-- Basic Progress Bars -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Basic Progress Bars</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 2rem;">
|
||||
<ui-progress-bar
|
||||
label="Small Progress"
|
||||
size="sm"
|
||||
[progress]="25"
|
||||
helperText="25% complete">
|
||||
</ui-progress-bar>
|
||||
<ui-progress-bar
|
||||
label="Medium Progress"
|
||||
size="md"
|
||||
[progress]="50"
|
||||
helperText="50% complete">
|
||||
</ui-progress-bar>
|
||||
<ui-progress-bar
|
||||
label="Large Progress"
|
||||
size="lg"
|
||||
[progress]="75"
|
||||
helperText="75% complete">
|
||||
</ui-progress-bar>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Progress Bar Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Color Variants</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 2rem;">
|
||||
<ui-progress-bar
|
||||
label="Primary Progress"
|
||||
variant="primary"
|
||||
[progress]="60"
|
||||
helperText="Default brand color">
|
||||
</ui-progress-bar>
|
||||
<ui-progress-bar
|
||||
label="Secondary Progress"
|
||||
variant="secondary"
|
||||
[progress]="45"
|
||||
helperText="Secondary color variant">
|
||||
</ui-progress-bar>
|
||||
<ui-progress-bar
|
||||
label="Success Progress"
|
||||
variant="success"
|
||||
[progress]="80"
|
||||
helperText="Indicates successful completion">
|
||||
</ui-progress-bar>
|
||||
<ui-progress-bar
|
||||
label="Warning Progress"
|
||||
variant="warning"
|
||||
[progress]="30"
|
||||
helperText="Indicates caution or attention needed">
|
||||
</ui-progress-bar>
|
||||
<ui-progress-bar
|
||||
label="Danger Progress"
|
||||
variant="danger"
|
||||
[progress]="15"
|
||||
helperText="Indicates error or critical state">
|
||||
</ui-progress-bar>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Progress Bar Types -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Progress Bar Types</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 2rem;">
|
||||
<ui-progress-bar
|
||||
label="Determinate Progress"
|
||||
progressType="determinate"
|
||||
[progress]="determinateProgress()"
|
||||
helperText="Shows exact progress value">
|
||||
</ui-progress-bar>
|
||||
<ui-progress-bar
|
||||
label="Indeterminate Progress"
|
||||
progressType="indeterminate"
|
||||
variant="primary"
|
||||
helperText="Shows ongoing activity without specific progress">
|
||||
</ui-progress-bar>
|
||||
<ui-progress-bar
|
||||
label="Buffer Progress"
|
||||
progressType="buffer"
|
||||
[progress]="bufferProgress()"
|
||||
[buffer]="bufferValue()"
|
||||
variant="secondary"
|
||||
helperText="Shows buffered content loading">
|
||||
</ui-progress-bar>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Striped and Animated -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Striped and Animated Progress Bars</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 2rem;">
|
||||
<ui-progress-bar
|
||||
label="Striped Progress"
|
||||
[progress]="65"
|
||||
[striped]="true"
|
||||
helperText="With striped pattern">
|
||||
</ui-progress-bar>
|
||||
<ui-progress-bar
|
||||
label="Animated Striped Progress"
|
||||
[progress]="55"
|
||||
[striped]="true"
|
||||
[animated]="true"
|
||||
variant="success"
|
||||
helperText="With animated stripes">
|
||||
</ui-progress-bar>
|
||||
<ui-progress-bar
|
||||
label="Animated Indeterminate"
|
||||
progressType="indeterminate"
|
||||
[animated]="true"
|
||||
variant="primary"
|
||||
helperText="Smooth indeterminate animation">
|
||||
</ui-progress-bar>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Progress Bar States</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 2rem;">
|
||||
<ui-progress-bar
|
||||
label="Normal State"
|
||||
[progress]="70"
|
||||
helperText="Default active state">
|
||||
</ui-progress-bar>
|
||||
<ui-progress-bar
|
||||
label="Disabled State"
|
||||
[progress]="40"
|
||||
state="disabled"
|
||||
helperText="Disabled progress bar">
|
||||
</ui-progress-bar>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Comparison -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Size and Variant Combinations</h3>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; max-width: 1200px;">
|
||||
|
||||
<!-- Small sizes -->
|
||||
<div>
|
||||
<h4>Small Size Variants</h4>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<ui-progress-bar size="sm" variant="primary" [progress]="25" label="Primary Small"></ui-progress-bar>
|
||||
<ui-progress-bar size="sm" variant="success" [progress]="50" label="Success Small"></ui-progress-bar>
|
||||
<ui-progress-bar size="sm" variant="warning" [progress]="75" label="Warning Small"></ui-progress-bar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Large sizes -->
|
||||
<div>
|
||||
<h4>Large Size Variants</h4>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<ui-progress-bar size="lg" variant="primary" [progress]="85" label="Primary Large"></ui-progress-bar>
|
||||
<ui-progress-bar size="lg" variant="success" [progress]="60" label="Success Large"></ui-progress-bar>
|
||||
<ui-progress-bar size="lg" variant="danger" [progress]="35" label="Danger Large"></ui-progress-bar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Without Labels -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Progress Bars without Labels</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1.5rem;">
|
||||
<ui-progress-bar
|
||||
[progress]="90"
|
||||
variant="success"
|
||||
[showLabel]="false"
|
||||
ariaLabel="File upload progress">
|
||||
</ui-progress-bar>
|
||||
<ui-progress-bar
|
||||
progressType="indeterminate"
|
||||
variant="primary"
|
||||
[showLabel]="false"
|
||||
ariaLabel="Loading content">
|
||||
</ui-progress-bar>
|
||||
<ui-progress-bar
|
||||
[progress]="45"
|
||||
[buffer]="70"
|
||||
progressType="buffer"
|
||||
variant="secondary"
|
||||
[showLabel]="false"
|
||||
ariaLabel="Video buffering progress">
|
||||
</ui-progress-bar>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Demo</h3>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 2rem; margin-bottom: 2rem;">
|
||||
|
||||
<!-- Controls -->
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; min-width: 200px;">
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Progress Value:</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
[(ngModel)]="demoProgress"
|
||||
style="width: 100%;">
|
||||
<span style="font-size: 0.875rem; color: #666;">{{ demoProgress }}%</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Buffer Value:</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
[(ngModel)]="demoBuffer"
|
||||
style="width: 100%;">
|
||||
<span style="font-size: 0.875rem; color: #666;">{{ demoBuffer }}%</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Size:</label>
|
||||
<select [(ngModel)]="demoSize" style="padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; width: 100%;">
|
||||
<option value="sm">Small</option>
|
||||
<option value="md">Medium</option>
|
||||
<option value="lg">Large</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Variant:</label>
|
||||
<select [(ngModel)]="demoVariant" style="padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; width: 100%;">
|
||||
<option value="primary">Primary</option>
|
||||
<option value="secondary">Secondary</option>
|
||||
<option value="success">Success</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="danger">Danger</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Type:</label>
|
||||
<select [(ngModel)]="demoType" style="padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; width: 100%;">
|
||||
<option value="determinate">Determinate</option>
|
||||
<option value="indeterminate">Indeterminate</option>
|
||||
<option value="buffer">Buffer</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Options:</label>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.25rem;">
|
||||
<label style="font-size: 0.875rem;">
|
||||
<input type="checkbox" [(ngModel)]="demoStriped" style="margin-right: 0.5rem;">
|
||||
Striped
|
||||
</label>
|
||||
<label style="font-size: 0.875rem;">
|
||||
<input type="checkbox" [(ngModel)]="demoAnimated" style="margin-right: 0.5rem;">
|
||||
Animated
|
||||
</label>
|
||||
<label style="font-size: 0.875rem;">
|
||||
<input type="checkbox" [(ngModel)]="demoShowPercentage" style="margin-right: 0.5rem;">
|
||||
Show Percentage
|
||||
</label>
|
||||
<label style="font-size: 0.875rem;">
|
||||
<input type="checkbox" [(ngModel)]="demoDisabled" style="margin-right: 0.5rem;">
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.5rem; margin-top: 1rem;">
|
||||
<button (click)="startDemo()" style="padding: 0.5rem 1rem; border: 1px solid #007bff; border-radius: 4px; background: #007bff; color: white; cursor: pointer; flex: 1;">
|
||||
Start Demo
|
||||
</button>
|
||||
<button (click)="resetDemo()" style="padding: 0.5rem 1rem; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer; flex: 1;">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Preview -->
|
||||
<div style="flex: 1; min-width: 300px;">
|
||||
<h4>Live Preview:</h4>
|
||||
<div style="padding: 2rem; border: 2px dashed #ccc; border-radius: 8px; background: #fafafa;">
|
||||
<ui-progress-bar
|
||||
[size]="demoSize"
|
||||
[variant]="demoVariant"
|
||||
[progressType]="demoType"
|
||||
[progress]="demoProgress"
|
||||
[buffer]="demoBuffer"
|
||||
[striped]="demoStriped"
|
||||
[animated]="demoAnimated"
|
||||
[showPercentage]="demoShowPercentage"
|
||||
[state]="demoDisabled ? 'disabled' : 'default'"
|
||||
label="Interactive Demo Progress"
|
||||
helperText="Customize the progress bar using the controls">
|
||||
</ui-progress-bar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Usage Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Basic Progress Bar:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-progress-bar
|
||||
label="Download Progress"
|
||||
size="md"
|
||||
variant="primary"
|
||||
[progress]="75"
|
||||
helperText="3 of 4 files downloaded">
|
||||
</ui-progress-bar></code></pre>
|
||||
|
||||
<h4>Indeterminate Progress:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-progress-bar
|
||||
label="Loading..."
|
||||
progressType="indeterminate"
|
||||
variant="primary"
|
||||
[animated]="true"
|
||||
helperText="Please wait while we process your request">
|
||||
</ui-progress-bar></code></pre>
|
||||
|
||||
<h4>Buffer Progress:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-progress-bar
|
||||
label="Video Loading"
|
||||
progressType="buffer"
|
||||
[progress]="currentProgress"
|
||||
[buffer]="bufferProgress"
|
||||
variant="secondary"
|
||||
helperText="Buffering video content">
|
||||
</ui-progress-bar></code></pre>
|
||||
|
||||
<h4>Striped Animated Progress:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-progress-bar
|
||||
label="Upload Progress"
|
||||
[progress]="uploadPercent"
|
||||
variant="success"
|
||||
[striped]="true"
|
||||
[animated]="true"
|
||||
size="lg"
|
||||
helperText="Uploading files...">
|
||||
</ui-progress-bar></code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Programmatic Control Example -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Programmatic Control Example</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<ui-progress-bar
|
||||
#programmableProgress
|
||||
label="File Processing"
|
||||
[progress]="programmableValue()"
|
||||
variant="success"
|
||||
helperText="Processing files..."
|
||||
size="lg">
|
||||
</ui-progress-bar>
|
||||
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||
<button (click)="simulateProgress()" style="padding: 0.5rem 1rem; border: 1px solid #28a745; border-radius: 4px; background: #28a745; color: white; cursor: pointer;">
|
||||
Simulate Progress
|
||||
</button>
|
||||
<button (click)="pauseProgress()" style="padding: 0.5rem 1rem; border: 1px solid #ffc107; border-radius: 4px; background: #ffc107; color: black; cursor: pointer;">
|
||||
Pause
|
||||
</button>
|
||||
<button (click)="resetProgrammable()" style="padding: 0.5rem 1rem; border: 1px solid #6c757d; border-radius: 4px; background: #6c757d; color: white; cursor: pointer;">
|
||||
Reset
|
||||
</button>
|
||||
<button (click)="completeProgress()" style="padding: 0.5rem 1rem; border: 1px solid #007bff; border-radius: 4px; background: #007bff; color: white; cursor: pointer;">
|
||||
Complete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 0.875rem; color: #666;">
|
||||
<strong>Current Progress:</strong> {{ programmableValue() }}% |
|
||||
<strong>Status:</strong> {{ progressStatus() }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(287, 12%, 35%);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid hsl(289, 14%, 90%);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: hsl(286, 20%, 99%);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
select, input[type="checkbox"], input[type="range"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class ProgressDemoComponent {
|
||||
|
||||
// Animated progress values
|
||||
determinateProgress = signal(35);
|
||||
bufferProgress = signal(25);
|
||||
bufferValue = signal(65);
|
||||
|
||||
// Interactive demo properties
|
||||
demoProgress = 60;
|
||||
demoBuffer = 80;
|
||||
demoSize: 'sm' | 'md' | 'lg' = 'md';
|
||||
demoVariant: 'primary' | 'secondary' | 'success' | 'warning' | 'danger' = 'primary';
|
||||
demoType: 'determinate' | 'indeterminate' | 'buffer' = 'determinate';
|
||||
demoStriped = false;
|
||||
demoAnimated = false;
|
||||
demoShowPercentage = true;
|
||||
demoDisabled = false;
|
||||
|
||||
// Programmable progress
|
||||
programmableValue = signal(0);
|
||||
progressStatus = signal('Ready');
|
||||
private progressInterval: any;
|
||||
private isPaused = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
// Start some animated progress bars
|
||||
this.animateProgress();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.progressInterval) {
|
||||
clearInterval(this.progressInterval);
|
||||
}
|
||||
}
|
||||
|
||||
private animateProgress(): void {
|
||||
// Animate the determinate progress
|
||||
setInterval(() => {
|
||||
const current = this.determinateProgress();
|
||||
const next = (current + 1) % 101;
|
||||
this.determinateProgress.set(next);
|
||||
}, 100);
|
||||
|
||||
// Animate the buffer progress
|
||||
setInterval(() => {
|
||||
const currentBuffer = this.bufferValue();
|
||||
const currentProgress = this.bufferProgress();
|
||||
|
||||
const nextBuffer = (currentBuffer + 0.5) % 101;
|
||||
const nextProgress = Math.min(nextBuffer - 20, 100);
|
||||
|
||||
this.bufferValue.set(nextBuffer);
|
||||
this.bufferProgress.set(Math.max(0, nextProgress));
|
||||
}, 150);
|
||||
}
|
||||
|
||||
startDemo(): void {
|
||||
this.demoProgress = 0;
|
||||
let progress = 0;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
progress += Math.random() * 3;
|
||||
this.demoProgress = Math.min(100, progress);
|
||||
|
||||
if (progress >= 100) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
resetDemo(): void {
|
||||
this.demoProgress = 0;
|
||||
this.demoBuffer = 0;
|
||||
this.demoSize = 'md';
|
||||
this.demoVariant = 'primary';
|
||||
this.demoType = 'determinate';
|
||||
this.demoStriped = false;
|
||||
this.demoAnimated = false;
|
||||
this.demoShowPercentage = true;
|
||||
this.demoDisabled = false;
|
||||
}
|
||||
|
||||
simulateProgress(): void {
|
||||
if (this.progressInterval) {
|
||||
clearInterval(this.progressInterval);
|
||||
}
|
||||
|
||||
this.isPaused = false;
|
||||
this.progressStatus.set('Processing...');
|
||||
|
||||
this.progressInterval = setInterval(() => {
|
||||
if (!this.isPaused) {
|
||||
const current = this.programmableValue();
|
||||
if (current < 100) {
|
||||
this.programmableValue.set(current + Math.random() * 2);
|
||||
} else {
|
||||
this.progressStatus.set('Complete!');
|
||||
clearInterval(this.progressInterval);
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
pauseProgress(): void {
|
||||
this.isPaused = !this.isPaused;
|
||||
this.progressStatus.set(this.isPaused ? 'Paused' : 'Processing...');
|
||||
}
|
||||
|
||||
resetProgrammable(): void {
|
||||
if (this.progressInterval) {
|
||||
clearInterval(this.progressInterval);
|
||||
}
|
||||
this.programmableValue.set(0);
|
||||
this.progressStatus.set('Ready');
|
||||
this.isPaused = false;
|
||||
}
|
||||
|
||||
completeProgress(): void {
|
||||
if (this.progressInterval) {
|
||||
clearInterval(this.progressInterval);
|
||||
}
|
||||
this.programmableValue.set(100);
|
||||
this.progressStatus.set('Complete!');
|
||||
this.isPaused = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,665 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
RadioButtonComponent,
|
||||
RadioGroupComponent,
|
||||
RadioButtonData
|
||||
} from '../../../../../ui-essentials/src/lib/components/forms/radio';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-radio-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
RadioButtonComponent,
|
||||
RadioGroupComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Radio Button Component Showcase</h2>
|
||||
|
||||
<!-- Basic Radio Buttons -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Basic Radio Buttons</h3>
|
||||
|
||||
<!-- Individual Radio Buttons -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Individual Radio Buttons</h4>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<ui-radio-button
|
||||
value="option1"
|
||||
label="Option 1"
|
||||
name="basic-group"
|
||||
[(ngModel)]="basicSelection"
|
||||
(selectionChange)="handleSelectionChange('basic', $event)"
|
||||
/>
|
||||
<ui-radio-button
|
||||
value="option2"
|
||||
label="Option 2"
|
||||
name="basic-group"
|
||||
[(ngModel)]="basicSelection"
|
||||
(selectionChange)="handleSelectionChange('basic', $event)"
|
||||
/>
|
||||
<ui-radio-button
|
||||
value="option3"
|
||||
label="Option 3"
|
||||
name="basic-group"
|
||||
[(ngModel)]="basicSelection"
|
||||
(selectionChange)="handleSelectionChange('basic', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Radio Group -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Radio Group</h4>
|
||||
<ui-radio-group
|
||||
label="Choose your preferred option"
|
||||
[options]="basicOptions"
|
||||
name="group-demo"
|
||||
[(ngModel)]="groupSelection"
|
||||
(selectionChange)="handleSelectionChange('group', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Size Variants</h3>
|
||||
|
||||
<!-- Small -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Small (sm)</h4>
|
||||
<ui-radio-group
|
||||
label="Small radio buttons"
|
||||
size="sm"
|
||||
[options]="sizeOptions"
|
||||
name="small-group"
|
||||
[(ngModel)]="sizeSelections['sm']"
|
||||
(selectionChange)="handleSizeSelection('sm', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Medium -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Medium (md) - Default</h4>
|
||||
<ui-radio-group
|
||||
label="Medium radio buttons"
|
||||
size="md"
|
||||
[options]="sizeOptions"
|
||||
name="medium-group"
|
||||
[(ngModel)]="sizeSelections['md']"
|
||||
(selectionChange)="handleSizeSelection('md', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Large -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Large (lg)</h4>
|
||||
<ui-radio-group
|
||||
label="Large radio buttons"
|
||||
size="lg"
|
||||
[options]="sizeOptions"
|
||||
name="large-group"
|
||||
[(ngModel)]="sizeSelections['lg']"
|
||||
(selectionChange)="handleSizeSelection('lg', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Colors -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Color Variants</h3>
|
||||
|
||||
<!-- Primary -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Primary (Default)</h4>
|
||||
<ui-radio-group
|
||||
label="Primary variant"
|
||||
variant="primary"
|
||||
[options]="colorOptions"
|
||||
name="primary-group"
|
||||
[(ngModel)]="variantSelections['primary']"
|
||||
(selectionChange)="handleVariantSelection('primary', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Secondary -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Secondary</h4>
|
||||
<ui-radio-group
|
||||
label="Secondary variant"
|
||||
variant="secondary"
|
||||
[options]="colorOptions"
|
||||
name="secondary-group"
|
||||
[(ngModel)]="variantSelections['secondary']"
|
||||
(selectionChange)="handleVariantSelection('secondary', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Success -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Success</h4>
|
||||
<ui-radio-group
|
||||
label="Success variant"
|
||||
variant="success"
|
||||
[options]="colorOptions"
|
||||
name="success-group"
|
||||
[(ngModel)]="variantSelections['success']"
|
||||
(selectionChange)="handleVariantSelection('success', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Warning -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Warning</h4>
|
||||
<ui-radio-group
|
||||
label="Warning variant"
|
||||
variant="warning"
|
||||
[options]="colorOptions"
|
||||
name="warning-group"
|
||||
[(ngModel)]="variantSelections['warning']"
|
||||
(selectionChange)="handleVariantSelection('warning', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Danger -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Danger</h4>
|
||||
<ui-radio-group
|
||||
label="Danger variant"
|
||||
variant="danger"
|
||||
[options]="colorOptions"
|
||||
name="danger-group"
|
||||
[(ngModel)]="variantSelections['danger']"
|
||||
(selectionChange)="handleVariantSelection('danger', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Orientation -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Orientation</h3>
|
||||
|
||||
<!-- Vertical -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Vertical (Default)</h4>
|
||||
<ui-radio-group
|
||||
label="Vertical orientation"
|
||||
orientation="vertical"
|
||||
[options]="orientationOptions"
|
||||
name="vertical-group"
|
||||
[(ngModel)]="orientationSelections['vertical']"
|
||||
(selectionChange)="handleOrientationSelection('vertical', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Horizontal -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Horizontal</h4>
|
||||
<ui-radio-group
|
||||
label="Horizontal orientation"
|
||||
orientation="horizontal"
|
||||
[options]="orientationOptions"
|
||||
name="horizontal-group"
|
||||
[(ngModel)]="orientationSelections['horizontal']"
|
||||
(selectionChange)="handleOrientationSelection('horizontal', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- With Descriptions -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Radio Buttons with Descriptions</h3>
|
||||
<ui-radio-group
|
||||
label="Choose your subscription plan"
|
||||
[options]="planOptions"
|
||||
name="plan-group"
|
||||
[(ngModel)]="planSelection"
|
||||
(selectionChange)="handleSelectionChange('plan', $event)"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Radio Button States</h3>
|
||||
|
||||
<!-- Disabled Group -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Disabled Group</h4>
|
||||
<ui-radio-group
|
||||
label="Disabled radio group"
|
||||
[options]="stateOptions"
|
||||
name="disabled-group"
|
||||
[disabled]="true"
|
||||
helperText="This entire group is disabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Individual Disabled Options -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Individual Disabled Options</h4>
|
||||
<ui-radio-group
|
||||
label="Some options disabled"
|
||||
[options]="disabledOptions"
|
||||
name="individual-disabled-group"
|
||||
[(ngModel)]="disabledSelection"
|
||||
(selectionChange)="handleSelectionChange('disabled', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Error State</h4>
|
||||
<ui-radio-group
|
||||
label="Required selection"
|
||||
[options]="errorOptions"
|
||||
name="error-group"
|
||||
[required]="true"
|
||||
errorMessage="Please select an option to continue"
|
||||
[(ngModel)]="errorSelection"
|
||||
(selectionChange)="handleSelectionChange('error', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- With Helper Text -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>With Helper Text</h4>
|
||||
<ui-radio-group
|
||||
label="Notification preferences"
|
||||
[options]="notificationOptions"
|
||||
name="notification-group"
|
||||
helperText="Choose how you'd like to receive notifications"
|
||||
[(ngModel)]="notificationSelection"
|
||||
(selectionChange)="handleSelectionChange('notification', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Dense Layout -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Dense Layout</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem;">
|
||||
<!-- Normal Spacing -->
|
||||
<div>
|
||||
<h4>Normal Spacing</h4>
|
||||
<ui-radio-group
|
||||
label="Normal spacing"
|
||||
[options]="denseOptions"
|
||||
name="normal-spacing-group"
|
||||
[(ngModel)]="denseSelections['normal']"
|
||||
(selectionChange)="handleDenseSelection('normal', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Dense Spacing -->
|
||||
<div>
|
||||
<h4>Dense Spacing</h4>
|
||||
<ui-radio-group
|
||||
label="Dense spacing"
|
||||
[options]="denseOptions"
|
||||
name="dense-spacing-group"
|
||||
[dense]="true"
|
||||
[(ngModel)]="denseSelections['dense']"
|
||||
(selectionChange)="handleDenseSelection('dense', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Usage Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Basic Radio Group:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-radio-group
|
||||
label="Choose an option"
|
||||
[options]="options"
|
||||
name="radio-group"
|
||||
[(ngModel)]="selectedValue"
|
||||
(selectionChange)="onSelectionChange($event)">
|
||||
</ui-radio-group></code></pre>
|
||||
|
||||
<h4>Individual Radio Button:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-radio-button
|
||||
value="option1"
|
||||
label="Option 1"
|
||||
name="radio-name"
|
||||
size="md"
|
||||
variant="primary"
|
||||
[(ngModel)]="selectedValue"
|
||||
(selectionChange)="onSelectionChange($event)">
|
||||
</ui-radio-button></code></pre>
|
||||
|
||||
<h4>With Descriptions:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-radio-group
|
||||
label="Subscription Plans"
|
||||
[options]="[
|
||||
{{ '{' }}
|
||||
value: 'basic',
|
||||
label: 'Basic Plan',
|
||||
description: 'Perfect for individuals'
|
||||
{{ '}' }},
|
||||
{{ '{' }}
|
||||
value: 'pro',
|
||||
label: 'Pro Plan',
|
||||
description: 'For small teams'
|
||||
{{ '}' }}
|
||||
]"
|
||||
name="subscription"
|
||||
[(ngModel)]="plan">
|
||||
</ui-radio-group></code></pre>
|
||||
|
||||
<h4>Horizontal Layout:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-radio-group
|
||||
label="Payment Method"
|
||||
[options]="paymentOptions"
|
||||
orientation="horizontal"
|
||||
name="payment"
|
||||
[(ngModel)]="paymentMethod">
|
||||
</ui-radio-group></code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Controls -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Controls</h3>
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<button
|
||||
style="padding: 0.5rem 1rem; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;"
|
||||
(click)="resetAllSelections()"
|
||||
>
|
||||
Reset All Selections
|
||||
</button>
|
||||
<button
|
||||
style="padding: 0.5rem 1rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;"
|
||||
(click)="fillSampleSelections()"
|
||||
>
|
||||
Fill Sample Selections
|
||||
</button>
|
||||
<button
|
||||
style="padding: 0.5rem 1rem; background: #6f42c1; color: white; border: none; border-radius: 4px; cursor: pointer;"
|
||||
(click)="toggleDisabled()"
|
||||
>
|
||||
{{ globalDisabled() ? 'Enable All' : 'Disable All' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (lastSelection()) {
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #e3f2fd; border-radius: 4px;">
|
||||
<strong>Last selection:</strong> {{ lastSelection() }}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Current Values -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Current Values</h3>
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; max-height: 400px; overflow-y: auto;">
|
||||
<pre>{{ getCurrentValues() }}</pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(287, 12%, 35%);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid hsl(289, 14%, 90%);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: hsl(286, 20%, 99%);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
button {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class RadioDemoComponent {
|
||||
// Basic selections
|
||||
basicSelection = '';
|
||||
groupSelection = '';
|
||||
|
||||
// Size selections
|
||||
sizeSelections: Record<string, string> = {
|
||||
'sm': '',
|
||||
'md': '',
|
||||
'lg': ''
|
||||
};
|
||||
|
||||
// Variant selections
|
||||
variantSelections: Record<string, string> = {
|
||||
'primary': '',
|
||||
'secondary': '',
|
||||
'success': '',
|
||||
'warning': '',
|
||||
'danger': ''
|
||||
};
|
||||
|
||||
// Orientation selections
|
||||
orientationSelections: Record<string, string> = {
|
||||
'vertical': '',
|
||||
'horizontal': ''
|
||||
};
|
||||
|
||||
// Other selections
|
||||
planSelection = '';
|
||||
disabledSelection = '';
|
||||
errorSelection = '';
|
||||
notificationSelection = '';
|
||||
|
||||
// Dense selections
|
||||
denseSelections: Record<string, string> = {
|
||||
'normal': '',
|
||||
'dense': ''
|
||||
};
|
||||
|
||||
// Signals for reactive state
|
||||
lastSelection = signal<string>('');
|
||||
globalDisabled = signal<boolean>(false);
|
||||
|
||||
// Option data
|
||||
basicOptions: RadioButtonData[] = [
|
||||
{ value: 'basic1', label: 'Basic Option 1' },
|
||||
{ value: 'basic2', label: 'Basic Option 2' },
|
||||
{ value: 'basic3', label: 'Basic Option 3' }
|
||||
];
|
||||
|
||||
sizeOptions: RadioButtonData[] = [
|
||||
{ value: 'size1', label: 'First Choice' },
|
||||
{ value: 'size2', label: 'Second Choice' },
|
||||
{ value: 'size3', label: 'Third Choice' }
|
||||
];
|
||||
|
||||
colorOptions: RadioButtonData[] = [
|
||||
{ value: 'color1', label: 'Red' },
|
||||
{ value: 'color2', label: 'Blue' },
|
||||
{ value: 'color3', label: 'Green' }
|
||||
];
|
||||
|
||||
orientationOptions: RadioButtonData[] = [
|
||||
{ value: 'orient1', label: 'Option A' },
|
||||
{ value: 'orient2', label: 'Option B' },
|
||||
{ value: 'orient3', label: 'Option C' },
|
||||
{ value: 'orient4', label: 'Option D' }
|
||||
];
|
||||
|
||||
planOptions: RadioButtonData[] = [
|
||||
{
|
||||
value: 'basic',
|
||||
label: 'Basic Plan',
|
||||
description: 'Perfect for individuals and small projects. Includes 5GB storage and basic support.'
|
||||
},
|
||||
{
|
||||
value: 'pro',
|
||||
label: 'Professional Plan',
|
||||
description: 'Great for small teams. Includes 50GB storage, priority support, and advanced features.'
|
||||
},
|
||||
{
|
||||
value: 'enterprise',
|
||||
label: 'Enterprise Plan',
|
||||
description: 'For large organizations. Unlimited storage, 24/7 support, and custom integrations.'
|
||||
}
|
||||
];
|
||||
|
||||
stateOptions: RadioButtonData[] = [
|
||||
{ value: 'state1', label: 'Option 1' },
|
||||
{ value: 'state2', label: 'Option 2' },
|
||||
{ value: 'state3', label: 'Option 3' }
|
||||
];
|
||||
|
||||
disabledOptions: RadioButtonData[] = [
|
||||
{ value: 'available1', label: 'Available Option 1' },
|
||||
{ value: 'disabled1', label: 'Disabled Option', disabled: true },
|
||||
{ value: 'available2', label: 'Available Option 2' },
|
||||
{ value: 'disabled2', label: 'Another Disabled Option', disabled: true }
|
||||
];
|
||||
|
||||
errorOptions: RadioButtonData[] = [
|
||||
{ value: 'error1', label: 'Option 1' },
|
||||
{ value: 'error2', label: 'Option 2' },
|
||||
{ value: 'error3', label: 'Option 3' }
|
||||
];
|
||||
|
||||
notificationOptions: RadioButtonData[] = [
|
||||
{ value: 'email', label: 'Email notifications' },
|
||||
{ value: 'sms', label: 'SMS notifications' },
|
||||
{ value: 'push', label: 'Push notifications' },
|
||||
{ value: 'none', label: 'No notifications' }
|
||||
];
|
||||
|
||||
denseOptions: RadioButtonData[] = [
|
||||
{ value: 'dense1', label: 'Compact Option 1' },
|
||||
{ value: 'dense2', label: 'Compact Option 2' },
|
||||
{ value: 'dense3', label: 'Compact Option 3' },
|
||||
{ value: 'dense4', label: 'Compact Option 4' }
|
||||
];
|
||||
|
||||
handleSelectionChange(group: string, value: string): void {
|
||||
this.lastSelection.set(`${group}: ${value}`);
|
||||
console.log(`Selection changed in ${group}: ${value}`);
|
||||
}
|
||||
|
||||
handleSizeSelection(size: string, value: string): void {
|
||||
this.sizeSelections[size] = value;
|
||||
this.handleSelectionChange(`size-${size}`, value);
|
||||
}
|
||||
|
||||
handleVariantSelection(variant: string, value: string): void {
|
||||
this.variantSelections[variant] = value;
|
||||
this.handleSelectionChange(`variant-${variant}`, value);
|
||||
}
|
||||
|
||||
handleOrientationSelection(orientation: string, value: string): void {
|
||||
this.orientationSelections[orientation] = value;
|
||||
this.handleSelectionChange(`orientation-${orientation}`, value);
|
||||
}
|
||||
|
||||
handleDenseSelection(type: string, value: string): void {
|
||||
this.denseSelections[type] = value;
|
||||
this.handleSelectionChange(`dense-${type}`, value);
|
||||
}
|
||||
|
||||
resetAllSelections(): void {
|
||||
this.basicSelection = '';
|
||||
this.groupSelection = '';
|
||||
this.planSelection = '';
|
||||
this.disabledSelection = '';
|
||||
this.errorSelection = '';
|
||||
this.notificationSelection = '';
|
||||
|
||||
Object.keys(this.sizeSelections).forEach(key => {
|
||||
this.sizeSelections[key] = '';
|
||||
});
|
||||
|
||||
Object.keys(this.variantSelections).forEach(key => {
|
||||
this.variantSelections[key] = '';
|
||||
});
|
||||
|
||||
Object.keys(this.orientationSelections).forEach(key => {
|
||||
this.orientationSelections[key] = '';
|
||||
});
|
||||
|
||||
Object.keys(this.denseSelections).forEach(key => {
|
||||
this.denseSelections[key] = '';
|
||||
});
|
||||
|
||||
this.lastSelection.set('All selections reset');
|
||||
console.log('All selections reset');
|
||||
}
|
||||
|
||||
fillSampleSelections(): void {
|
||||
this.basicSelection = 'option2';
|
||||
this.groupSelection = 'basic2';
|
||||
this.planSelection = 'pro';
|
||||
this.notificationSelection = 'email';
|
||||
this.sizeSelections['md'] = 'size2';
|
||||
this.variantSelections['primary'] = 'color1';
|
||||
this.orientationSelections['horizontal'] = 'orient3';
|
||||
this.denseSelections['dense'] = 'dense2';
|
||||
|
||||
this.lastSelection.set('Sample selections filled');
|
||||
console.log('Sample selections filled');
|
||||
}
|
||||
|
||||
toggleDisabled(): void {
|
||||
this.globalDisabled.set(!this.globalDisabled());
|
||||
this.lastSelection.set(`All controls ${this.globalDisabled() ? 'disabled' : 'enabled'}`);
|
||||
console.log(`Global disabled state: ${this.globalDisabled()}`);
|
||||
}
|
||||
|
||||
getCurrentValues(): string {
|
||||
const values = {
|
||||
basic: this.basicSelection,
|
||||
group: this.groupSelection,
|
||||
plan: this.planSelection,
|
||||
disabled: this.disabledSelection,
|
||||
error: this.errorSelection,
|
||||
notification: this.notificationSelection,
|
||||
sizes: this.sizeSelections,
|
||||
variants: this.variantSelections,
|
||||
orientations: this.orientationSelections,
|
||||
dense: this.denseSelections
|
||||
};
|
||||
|
||||
return JSON.stringify(values, null, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,550 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { SearchBarComponent, SearchSuggestion } from '../../../../../ui-essentials/src/lib/components/forms/search';
|
||||
import {
|
||||
faSearch,
|
||||
faMicrophone,
|
||||
faCamera,
|
||||
faFilter,
|
||||
faUser,
|
||||
faFile,
|
||||
faFolder,
|
||||
faTag,
|
||||
faHeart,
|
||||
faStar
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-search-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
SearchBarComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Search Bar Component Showcase</h2>
|
||||
|
||||
<!-- Basic Search Bars -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Basic Search Bars</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-search-bar
|
||||
placeholder="Search anything..."
|
||||
[(ngModel)]="basicSearch1"
|
||||
(searchChange)="onSearchChange('basic-1', $event)"
|
||||
(searchSubmit)="onSearchSubmit('basic-1', $event)"
|
||||
/>
|
||||
<ui-search-bar
|
||||
placeholder="Search with label..."
|
||||
label="Search"
|
||||
[(ngModel)]="basicSearch2"
|
||||
(searchChange)="onSearchChange('basic-2', $event)"
|
||||
/>
|
||||
<ui-search-bar
|
||||
placeholder="Required search field..."
|
||||
label="Search (Required)"
|
||||
[required]="true"
|
||||
[(ngModel)]="basicSearch3"
|
||||
(searchChange)="onSearchChange('basic-3', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Size Variants</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-search-bar
|
||||
placeholder="Small search..."
|
||||
size="sm"
|
||||
[(ngModel)]="sizeSmall"
|
||||
(searchChange)="onSearchChange('size-sm', $event)"
|
||||
/>
|
||||
<ui-search-bar
|
||||
placeholder="Medium search (default)..."
|
||||
size="md"
|
||||
[(ngModel)]="sizeMedium"
|
||||
(searchChange)="onSearchChange('size-md', $event)"
|
||||
/>
|
||||
<ui-search-bar
|
||||
placeholder="Large search..."
|
||||
size="lg"
|
||||
[(ngModel)]="sizeLarge"
|
||||
(searchChange)="onSearchChange('size-lg', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Styles -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Style Variants</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-search-bar
|
||||
placeholder="Outlined search (default)..."
|
||||
variant="outlined"
|
||||
[(ngModel)]="variantOutlined"
|
||||
(searchChange)="onSearchChange('variant-outlined', $event)"
|
||||
/>
|
||||
<ui-search-bar
|
||||
placeholder="Filled search..."
|
||||
variant="filled"
|
||||
[(ngModel)]="variantFilled"
|
||||
(searchChange)="onSearchChange('variant-filled', $event)"
|
||||
/>
|
||||
<ui-search-bar
|
||||
placeholder="Elevated search..."
|
||||
variant="elevated"
|
||||
[(ngModel)]="variantElevated"
|
||||
(searchChange)="onSearchChange('variant-elevated', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- With Icons -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>With Icons</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-search-bar
|
||||
placeholder="Custom leading icon..."
|
||||
[leadingIcon]="faUser"
|
||||
[(ngModel)]="iconSearch1"
|
||||
(searchChange)="onSearchChange('icon-1', $event)"
|
||||
/>
|
||||
<ui-search-bar
|
||||
placeholder="With trailing icons..."
|
||||
[trailingIcons]="basicTrailingIcons"
|
||||
[(ngModel)]="iconSearch2"
|
||||
(searchChange)="onSearchChange('icon-2', $event)"
|
||||
(trailingIconClick)="onTrailingIconClick($event)"
|
||||
/>
|
||||
<ui-search-bar
|
||||
placeholder="Multiple trailing icons..."
|
||||
[trailingIcons]="multipleTrailingIcons"
|
||||
[(ngModel)]="iconSearch3"
|
||||
(searchChange)="onSearchChange('icon-3', $event)"
|
||||
(trailingIconClick)="onTrailingIconClick($event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- With Suggestions -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>With Suggestions</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-search-bar
|
||||
placeholder="Search with suggestions..."
|
||||
[suggestions]="suggestions"
|
||||
[(ngModel)]="suggestionSearch1"
|
||||
(searchChange)="onSearchChange('suggestions-1', $event)"
|
||||
(suggestionSelect)="onSuggestionSelect($event)"
|
||||
/>
|
||||
<ui-search-bar
|
||||
placeholder="Search with categorized suggestions..."
|
||||
[suggestions]="categorizedSuggestions"
|
||||
[(ngModel)]="suggestionSearch2"
|
||||
(searchChange)="onSearchChange('suggestions-2', $event)"
|
||||
(suggestionSelect)="onSuggestionSelect($event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>States</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-search-bar
|
||||
placeholder="Disabled search..."
|
||||
[disabled]="true"
|
||||
[(ngModel)]="stateDisabled"
|
||||
/>
|
||||
<ui-search-bar
|
||||
placeholder="Readonly search..."
|
||||
[readonly]="true"
|
||||
[(ngModel)]="stateReadonly"
|
||||
/>
|
||||
<ui-search-bar
|
||||
placeholder="Error state..."
|
||||
state="error"
|
||||
errorMessage="Please enter a valid search term"
|
||||
[(ngModel)]="stateError"
|
||||
(searchChange)="onSearchChange('state-error', $event)"
|
||||
/>
|
||||
<ui-search-bar
|
||||
placeholder="With helper text..."
|
||||
helperText="Enter at least 3 characters to search"
|
||||
[(ngModel)]="stateHelper"
|
||||
(searchChange)="onSearchChange('state-helper', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Advanced Features -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Advanced Features</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; margin-bottom: 2rem;">
|
||||
<ui-search-bar
|
||||
placeholder="Non-clearable search..."
|
||||
[clearable]="false"
|
||||
[(ngModel)]="advancedSearch1"
|
||||
(searchChange)="onSearchChange('advanced-1', $event)"
|
||||
/>
|
||||
<ui-search-bar
|
||||
placeholder="Auto-focus search..."
|
||||
[autofocus]="true"
|
||||
[(ngModel)]="advancedSearch2"
|
||||
(searchChange)="onSearchChange('advanced-2', $event)"
|
||||
/>
|
||||
<ui-search-bar
|
||||
placeholder="Search with max suggestions..."
|
||||
[suggestions]="suggestions"
|
||||
[maxSuggestions]="3"
|
||||
[(ngModel)]="advancedSearch3"
|
||||
(searchChange)="onSearchChange('advanced-3', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Controls -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Controls</h3>
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<button
|
||||
style="padding: 0.5rem 1rem; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;"
|
||||
(click)="clearAllSearches()"
|
||||
>
|
||||
Clear All Searches
|
||||
</button>
|
||||
<button
|
||||
style="padding: 0.5rem 1rem; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;"
|
||||
(click)="fillSampleSearches()"
|
||||
>
|
||||
Fill Sample Searches
|
||||
</button>
|
||||
<button
|
||||
style="padding: 0.5rem 1rem; background: #6f42c1; color: white; border: none; border-radius: 4px; cursor: pointer;"
|
||||
(click)="toggleStates()"
|
||||
>
|
||||
{{ globalDisabled() ? 'Enable All' : 'Disable All' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (lastAction()) {
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #e3f2fd; border-radius: 4px;">
|
||||
<strong>Last action:</strong> {{ lastAction() }}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Code Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Code Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">
|
||||
<h4>Basic Usage:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-search-bar
|
||||
placeholder="Search..."
|
||||
[(ngModel)]="searchValue"
|
||||
(searchChange)="onSearchChange($event)"
|
||||
(searchSubmit)="onSearchSubmit($event)">
|
||||
</ui-search-bar></code></pre>
|
||||
|
||||
<h4>With Suggestions:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-search-bar
|
||||
placeholder="Search with suggestions..."
|
||||
[suggestions]="suggestions"
|
||||
[(ngModel)]="searchValue"
|
||||
(suggestionSelect)="onSuggestionSelect($event)">
|
||||
</ui-search-bar></code></pre>
|
||||
|
||||
<h4>With Icons:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-search-bar
|
||||
placeholder="Search with icons..."
|
||||
[leadingIcon]="faSearch"
|
||||
[trailingIcons]="iconButtons"
|
||||
variant="filled"
|
||||
size="lg"
|
||||
(trailingIconClick)="onIconClick($event)">
|
||||
</ui-search-bar></code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Current Values -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Current Values</h3>
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; max-height: 400px; overflow-y: auto;">
|
||||
<pre>{{ getCurrentValues() }}</pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(279, 14%, 35%);
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
code {
|
||||
color: hsl(279, 14%, 15%);
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid #e9ecef;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class SearchDemoComponent {
|
||||
// FontAwesome icons
|
||||
protected readonly faSearch = faSearch;
|
||||
protected readonly faMicrophone = faMicrophone;
|
||||
protected readonly faCamera = faCamera;
|
||||
protected readonly faFilter = faFilter;
|
||||
protected readonly faUser = faUser;
|
||||
protected readonly faFile = faFile;
|
||||
protected readonly faFolder = faFolder;
|
||||
protected readonly faTag = faTag;
|
||||
protected readonly faHeart = faHeart;
|
||||
protected readonly faStar = faStar;
|
||||
|
||||
// Basic search values
|
||||
basicSearch1 = signal<string>('');
|
||||
basicSearch2 = signal<string>('');
|
||||
basicSearch3 = signal<string>('');
|
||||
|
||||
// Size variants
|
||||
sizeSmall = signal<string>('');
|
||||
sizeMedium = signal<string>('');
|
||||
sizeLarge = signal<string>('');
|
||||
|
||||
// Variant styles
|
||||
variantOutlined = signal<string>('');
|
||||
variantFilled = signal<string>('');
|
||||
variantElevated = signal<string>('');
|
||||
|
||||
// Icon searches
|
||||
iconSearch1 = signal<string>('');
|
||||
iconSearch2 = signal<string>('');
|
||||
iconSearch3 = signal<string>('');
|
||||
|
||||
// Suggestion searches
|
||||
suggestionSearch1 = signal<string>('');
|
||||
suggestionSearch2 = signal<string>('');
|
||||
|
||||
// State searches
|
||||
stateDisabled = signal<string>('Disabled value');
|
||||
stateReadonly = signal<string>('Readonly value');
|
||||
stateError = signal<string>('');
|
||||
stateHelper = signal<string>('');
|
||||
|
||||
// Advanced searches
|
||||
advancedSearch1 = signal<string>('');
|
||||
advancedSearch2 = signal<string>('');
|
||||
advancedSearch3 = signal<string>('');
|
||||
|
||||
// Control states
|
||||
globalDisabled = signal<boolean>(false);
|
||||
lastAction = signal<string>('');
|
||||
|
||||
// Icon configurations
|
||||
basicTrailingIcons = [
|
||||
{
|
||||
id: 'mic',
|
||||
icon: faMicrophone,
|
||||
label: 'Voice search',
|
||||
disabled: false
|
||||
}
|
||||
];
|
||||
|
||||
multipleTrailingIcons = [
|
||||
{
|
||||
id: 'mic',
|
||||
icon: faMicrophone,
|
||||
label: 'Voice search',
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
id: 'camera',
|
||||
icon: faCamera,
|
||||
label: 'Image search',
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
id: 'filter',
|
||||
icon: faFilter,
|
||||
label: 'Filter results',
|
||||
disabled: false
|
||||
}
|
||||
];
|
||||
|
||||
// Search suggestions
|
||||
suggestions: SearchSuggestion[] = [
|
||||
{ id: '1', text: 'Angular components', icon: faFile },
|
||||
{ id: '2', text: 'TypeScript interfaces', icon: faFile },
|
||||
{ id: '3', text: 'SCSS mixins', icon: faFile },
|
||||
{ id: '4', text: 'Design tokens', icon: faTag },
|
||||
{ id: '5', text: 'Material Design', icon: faHeart },
|
||||
{ id: '6', text: 'Component library', icon: faFolder },
|
||||
{ id: '7', text: 'UI patterns', icon: faStar },
|
||||
{ id: '8', text: 'Accessibility guidelines', icon: faFile }
|
||||
];
|
||||
|
||||
categorizedSuggestions: SearchSuggestion[] = [
|
||||
{ id: '1', text: 'User profile', category: 'Users', icon: faUser },
|
||||
{ id: '2', text: 'Project documentation', category: 'Files', icon: faFile },
|
||||
{ id: '3', text: 'Design system', category: 'Files', icon: faFolder },
|
||||
{ id: '4', text: 'Component library', category: 'Code', icon: faTag },
|
||||
{ id: '5', text: 'UI components', category: 'Code', icon: faTag },
|
||||
{ id: '6', text: 'Admin settings', category: 'Settings', icon: faFilter }
|
||||
];
|
||||
|
||||
onSearchChange(searchId: string, value: string): void {
|
||||
this.lastAction.set(`Search changed: ${searchId} = "${value}"`);
|
||||
console.log(`Search ${searchId} changed:`, value);
|
||||
}
|
||||
|
||||
onSearchSubmit(searchId: string, value: string): void {
|
||||
this.lastAction.set(`Search submitted: ${searchId} = "${value}"`);
|
||||
console.log(`Search ${searchId} submitted:`, value);
|
||||
}
|
||||
|
||||
onSuggestionSelect(suggestion: SearchSuggestion): void {
|
||||
this.lastAction.set(`Suggestion selected: "${suggestion.text}" (${suggestion.id})`);
|
||||
console.log('Suggestion selected:', suggestion);
|
||||
}
|
||||
|
||||
onTrailingIconClick(iconData: {id: string, icon: any}): void {
|
||||
this.lastAction.set(`Icon clicked: ${iconData.id}`);
|
||||
console.log('Trailing icon clicked:', iconData);
|
||||
}
|
||||
|
||||
clearAllSearches(): void {
|
||||
this.basicSearch1.set('');
|
||||
this.basicSearch2.set('');
|
||||
this.basicSearch3.set('');
|
||||
this.sizeSmall.set('');
|
||||
this.sizeMedium.set('');
|
||||
this.sizeLarge.set('');
|
||||
this.variantOutlined.set('');
|
||||
this.variantFilled.set('');
|
||||
this.variantElevated.set('');
|
||||
this.iconSearch1.set('');
|
||||
this.iconSearch2.set('');
|
||||
this.iconSearch3.set('');
|
||||
this.suggestionSearch1.set('');
|
||||
this.suggestionSearch2.set('');
|
||||
this.stateError.set('');
|
||||
this.stateHelper.set('');
|
||||
this.advancedSearch1.set('');
|
||||
this.advancedSearch2.set('');
|
||||
this.advancedSearch3.set('');
|
||||
|
||||
this.lastAction.set('All searches cleared');
|
||||
}
|
||||
|
||||
fillSampleSearches(): void {
|
||||
this.basicSearch1.set('Sample search');
|
||||
this.basicSearch2.set('Labeled search');
|
||||
this.basicSearch3.set('Required search');
|
||||
this.sizeSmall.set('Small');
|
||||
this.sizeMedium.set('Medium');
|
||||
this.sizeLarge.set('Large');
|
||||
this.variantOutlined.set('Outlined');
|
||||
this.variantFilled.set('Filled');
|
||||
this.variantElevated.set('Elevated');
|
||||
this.iconSearch1.set('User search');
|
||||
this.iconSearch2.set('Voice search');
|
||||
this.iconSearch3.set('Multi-icon search');
|
||||
this.suggestionSearch1.set('Angular');
|
||||
this.suggestionSearch2.set('Design');
|
||||
this.stateError.set('Error text');
|
||||
this.stateHelper.set('Helper text');
|
||||
this.advancedSearch1.set('Advanced');
|
||||
this.advancedSearch2.set('Focused');
|
||||
this.advancedSearch3.set('Limited');
|
||||
|
||||
this.lastAction.set('All searches filled with sample data');
|
||||
}
|
||||
|
||||
toggleStates(): void {
|
||||
this.globalDisabled.update(disabled => !disabled);
|
||||
this.lastAction.set(`Global state: ${this.globalDisabled() ? 'disabled' : 'enabled'}`);
|
||||
}
|
||||
|
||||
getCurrentValues(): string {
|
||||
const values = {
|
||||
basic: {
|
||||
search1: this.basicSearch1(),
|
||||
search2: this.basicSearch2(),
|
||||
search3: this.basicSearch3()
|
||||
},
|
||||
sizes: {
|
||||
small: this.sizeSmall(),
|
||||
medium: this.sizeMedium(),
|
||||
large: this.sizeLarge()
|
||||
},
|
||||
variants: {
|
||||
outlined: this.variantOutlined(),
|
||||
filled: this.variantFilled(),
|
||||
elevated: this.variantElevated()
|
||||
},
|
||||
icons: {
|
||||
icon1: this.iconSearch1(),
|
||||
icon2: this.iconSearch2(),
|
||||
icon3: this.iconSearch3()
|
||||
},
|
||||
suggestions: {
|
||||
suggestion1: this.suggestionSearch1(),
|
||||
suggestion2: this.suggestionSearch2()
|
||||
},
|
||||
states: {
|
||||
disabled: this.stateDisabled(),
|
||||
readonly: this.stateReadonly(),
|
||||
error: this.stateError(),
|
||||
helper: this.stateHelper()
|
||||
},
|
||||
advanced: {
|
||||
advanced1: this.advancedSearch1(),
|
||||
advanced2: this.advancedSearch2(),
|
||||
advanced3: this.advancedSearch3()
|
||||
},
|
||||
controls: {
|
||||
globalDisabled: this.globalDisabled(),
|
||||
lastAction: this.lastAction()
|
||||
}
|
||||
};
|
||||
|
||||
return JSON.stringify(values, null, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
.demo-container {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
color: #1f2937;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-subsection {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
h4 {
|
||||
font-size: 1.125rem;
|
||||
color: #4b5563;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.demo-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
background: #f9fafb;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
|
||||
label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-interactive {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
|
||||
.toggle-button {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #2563eb;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
|
||||
select {
|
||||
background: #ffffff;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
color: #1f2937;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
min-height: 400px;
|
||||
|
||||
.real-content {
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
color: #1f2937;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 0.875rem;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: auto;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
color: #1f2937;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.best-practices {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
font-size: 1rem;
|
||||
color: #1f2937;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-left: 1rem;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #10b981;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #1f2937;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-examples {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
|
||||
.code-example {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
|
||||
h4 {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #ffffff;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
|
||||
code {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.875rem;
|
||||
color: #1f2937;
|
||||
white-space: pre;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-interactive .demo-controls {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.code-examples {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode adjustments (if needed in the future)
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.demo-item {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
|
||||
label {
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-interactive {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
|
||||
.demo-controls label select {
|
||||
background: #4b5563;
|
||||
border-color: #6b7280;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
}
|
||||
|
||||
.best-practices {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
|
||||
ul li {
|
||||
color: #f3f4f6;
|
||||
|
||||
strong {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-examples .code-example {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
|
||||
h4 {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #4b5563;
|
||||
|
||||
code {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section h3 {
|
||||
color: #f3f4f6;
|
||||
border-color: #6b7280;
|
||||
}
|
||||
|
||||
.demo-subsection h4 {
|
||||
color: #d1d5db;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { SKELETON_COMPONENTS } from '../../../../../ui-essentials/src/lib/components/feedback/skeleton-loader';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-skeleton-loader-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, ...SKELETON_COMPONENTS],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Skeleton Loader Demo</h2>
|
||||
<p>Skeleton loaders provide visual placeholders while content is loading, improving perceived performance and user experience.</p>
|
||||
|
||||
<!-- Basic Sizes -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-item">
|
||||
<label>{{ size }}</label>
|
||||
<ui-skeleton-loader [size]="size" shape="text"></ui-skeleton-loader>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Shapes -->
|
||||
<section class="demo-section">
|
||||
<h3>Shapes</h3>
|
||||
<div class="demo-grid">
|
||||
@for (shape of shapes; track shape) {
|
||||
<div class="demo-item">
|
||||
<label>{{ shape }}</label>
|
||||
<ui-skeleton-loader [shape]="shape" size="md"></ui-skeleton-loader>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Animations -->
|
||||
<section class="demo-section">
|
||||
<h3>Animation Types</h3>
|
||||
<div class="demo-row">
|
||||
@for (animation of animations; track animation) {
|
||||
<div class="demo-item">
|
||||
<label>{{ animation }}</label>
|
||||
<ui-skeleton-loader
|
||||
shape="text"
|
||||
[animation]="animation"
|
||||
size="md">
|
||||
</ui-skeleton-loader>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Width Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Width Variants</h3>
|
||||
<div class="demo-column">
|
||||
@for (width of widths; track width) {
|
||||
<div class="demo-item">
|
||||
<label>{{ width }}</label>
|
||||
<ui-skeleton-loader
|
||||
shape="text"
|
||||
[width]="width"
|
||||
size="md">
|
||||
</ui-skeleton-loader>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pre-built Components -->
|
||||
<section class="demo-section">
|
||||
<h3>Pre-built Components</h3>
|
||||
|
||||
<!-- Text Block -->
|
||||
<div class="demo-subsection">
|
||||
<h4>Text Block</h4>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<label>3 lines (default)</label>
|
||||
<ui-skeleton-text-block></ui-skeleton-text-block>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<label>5 lines</label>
|
||||
<ui-skeleton-text-block [lines]="5"></ui-skeleton-text-block>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<label>Custom last line width</label>
|
||||
<ui-skeleton-text-block [lines]="4" lastLineWidth="w-25"></ui-skeleton-text-block>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile -->
|
||||
<div class="demo-subsection">
|
||||
<h4>Profile</h4>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<label>Default</label>
|
||||
<ui-skeleton-profile></ui-skeleton-profile>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<label>Small Avatar</label>
|
||||
<ui-skeleton-profile avatarSize="sm"></ui-skeleton-profile>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<label>No Extra Info</label>
|
||||
<ui-skeleton-profile [showExtraInfo]="false"></ui-skeleton-profile>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cards -->
|
||||
<div class="demo-subsection">
|
||||
<h4>Card</h4>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<label>Full Card</label>
|
||||
<ui-skeleton-card></ui-skeleton-card>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<label>No Image</label>
|
||||
<ui-skeleton-card [showImage]="false"></ui-skeleton-card>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<label>No Actions</label>
|
||||
<ui-skeleton-card [showActions]="false"></ui-skeleton-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Article -->
|
||||
<div class="demo-subsection">
|
||||
<h4>Article</h4>
|
||||
<div class="demo-item">
|
||||
<label>Full Article</label>
|
||||
<ui-skeleton-article></ui-skeleton-article>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<label>Article without Image</label>
|
||||
<ui-skeleton-article [showImage]="false"></ui-skeleton-article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table Rows -->
|
||||
<div class="demo-subsection">
|
||||
<h4>Table Rows</h4>
|
||||
<div class="demo-column">
|
||||
<label>Table Skeleton (3 rows)</label>
|
||||
@for (row of [1,2,3]; track row) {
|
||||
<ui-skeleton-table-row [columns]="4"></ui-skeleton-table-row>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Custom Groups -->
|
||||
<section class="demo-section">
|
||||
<h3>Skeleton Groups</h3>
|
||||
|
||||
<div class="demo-subsection">
|
||||
<h4>Vertical Group</h4>
|
||||
<div class="demo-item">
|
||||
<ui-skeleton-group>
|
||||
<ui-skeleton-loader shape="heading" size="lg" width="w-75"></ui-skeleton-loader>
|
||||
<ui-skeleton-loader shape="text" width="w-full"></ui-skeleton-loader>
|
||||
<ui-skeleton-loader shape="text" width="w-50"></ui-skeleton-loader>
|
||||
</ui-skeleton-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-subsection">
|
||||
<h4>Horizontal Group</h4>
|
||||
<div class="demo-item">
|
||||
<ui-skeleton-group variant="horizontal">
|
||||
<ui-skeleton-loader shape="avatar" size="md"></ui-skeleton-loader>
|
||||
<ui-skeleton-loader shape="button" customWidth="100px"></ui-skeleton-loader>
|
||||
<ui-skeleton-loader shape="button" customWidth="80px"></ui-skeleton-loader>
|
||||
</ui-skeleton-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-subsection">
|
||||
<h4>Grid Group</h4>
|
||||
<div class="demo-item">
|
||||
<ui-skeleton-group variant="grid">
|
||||
<ui-skeleton-loader shape="card"></ui-skeleton-loader>
|
||||
<ui-skeleton-loader shape="card"></ui-skeleton-loader>
|
||||
<ui-skeleton-loader shape="card"></ui-skeleton-loader>
|
||||
<ui-skeleton-loader shape="card"></ui-skeleton-loader>
|
||||
</ui-skeleton-group>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Demo -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Loading Simulation</h3>
|
||||
<div class="demo-interactive">
|
||||
<div class="demo-controls">
|
||||
<button (click)="toggleLoading()" class="toggle-button">
|
||||
{{ isLoading ? 'Show Content' : 'Show Loading' }}
|
||||
</button>
|
||||
<label>
|
||||
Animation:
|
||||
<select [(ngModel)]="selectedAnimation" (change)="onAnimationChange()">
|
||||
@for (animation of animations; track animation) {
|
||||
<option [value]="animation">{{ animation }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
@if (isLoading) {
|
||||
<!-- Loading state with selected animation -->
|
||||
<ui-skeleton-article
|
||||
[animation]="selectedAnimation"
|
||||
[showImage]="true">
|
||||
</ui-skeleton-article>
|
||||
} @else {
|
||||
<!-- Actual content -->
|
||||
<article class="real-content">
|
||||
<h3>Sample Article Title</h3>
|
||||
<p class="meta">Published on January 15, 2024 by John Doe</p>
|
||||
<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZGRkIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPjQwMCB4IDIwMDwvdGV4dD48L3N2Zz4=" alt="Sample image" />
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
||||
<p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
|
||||
<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Best Practices -->
|
||||
<section class="demo-section">
|
||||
<h3>Best Practices</h3>
|
||||
<div class="best-practices">
|
||||
<ul>
|
||||
<li><strong>Match Content Structure:</strong> Skeleton shapes should closely match the final content layout</li>
|
||||
<li><strong>Use Appropriate Animations:</strong> Shimmer for fast loading, pulse for slower operations</li>
|
||||
<li><strong>Respect Accessibility:</strong> Include proper ARIA labels and roles</li>
|
||||
<li><strong>Consider Performance:</strong> Reduce animation complexity on low-performance devices</li>
|
||||
<li><strong>Maintain Consistency:</strong> Use consistent skeleton patterns across your application</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Code Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Code Examples</h3>
|
||||
<div class="code-examples">
|
||||
<div class="code-example">
|
||||
<h4>Basic Skeleton</h4>
|
||||
<pre><code><ui-skeleton-loader shape="text" size="md"></ui-skeleton-loader></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
<h4>Profile Skeleton</h4>
|
||||
<pre><code><ui-skeleton-profile avatarSize="lg"></ui-skeleton-profile></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
<h4>Custom Group</h4>
|
||||
<pre><code><ui-skeleton-group variant="horizontal">
|
||||
<ui-skeleton-loader shape="avatar" size="sm"></ui-skeleton-loader>
|
||||
<ui-skeleton-loader shape="text" width="w-75"></ui-skeleton-loader>
|
||||
</ui-skeleton-group></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './skeleton-loader-demo.component.scss'
|
||||
})
|
||||
export class SkeletonLoaderDemoComponent {
|
||||
sizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
|
||||
shapes = ['text', 'heading', 'avatar', 'card', 'button', 'image', 'circle', 'rounded', 'square'] as const;
|
||||
animations = ['shimmer', 'pulse', 'wave'] as const;
|
||||
widths = ['w-25', 'w-50', 'w-75', 'w-full'] as const;
|
||||
|
||||
isLoading = true;
|
||||
selectedAnimation = 'shimmer' as any;
|
||||
|
||||
toggleLoading(): void {
|
||||
this.isLoading = !this.isLoading;
|
||||
}
|
||||
|
||||
onAnimationChange(): void {
|
||||
// Force re-render by toggling loading state
|
||||
if (this.isLoading) {
|
||||
this.isLoading = false;
|
||||
setTimeout(() => {
|
||||
this.isLoading = true;
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic' as tokens;
|
||||
|
||||
.demo-container {
|
||||
padding: tokens.$semantic-spacing-layout-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: tokens.$semantic-spacing-layout-lg;
|
||||
|
||||
h3 {
|
||||
margin-bottom: tokens.$semantic-spacing-component-md;
|
||||
color: tokens.$semantic-color-text-primary;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: tokens.$semantic-spacing-component-sm;
|
||||
color: tokens.$semantic-color-text-secondary;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
color: tokens.$semantic-color-text-secondary;
|
||||
margin-bottom: tokens.$semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: tokens.$semantic-spacing-layout-md;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: tokens.$semantic-spacing-layout-md;
|
||||
}
|
||||
|
||||
.demo-spacer-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 200px;
|
||||
margin-bottom: tokens.$semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-direction-container {
|
||||
padding: tokens.$semantic-spacing-component-md;
|
||||
border: 1px solid tokens.$semantic-color-border-subtle;
|
||||
border-radius: tokens.$semantic-border-radius-md;
|
||||
}
|
||||
|
||||
.demo-horizontal-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 2px dashed tokens.$semantic-color-border-subtle;
|
||||
border-radius: tokens.$semantic-border-radius-sm;
|
||||
padding: tokens.$semantic-spacing-component-sm;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.demo-vertical-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 2px dashed tokens.$semantic-color-border-subtle;
|
||||
border-radius: tokens.$semantic-border-radius-sm;
|
||||
padding: tokens.$semantic-spacing-component-sm;
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.demo-flex-container {
|
||||
margin-bottom: tokens.$semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-flex-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 2px dashed tokens.$semantic-color-border-subtle;
|
||||
border-radius: tokens.$semantic-border-radius-sm;
|
||||
padding: tokens.$semantic-spacing-component-sm;
|
||||
min-height: 60px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.demo-flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 2px dashed tokens.$semantic-color-border-subtle;
|
||||
border-radius: tokens.$semantic-border-radius-sm;
|
||||
padding: tokens.$semantic-spacing-component-sm;
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.demo-box {
|
||||
background: tokens.$semantic-color-container-primary;
|
||||
color: tokens.$semantic-color-on-container-primary;
|
||||
padding: tokens.$semantic-spacing-component-sm;
|
||||
border-radius: tokens.$semantic-border-radius-sm;
|
||||
font-weight: 500;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.demo-label {
|
||||
font-size: 0.875rem;
|
||||
color: tokens.$semantic-color-text-secondary;
|
||||
margin-bottom: tokens.$semantic-spacing-component-xs;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background: tokens.$semantic-color-surface-primary;
|
||||
border: 1px solid tokens.$semantic-color-border-subtle;
|
||||
border-radius: tokens.$semantic-border-radius-md;
|
||||
padding: tokens.$semantic-spacing-component-lg;
|
||||
max-width: 400px;
|
||||
|
||||
.demo-card-header {
|
||||
font-weight: 600;
|
||||
color: tokens.$semantic-color-text-primary;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.demo-card-content {
|
||||
color: tokens.$semantic-color-text-secondary;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.demo-card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
background: tokens.$semantic-color-surface-secondary;
|
||||
color: tokens.$semantic-color-text-primary;
|
||||
border: 1px solid tokens.$semantic-color-border-subtle;
|
||||
border-radius: tokens.$semantic-border-radius-sm;
|
||||
padding: tokens.$semantic-spacing-component-sm tokens.$semantic-spacing-component-md;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
|
||||
&--primary {
|
||||
background: tokens.$semantic-color-container-primary;
|
||||
color: tokens.$semantic-color-on-container-primary;
|
||||
border-color: tokens.$semantic-color-container-primary;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-spacer-container {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SpacerComponent } from '../../../../../ui-essentials/src/lib/components/layout';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-spacer-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, SpacerComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Spacer Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
<p>Spacers with debug mode enabled to show spacing</p>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-spacer-container">
|
||||
<div class="demo-label">{{ size }}</div>
|
||||
<div class="demo-box">A</div>
|
||||
<ui-spacer [size]="size" [debug]="true"></ui-spacer>
|
||||
<div class="demo-box">B</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Directional Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Directional Spacing</h3>
|
||||
|
||||
<div class="demo-grid">
|
||||
<div class="demo-direction-container">
|
||||
<h4>Horizontal Spacing</h4>
|
||||
<div class="demo-horizontal-container">
|
||||
<div class="demo-box">Left</div>
|
||||
<ui-spacer size="lg" direction="horizontal" [debug]="true"></ui-spacer>
|
||||
<div class="demo-box">Right</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-direction-container">
|
||||
<h4>Vertical Spacing</h4>
|
||||
<div class="demo-vertical-container">
|
||||
<div class="demo-box">Top</div>
|
||||
<ui-spacer size="lg" direction="vertical" [debug]="true"></ui-spacer>
|
||||
<div class="demo-box">Bottom</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Component Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Component Spacing Variants</h3>
|
||||
<p>Semantic spacing tokens for different component contexts</p>
|
||||
<div class="demo-row">
|
||||
@for (variant of componentVariants; track variant) {
|
||||
<div class="demo-spacer-container">
|
||||
<div class="demo-label">{{ variant }}</div>
|
||||
<div class="demo-box">A</div>
|
||||
<ui-spacer [variant]="variant" [debug]="true"></ui-spacer>
|
||||
<div class="demo-box">B</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Layout Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Layout Spacing Variants</h3>
|
||||
<p>Semantic spacing tokens for layout elements</p>
|
||||
<div class="demo-row">
|
||||
@for (variant of layoutVariants; track variant) {
|
||||
<div class="demo-spacer-container">
|
||||
<div class="demo-label">{{ variant }}</div>
|
||||
<div class="demo-box">Section A</div>
|
||||
<ui-spacer [variant]="variant" [debug]="true"></ui-spacer>
|
||||
<div class="demo-box">Section B</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Flexible Spacer -->
|
||||
<section class="demo-section">
|
||||
<h3>Flexible Spacer</h3>
|
||||
<p>Spacer that grows to fill available space</p>
|
||||
|
||||
<div class="demo-flex-container">
|
||||
<h4>Horizontal Flexible</h4>
|
||||
<div class="demo-flex-row">
|
||||
<div class="demo-box">Left</div>
|
||||
<ui-spacer direction="horizontal" [flexible]="true" [debug]="true"></ui-spacer>
|
||||
<div class="demo-box">Right</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-flex-container">
|
||||
<h4>Vertical Flexible</h4>
|
||||
<div class="demo-flex-column">
|
||||
<div class="demo-box">Top</div>
|
||||
<ui-spacer direction="vertical" [flexible]="true" [debug]="true"></ui-spacer>
|
||||
<div class="demo-box">Bottom</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Custom Spacing -->
|
||||
<section class="demo-section">
|
||||
<h3>Custom Spacing</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-spacer-container">
|
||||
<div class="demo-label">Custom Width: 100px</div>
|
||||
<div class="demo-horizontal-container">
|
||||
<div class="demo-box">A</div>
|
||||
<ui-spacer customWidth="100px" direction="horizontal" [debug]="true"></ui-spacer>
|
||||
<div class="demo-box">B</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-spacer-container">
|
||||
<div class="demo-label">Custom Height: 80px</div>
|
||||
<div class="demo-vertical-container">
|
||||
<div class="demo-box">Top</div>
|
||||
<ui-spacer customHeight="80px" direction="vertical" [debug]="true"></ui-spacer>
|
||||
<div class="demo-box">Bottom</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Real World Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Real World Usage</h3>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Card Layout with Spacers</h4>
|
||||
<div class="demo-card-header">Header Content</div>
|
||||
<ui-spacer variant="component-md"></ui-spacer>
|
||||
<div class="demo-card-content">Main content goes here with proper spacing</div>
|
||||
<ui-spacer variant="component-sm"></ui-spacer>
|
||||
<div class="demo-card-actions">
|
||||
<button class="demo-button">Cancel</button>
|
||||
<ui-spacer direction="horizontal" [flexible]="true"></ui-spacer>
|
||||
<button class="demo-button demo-button--primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './spacer-demo.component.scss'
|
||||
})
|
||||
export class SpacerDemoComponent {
|
||||
sizes: ('xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl')[] = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl'];
|
||||
componentVariants: ('component-xs' | 'component-sm' | 'component-md' | 'component-lg' | 'component-xl')[] = [
|
||||
'component-xs', 'component-sm', 'component-md', 'component-lg', 'component-xl'
|
||||
];
|
||||
layoutVariants: ('layout-xs' | 'layout-sm' | 'layout-md' | 'layout-lg' | 'layout-xl')[] = [
|
||||
'layout-xs', 'layout-sm', 'layout-md', 'layout-lg', 'layout-xl'
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators, FormsModule } from '@angular/forms';
|
||||
import { SwitchComponent } from '../../../../../ui-essentials/src/lib/components/forms/switch';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-switch-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
SwitchComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Switch Component Showcase</h2>
|
||||
|
||||
<!-- Basic Switches -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Basic Switches</h3>
|
||||
<div style="display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-switch size="sm" label="Small" (switchChange)="handleToggle('small', $event)"></ui-switch>
|
||||
<ui-switch size="md" label="Medium" (switchChange)="handleToggle('medium', $event)"></ui-switch>
|
||||
<ui-switch size="lg" label="Large" (switchChange)="handleToggle('large', $event)"></ui-switch>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Switch Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Variants</h3>
|
||||
<div style="display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-switch variant="primary" label="Primary" (switchChange)="handleToggle('primary', $event)"></ui-switch>
|
||||
<ui-switch variant="secondary" label="Secondary" (switchChange)="handleToggle('secondary', $event)"></ui-switch>
|
||||
<ui-switch variant="success" label="Success" (switchChange)="handleToggle('success', $event)"></ui-switch>
|
||||
<ui-switch variant="warning" label="Warning" (switchChange)="handleToggle('warning', $event)"></ui-switch>
|
||||
<ui-switch variant="danger" label="Danger" (switchChange)="handleToggle('danger', $event)"></ui-switch>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Switch States -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>States</h3>
|
||||
<div style="display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<ui-switch label="Enabled" (switchChange)="handleToggle('enabled', $event)"></ui-switch>
|
||||
<ui-switch label="Disabled" [disabled]="true" (switchChange)="handleToggle('disabled', $event)"></ui-switch>
|
||||
<ui-switch label="Pre-checked" [(ngModel)]="preCheckedValue" (switchChange)="handleToggle('pre-checked', $event)"></ui-switch>
|
||||
<ui-switch label="Required" [required]="true" (switchChange)="handleToggle('required', $event)"></ui-switch>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Comparison -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Size Comparison</h3>
|
||||
<div style="display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; margin-bottom: 2rem;">
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; align-items: center;">
|
||||
<ui-switch size="sm" variant="primary" label="Small Primary" (switchChange)="handleToggle('sm-primary', $event)"></ui-switch>
|
||||
<ui-switch size="md" variant="primary" label="Medium Primary" (switchChange)="handleToggle('md-primary', $event)"></ui-switch>
|
||||
<ui-switch size="lg" variant="primary" label="Large Primary" (switchChange)="handleToggle('lg-primary', $event)"></ui-switch>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; align-items: center;">
|
||||
<ui-switch size="sm" variant="success" label="Small Success" (switchChange)="handleToggle('sm-success', $event)"></ui-switch>
|
||||
<ui-switch size="md" variant="success" label="Medium Success" (switchChange)="handleToggle('md-success', $event)"></ui-switch>
|
||||
<ui-switch size="lg" variant="success" label="Large Success" (switchChange)="handleToggle('lg-success', $event)"></ui-switch>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; align-items: center;">
|
||||
<ui-switch size="sm" variant="danger" label="Small Danger" (switchChange)="handleToggle('sm-danger', $event)"></ui-switch>
|
||||
<ui-switch size="md" variant="danger" label="Medium Danger" (switchChange)="handleToggle('md-danger', $event)"></ui-switch>
|
||||
<ui-switch size="lg" variant="danger" label="Large Danger" (switchChange)="handleToggle('lg-danger', $event)"></ui-switch>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Switches without Labels -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Switches without Labels</h3>
|
||||
<div style="display: flex; align-items: center; gap: 2rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem; align-items: center;">
|
||||
<ui-switch size="sm" variant="primary" ariaLabel="Small toggle" (switchChange)="handleToggle('no-label-sm', $event)"></ui-switch>
|
||||
<span style="font-size: 12px; color: #666;">Small</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem; align-items: center;">
|
||||
<ui-switch size="md" variant="secondary" ariaLabel="Medium toggle" (switchChange)="handleToggle('no-label-md', $event)"></ui-switch>
|
||||
<span style="font-size: 12px; color: #666;">Medium</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem; align-items: center;">
|
||||
<ui-switch size="lg" variant="success" ariaLabel="Large toggle" (switchChange)="handleToggle('no-label-lg', $event)"></ui-switch>
|
||||
<span style="font-size: 12px; color: #666;">Large</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Switches with Helper Text -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>With Helper Text</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 1.5rem;">
|
||||
<ui-switch
|
||||
label="Email Notifications"
|
||||
helperText="Receive email updates about your account activity"
|
||||
(switchChange)="handleToggle('email-notifications', $event)">
|
||||
</ui-switch>
|
||||
<ui-switch
|
||||
label="Dark Mode"
|
||||
helperText="Switch to dark theme for better viewing in low light"
|
||||
variant="secondary"
|
||||
(switchChange)="handleToggle('dark-mode', $event)">
|
||||
</ui-switch>
|
||||
<ui-switch
|
||||
label="Auto-save"
|
||||
helperText="Automatically save your work every 30 seconds"
|
||||
variant="success"
|
||||
[(ngModel)]="autoSaveValue"
|
||||
(switchChange)="handleToggle('auto-save', $event)">
|
||||
</ui-switch>
|
||||
<ui-switch
|
||||
label="Data Sharing"
|
||||
helperText="Share anonymous usage data to help improve our services"
|
||||
variant="warning"
|
||||
(switchChange)="handleToggle('data-sharing', $event)">
|
||||
</ui-switch>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Reactive Forms Integration -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Reactive Forms Integration</h3>
|
||||
<form [formGroup]="settingsForm" style="display: flex; flex-direction: column; gap: 1.5rem; max-width: 400px;">
|
||||
<ui-switch
|
||||
formControlName="notifications"
|
||||
label="Push Notifications"
|
||||
helperText="Receive push notifications on your device">
|
||||
</ui-switch>
|
||||
<ui-switch
|
||||
formControlName="newsletter"
|
||||
label="Newsletter Subscription"
|
||||
variant="secondary"
|
||||
helperText="Subscribe to our weekly newsletter">
|
||||
</ui-switch>
|
||||
<ui-switch
|
||||
formControlName="analytics"
|
||||
label="Analytics Tracking"
|
||||
variant="warning"
|
||||
[required]="true"
|
||||
helperText="Required for core functionality">
|
||||
</ui-switch>
|
||||
<ui-switch
|
||||
formControlName="marketing"
|
||||
label="Marketing Emails"
|
||||
variant="danger"
|
||||
helperText="Receive promotional offers and updates">
|
||||
</ui-switch>
|
||||
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 4px;">
|
||||
<h4>Form Values:</h4>
|
||||
<pre style="font-size: 0.875rem; margin: 0;">{{ getFormValues() }}</pre>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<button type="button" (click)="resetForm()" style="padding: 0.5rem 1rem; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;">
|
||||
Reset Form
|
||||
</button>
|
||||
<button type="button" (click)="toggleAllSwitches()" style="padding: 0.5rem 1rem; border: 1px solid #007bff; border-radius: 4px; background: #007bff; color: white; cursor: pointer;">
|
||||
Toggle All
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Usage Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Basic Switch:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-switch
|
||||
label="Enable feature"
|
||||
size="md"
|
||||
variant="primary"
|
||||
(switchChange)="onToggle($event)">
|
||||
</ui-switch></code></pre>
|
||||
|
||||
<h4>Switch with Helper Text:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-switch
|
||||
label="Email Notifications"
|
||||
helperText="Receive updates via email"
|
||||
variant="success"
|
||||
[ngModel]="emailEnabled"
|
||||
(switchChange)="toggleEmail($event)">
|
||||
</ui-switch></code></pre>
|
||||
|
||||
<h4>Reactive Form Switch:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-switch
|
||||
formControlName="agreement"
|
||||
label="I agree to terms"
|
||||
variant="primary"
|
||||
[required]="true">
|
||||
</ui-switch></code></pre>
|
||||
|
||||
<h4>Switch without Label:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-switch
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
ariaLabel="Toggle feature"
|
||||
(switchChange)="handleToggle($event)">
|
||||
</ui-switch></code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Demo</h3>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 2rem; margin-bottom: 2rem;">
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Size:</label>
|
||||
<select [(ngModel)]="demoSizeValue" style="padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px;">
|
||||
<option value="sm">Small</option>
|
||||
<option value="md">Medium</option>
|
||||
<option value="lg">Large</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Variant:</label>
|
||||
<select [(ngModel)]="demoVariantValue" style="padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px;">
|
||||
<option value="primary">Primary</option>
|
||||
<option value="secondary">Secondary</option>
|
||||
<option value="success">Success</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="danger">Danger</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">Options:</label>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.25rem;">
|
||||
<label style="font-size: 0.875rem;">
|
||||
<input type="checkbox" [(ngModel)]="demoDisabledValue" style="margin-right: 0.5rem;">
|
||||
Disabled
|
||||
</label>
|
||||
<label style="font-size: 0.875rem;">
|
||||
<input type="checkbox" [(ngModel)]="demoRequiredValue" style="margin-right: 0.5rem;">
|
||||
Required
|
||||
</label>
|
||||
<label style="font-size: 0.875rem;">
|
||||
<input type="checkbox" [(ngModel)]="demoWithHelperValue" style="margin-right: 0.5rem;">
|
||||
With Helper Text
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 2rem; border: 2px dashed #ccc; border-radius: 8px; background: #fafafa;">
|
||||
<h4>Live Preview:</h4>
|
||||
<ui-switch
|
||||
[size]="demoSizeValue"
|
||||
[variant]="demoVariantValue"
|
||||
[disabled]="demoDisabledValue"
|
||||
[required]="demoRequiredValue"
|
||||
[helperText]="demoWithHelperValue ? 'This is example helper text for the switch component.' : ''"
|
||||
label="Interactive Demo Switch"
|
||||
[(ngModel)]="demoCheckedValue"
|
||||
(switchChange)="handleDemoToggle($event)">
|
||||
</ui-switch>
|
||||
|
||||
<div style="margin-top: 1rem; font-size: 0.875rem; color: #666;">
|
||||
<strong>Current state:</strong> {{ demoCheckedValue ? 'ON' : 'OFF' }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Last Action Display -->
|
||||
@if (lastAction()) {
|
||||
<section style="margin-bottom: 2rem;">
|
||||
<div style="padding: 1rem; background: #e3f2fd; border-radius: 4px; border-left: 4px solid #2196f3;">
|
||||
<strong>Last Action:</strong> {{ lastAction() }}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(287, 12%, 35%);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid hsl(289, 14%, 90%);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: hsl(286, 20%, 99%);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
select, input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class SwitchDemoComponent {
|
||||
lastAction = signal<string>('');
|
||||
|
||||
// Template values for ngModel binding
|
||||
preCheckedValue = true;
|
||||
autoSaveValue = true;
|
||||
|
||||
// Interactive demo properties
|
||||
demoSizeValue: 'sm' | 'md' | 'lg' = 'md';
|
||||
demoVariantValue: 'primary' | 'secondary' | 'success' | 'warning' | 'danger' = 'primary';
|
||||
demoDisabledValue = false;
|
||||
demoRequiredValue = false;
|
||||
demoWithHelperValue = false;
|
||||
demoCheckedValue = false;
|
||||
|
||||
// Reactive form
|
||||
settingsForm = new FormGroup({
|
||||
notifications: new FormControl(false),
|
||||
newsletter: new FormControl(true),
|
||||
analytics: new FormControl(false, [Validators.requiredTrue]),
|
||||
marketing: new FormControl(false)
|
||||
});
|
||||
|
||||
handleToggle(switchName: string, checked: boolean): void {
|
||||
const action = `${switchName}: ${checked ? 'ON' : 'OFF'}`;
|
||||
this.lastAction.set(action);
|
||||
console.log(`Switch toggled - ${action}`);
|
||||
}
|
||||
|
||||
handleDemoToggle(checked: boolean): void {
|
||||
this.demoCheckedValue = checked;
|
||||
this.handleToggle('Interactive Demo', checked);
|
||||
}
|
||||
|
||||
getFormValues(): string {
|
||||
const values = this.settingsForm.value;
|
||||
return JSON.stringify(values, null, 2);
|
||||
}
|
||||
|
||||
resetForm(): void {
|
||||
this.settingsForm.reset({
|
||||
notifications: false,
|
||||
newsletter: false,
|
||||
analytics: false,
|
||||
marketing: false
|
||||
});
|
||||
this.lastAction.set('Form reset to default values');
|
||||
}
|
||||
|
||||
toggleAllSwitches(): void {
|
||||
const allEnabled = Object.values(this.settingsForm.value).every(val => val === true);
|
||||
const newState = !allEnabled;
|
||||
|
||||
this.settingsForm.patchValue({
|
||||
notifications: newState,
|
||||
newsletter: newState,
|
||||
analytics: newState,
|
||||
marketing: newState
|
||||
});
|
||||
|
||||
this.lastAction.set(`All switches turned ${newState ? 'ON' : 'OFF'}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,637 @@
|
||||
import { Component, ChangeDetectionStrategy, TemplateRef, ViewChild } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
TableComponent,
|
||||
TableActionsComponent,
|
||||
type TableColumn,
|
||||
type TableAction,
|
||||
type TableSortEvent
|
||||
} from '../../../../../ui-essentials/src/lib/components/data-display/table';
|
||||
import { StatusBadgeComponent } from '../../../../../ui-essentials/src/lib/components/feedback';
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
status: 'active' | 'inactive' | 'pending';
|
||||
role: string;
|
||||
lastLogin: string;
|
||||
joinDate: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-table-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
TableComponent,
|
||||
StatusBadgeComponent,
|
||||
TableActionsComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Table Component Showcase</h2>
|
||||
|
||||
<!-- Basic Table -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Basic Table</h3>
|
||||
<ui-table
|
||||
[data]="userData"
|
||||
[columns]="basicColumns"
|
||||
variant="default"
|
||||
size="default"
|
||||
[hoverable]="true"
|
||||
(sort)="handleSort($event)"
|
||||
(rowClick)="handleRowClick($event)">
|
||||
</ui-table>
|
||||
</section>
|
||||
|
||||
<!-- Table Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Table Variants</h3>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Striped Table</h4>
|
||||
<ui-table
|
||||
[data]="userData.slice(0, 5)"
|
||||
[columns]="basicColumns"
|
||||
variant="striped"
|
||||
size="default"
|
||||
[hoverable]="true">
|
||||
</ui-table>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Bordered Table</h4>
|
||||
<ui-table
|
||||
[data]="userData.slice(0, 5)"
|
||||
[columns]="basicColumns"
|
||||
variant="bordered"
|
||||
size="default"
|
||||
[hoverable]="true">
|
||||
</ui-table>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Minimal Table</h4>
|
||||
<ui-table
|
||||
[data]="userData.slice(0, 5)"
|
||||
[columns]="basicColumns"
|
||||
variant="minimal"
|
||||
size="default"
|
||||
[hoverable]="true">
|
||||
</ui-table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Size Variants</h3>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Compact Size</h4>
|
||||
<ui-table
|
||||
[data]="userData.slice(0, 4)"
|
||||
[columns]="basicColumns"
|
||||
variant="default"
|
||||
size="compact"
|
||||
[hoverable]="true">
|
||||
</ui-table>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Comfortable Size</h4>
|
||||
<ui-table
|
||||
[data]="userData.slice(0, 4)"
|
||||
[columns]="basicColumns"
|
||||
variant="default"
|
||||
size="comfortable"
|
||||
[hoverable]="true">
|
||||
</ui-table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Advanced Table with Status and Actions -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Advanced Table with Status Badges and Actions</h3>
|
||||
<ui-table
|
||||
[data]="userData"
|
||||
[columns]="advancedColumns"
|
||||
variant="default"
|
||||
size="default"
|
||||
[hoverable]="true"
|
||||
[cellTemplates]="cellTemplates"
|
||||
[sortColumn]="currentSortColumn"
|
||||
[sortDirection]="currentSortDirection"
|
||||
(sort)="handleAdvancedSort($event)"
|
||||
(rowClick)="handleAdvancedRowClick($event)">
|
||||
</ui-table>
|
||||
|
||||
<!-- Cell Templates -->
|
||||
<ng-template #statusTemplate let-value let-row="row">
|
||||
<ui-status-badge
|
||||
[variant]="getStatusVariant(value)"
|
||||
[dot]="true"
|
||||
size="medium">
|
||||
{{ value | titlecase }}
|
||||
</ui-status-badge>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #actionsTemplate let-row="row" let-index="index">
|
||||
<ui-table-actions
|
||||
[actions]="getActionsForUser(row)"
|
||||
[data]="row"
|
||||
[index]="index"
|
||||
size="medium"
|
||||
[showLabels]="false"
|
||||
(actionClick)="handleAction($event)">
|
||||
</ui-table-actions>
|
||||
</ng-template>
|
||||
</section>
|
||||
|
||||
<!-- Sticky Header Table -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Sticky Header Table</h3>
|
||||
<div style="max-height: 300px; overflow-y: auto; border: 1px solid #ddd; border-radius: 8px;">
|
||||
<ui-table
|
||||
[data]="largeDataset"
|
||||
[columns]="basicColumns"
|
||||
variant="default"
|
||||
size="compact"
|
||||
[stickyHeader]="true"
|
||||
[hoverable]="true">
|
||||
</ui-table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Loading State -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Loading State</h3>
|
||||
<ui-table
|
||||
[data]="[]"
|
||||
[columns]="basicColumns"
|
||||
variant="default"
|
||||
size="default"
|
||||
[loading]="isLoading"
|
||||
loadingMessage="Loading user data...">
|
||||
</ui-table>
|
||||
<button
|
||||
(click)="toggleLoading()"
|
||||
style="margin-top: 1rem; padding: 0.5rem 1rem; border: 1px solid #007bff; background: #007bff; color: white; border-radius: 4px; cursor: pointer;">
|
||||
{{ isLoading ? 'Stop Loading' : 'Start Loading' }}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<!-- Empty State -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Empty State</h3>
|
||||
<ui-table
|
||||
[data]="[]"
|
||||
[columns]="basicColumns"
|
||||
variant="default"
|
||||
size="default"
|
||||
[showEmptyState]="true"
|
||||
emptyMessage="No users found. Try adding some users first.">
|
||||
</ui-table>
|
||||
</section>
|
||||
|
||||
<!-- Selectable Table -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Selectable Rows</h3>
|
||||
<ui-table
|
||||
[data]="userData.slice(0, 6)"
|
||||
[columns]="basicColumns"
|
||||
variant="default"
|
||||
size="default"
|
||||
[selectable]="true"
|
||||
[hoverable]="true"
|
||||
(selectionChange)="handleSelectionChange($event)">
|
||||
</ui-table>
|
||||
@if (selectedUsers.length > 0) {
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #e3f2fd; border-radius: 4px;">
|
||||
<strong>Selected users:</strong> {{ selectedUsers.length }}
|
||||
<div style="margin-top: 0.5rem;">
|
||||
@for (user of selectedUsers; track user.id) {
|
||||
<span style="display: inline-block; margin: 0.25rem; padding: 0.25rem 0.5rem; background: #fff; border-radius: 4px;">
|
||||
{{ user.name }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Usage Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Basic Table:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-table
|
||||
[data]="tableData"
|
||||
[columns]="columns"
|
||||
variant="striped"
|
||||
size="default"
|
||||
[hoverable]="true"
|
||||
(sort)="handleSort($event)"
|
||||
(rowClick)="handleRowClick($event)">
|
||||
</ui-table></code></pre>
|
||||
|
||||
<h4>With Custom Cell Templates:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-table
|
||||
[data]="userData"
|
||||
[columns]="columns"
|
||||
[cellTemplates]="cellTemplates">
|
||||
</ui-table>
|
||||
|
||||
<ng-template #statusTemplate let-value>
|
||||
<ui-status-badge [variant]="getVariant(value)">
|
||||
{{ "{{ value }}" }}
|
||||
</ui-status-badge>
|
||||
</ng-template></code></pre>
|
||||
|
||||
<h4>Column Configuration:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>columns: TableColumn[] = [
|
||||
{{ '{' }} key: 'name', label: 'Name', sortable: true {{ '}' }},
|
||||
{{ '{' }} key: 'email', label: 'Email', sortable: true {{ '}' }},
|
||||
{{ '{' }} key: 'status', label: 'Status', align: 'center' {{ '}' }},
|
||||
{{ '{' }} key: 'actions', label: 'Actions', align: 'right' {{ '}' }}
|
||||
];</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Controls -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Demo Controls</h3>
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
<button (click)="addRandomUser()" style="padding: 0.5rem 1rem; border: 1px solid #28a745; background: #28a745; color: white; border-radius: 4px; cursor: pointer;">
|
||||
Add Random User
|
||||
</button>
|
||||
<button (click)="clearAllUsers()" style="padding: 0.5rem 1rem; border: 1px solid #dc3545; background: #dc3545; color: white; border-radius: 4px; cursor: pointer;">
|
||||
Clear All Users
|
||||
</button>
|
||||
<button (click)="resetDemo()" style="padding: 0.5rem 1rem; border: 1px solid #6c757d; background: #6c757d; color: white; border-radius: 4px; cursor: pointer;">
|
||||
Reset Demo
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (lastAction) {
|
||||
<div style="padding: 1rem; background: #d4edda; border: 1px solid #c3e6cb; border-radius: 4px; color: #155724;">
|
||||
<strong>Last Action:</strong> {{ lastAction }}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid hsl(258, 100%, 47%);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: hsl(279, 14%, 25%);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: hsl(287, 12%, 35%);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid hsl(289, 14%, 90%);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: hsl(286, 20%, 99%);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #d63384;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class TableDemoComponent {
|
||||
@ViewChild('statusTemplate') statusTemplate!: TemplateRef<any>;
|
||||
@ViewChild('actionsTemplate') actionsTemplate!: TemplateRef<any>;
|
||||
|
||||
isLoading = false;
|
||||
selectedUsers: User[] = [];
|
||||
lastAction = '';
|
||||
currentSortColumn = '';
|
||||
currentSortDirection: 'asc' | 'desc' | null = null;
|
||||
|
||||
cellTemplates: Record<string, TemplateRef<any>> = {};
|
||||
|
||||
userData: User[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Alice Johnson',
|
||||
email: 'alice@example.com',
|
||||
status: 'active',
|
||||
role: 'Admin',
|
||||
lastLogin: '2024-01-15',
|
||||
joinDate: '2023-01-15'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Bob Smith',
|
||||
email: 'bob@example.com',
|
||||
status: 'inactive',
|
||||
role: 'User',
|
||||
lastLogin: '2024-01-10',
|
||||
joinDate: '2023-02-20'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Carol Williams',
|
||||
email: 'carol@example.com',
|
||||
status: 'pending',
|
||||
role: 'Editor',
|
||||
lastLogin: '2024-01-14',
|
||||
joinDate: '2023-03-10'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'David Brown',
|
||||
email: 'david@example.com',
|
||||
status: 'active',
|
||||
role: 'User',
|
||||
lastLogin: '2024-01-16',
|
||||
joinDate: '2023-04-05'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Eve Davis',
|
||||
email: 'eve@example.com',
|
||||
status: 'active',
|
||||
role: 'Admin',
|
||||
lastLogin: '2024-01-16',
|
||||
joinDate: '2023-05-12'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Frank Miller',
|
||||
email: 'frank@example.com',
|
||||
status: 'inactive',
|
||||
role: 'User',
|
||||
lastLogin: '2024-01-08',
|
||||
joinDate: '2023-06-18'
|
||||
}
|
||||
];
|
||||
|
||||
basicColumns: TableColumn[] = [
|
||||
{ key: 'name', label: 'Name', sortable: true },
|
||||
{ key: 'email', label: 'Email', sortable: true },
|
||||
{ key: 'role', label: 'Role', sortable: true },
|
||||
{ key: 'lastLogin', label: 'Last Login', sortable: true, align: 'right' }
|
||||
];
|
||||
|
||||
advancedColumns: TableColumn[] = [
|
||||
{ key: 'name', label: 'Name', sortable: true },
|
||||
{ key: 'email', label: 'Email', sortable: true },
|
||||
{ key: 'status', label: 'Status', align: 'center' },
|
||||
{ key: 'role', label: 'Role', sortable: true },
|
||||
{ key: 'actions', label: 'Actions', align: 'right', width: '120px' }
|
||||
];
|
||||
|
||||
largeDataset: User[] = [];
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
// Set up cell templates after view init
|
||||
setTimeout(() => {
|
||||
this.cellTemplates = {
|
||||
'status': this.statusTemplate,
|
||||
'actions': this.actionsTemplate
|
||||
};
|
||||
});
|
||||
|
||||
// Generate large dataset for sticky header demo
|
||||
this.largeDataset = this.generateLargeDataset(50);
|
||||
}
|
||||
|
||||
handleSort(event: TableSortEvent): void {
|
||||
this.lastAction = `Sorted by ${event.column} (${event.direction || 'none'})`;
|
||||
console.log('Sort event:', event);
|
||||
}
|
||||
|
||||
handleAdvancedSort(event: TableSortEvent): void {
|
||||
this.currentSortColumn = event.column;
|
||||
this.currentSortDirection = event.direction;
|
||||
this.handleSort(event);
|
||||
|
||||
// Actually sort the data
|
||||
if (event.direction) {
|
||||
this.userData.sort((a, b) => {
|
||||
const aVal = this.getCellValue(a, event.column);
|
||||
const bVal = this.getCellValue(b, event.column);
|
||||
|
||||
if (event.direction === 'asc') {
|
||||
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
||||
} else {
|
||||
return aVal > bVal ? -1 : aVal < bVal ? 1 : 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleRowClick(event: {row: User, index: number}): void {
|
||||
this.lastAction = `Clicked row: ${event.row.name} (index: ${event.index})`;
|
||||
console.log('Row click:', event);
|
||||
}
|
||||
|
||||
handleAdvancedRowClick(event: {row: User, index: number}): void {
|
||||
this.lastAction = `Advanced row click: ${event.row.name}`;
|
||||
console.log('Advanced row click:', event);
|
||||
}
|
||||
|
||||
handleSelectionChange(selectedUsers: User[]): void {
|
||||
this.selectedUsers = selectedUsers;
|
||||
this.lastAction = `Selection changed: ${selectedUsers.length} users selected`;
|
||||
}
|
||||
|
||||
handleAction(event: any): void {
|
||||
const { action, data, index } = event;
|
||||
this.lastAction = `Action "${action.id}" on user ${data.name} (index: ${index})`;
|
||||
|
||||
switch (action.id) {
|
||||
case 'edit':
|
||||
console.log('Edit user:', data);
|
||||
break;
|
||||
case 'delete':
|
||||
this.deleteUser(data.id);
|
||||
break;
|
||||
case 'view':
|
||||
console.log('View user:', data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getStatusVariant(status: string): any {
|
||||
const variants: any = {
|
||||
'active': 'success',
|
||||
'inactive': 'danger',
|
||||
'pending': 'warning'
|
||||
};
|
||||
return variants[status] || 'neutral';
|
||||
}
|
||||
|
||||
getActionsForUser(user: User): TableAction[] {
|
||||
const baseActions: TableAction[] = [
|
||||
{ id: 'view', label: 'View', variant: 'view', icon: '👁️' },
|
||||
{ id: 'edit', label: 'Edit', variant: 'edit', icon: '✏️' }
|
||||
];
|
||||
|
||||
if (user.status !== 'active') {
|
||||
baseActions.push({
|
||||
id: 'delete',
|
||||
label: 'Delete',
|
||||
variant: 'delete',
|
||||
icon: '🗑️',
|
||||
confirmMessage: `Are you sure you want to delete ${user.name}?`
|
||||
});
|
||||
}
|
||||
|
||||
return baseActions;
|
||||
}
|
||||
|
||||
toggleLoading(): void {
|
||||
this.isLoading = !this.isLoading;
|
||||
this.lastAction = `Loading ${this.isLoading ? 'started' : 'stopped'}`;
|
||||
}
|
||||
|
||||
addRandomUser(): void {
|
||||
const names = ['John Doe', 'Jane Smith', 'Mike Johnson', 'Sarah Wilson', 'Tom Anderson'];
|
||||
const roles = ['Admin', 'User', 'Editor', 'Moderator'];
|
||||
const statuses: ('active' | 'inactive' | 'pending')[] = ['active', 'inactive', 'pending'];
|
||||
|
||||
const newUser: User = {
|
||||
id: Math.max(...this.userData.map(u => u.id)) + 1,
|
||||
name: names[Math.floor(Math.random() * names.length)],
|
||||
email: `user${Date.now()}@example.com`,
|
||||
status: statuses[Math.floor(Math.random() * statuses.length)],
|
||||
role: roles[Math.floor(Math.random() * roles.length)],
|
||||
lastLogin: new Date().toISOString().split('T')[0],
|
||||
joinDate: new Date().toISOString().split('T')[0]
|
||||
};
|
||||
|
||||
this.userData = [...this.userData, newUser];
|
||||
this.lastAction = `Added new user: ${newUser.name}`;
|
||||
}
|
||||
|
||||
clearAllUsers(): void {
|
||||
this.userData = [];
|
||||
this.selectedUsers = [];
|
||||
this.lastAction = 'Cleared all users';
|
||||
}
|
||||
|
||||
resetDemo(): void {
|
||||
this.ngOnInit();
|
||||
this.selectedUsers = [];
|
||||
this.isLoading = false;
|
||||
this.lastAction = 'Demo reset to initial state';
|
||||
}
|
||||
|
||||
private deleteUser(id: number): void {
|
||||
this.userData = this.userData.filter(user => user.id !== id);
|
||||
this.selectedUsers = this.selectedUsers.filter(user => user.id !== id);
|
||||
this.lastAction = `Deleted user with ID: ${id}`;
|
||||
}
|
||||
|
||||
private getCellValue(row: any, key: string): any {
|
||||
return key.split('.').reduce((obj, prop) => obj?.[prop], row) ?? '';
|
||||
}
|
||||
|
||||
private generateLargeDataset(count: number): User[] {
|
||||
const dataset: User[] = [];
|
||||
const names = ['Alex', 'Morgan', 'Casey', 'Jordan', 'Taylor', 'Riley', 'Avery', 'Quinn'];
|
||||
const domains = ['example.com', 'test.com', 'demo.com', 'sample.com'];
|
||||
const roles = ['Admin', 'User', 'Editor', 'Moderator', 'Viewer'];
|
||||
const statuses: ('active' | 'inactive' | 'pending')[] = ['active', 'inactive', 'pending'];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
dataset.push({
|
||||
id: i + 100,
|
||||
name: `${names[i % names.length]} ${i + 1}`,
|
||||
email: `user${i + 100}@${domains[i % domains.length]}`,
|
||||
status: statuses[i % statuses.length],
|
||||
role: roles[i % roles.length],
|
||||
lastLogin: new Date(2024, 0, (i % 30) + 1).toISOString().split('T')[0],
|
||||
joinDate: new Date(2023, (i % 12), (i % 28) + 1).toISOString().split('T')[0]
|
||||
});
|
||||
}
|
||||
|
||||
return dataset;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Reset to initial data
|
||||
this.userData = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Alice Johnson',
|
||||
email: 'alice@example.com',
|
||||
status: 'active',
|
||||
role: 'Admin',
|
||||
lastLogin: '2024-01-15',
|
||||
joinDate: '2023-01-15'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Bob Smith',
|
||||
email: 'bob@example.com',
|
||||
status: 'inactive',
|
||||
role: 'User',
|
||||
lastLogin: '2024-01-10',
|
||||
joinDate: '2023-02-20'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Carol Williams',
|
||||
email: 'carol@example.com',
|
||||
status: 'pending',
|
||||
role: 'Editor',
|
||||
lastLogin: '2024-01-14',
|
||||
joinDate: '2023-03-10'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'David Brown',
|
||||
email: 'david@example.com',
|
||||
status: 'active',
|
||||
role: 'User',
|
||||
lastLogin: '2024-01-16',
|
||||
joinDate: '2023-04-05'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Eve Davis',
|
||||
email: 'eve@example.com',
|
||||
status: 'active',
|
||||
role: 'Admin',
|
||||
lastLogin: '2024-01-16',
|
||||
joinDate: '2023-05-12'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Frank Miller',
|
||||
email: 'frank@example.com',
|
||||
status: 'inactive',
|
||||
role: 'User',
|
||||
lastLogin: '2024-01-08',
|
||||
joinDate: '2023-06-18'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
.demo-container {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
h4 {
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-interactive {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.demo-output {
|
||||
h4 {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #6b7280;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-form {
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.submit-btn, .reset-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: 1px solid #3b82f6;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #2563eb;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: transparent;
|
||||
color: #6b7280;
|
||||
border: 1px solid #d1d5db;
|
||||
|
||||
&:hover {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
}
|
||||
|
||||
.form-status {
|
||||
background: #f3f4f6;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
|
||||
p {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #e5e7eb;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-interactive {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TimePickerComponent, TimeValue } from '../../../../../ui-essentials/src/lib/components/forms/time-picker/time-picker.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-time-picker-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, TimePickerComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Time Picker Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ size | uppercase }}</h4>
|
||||
<ui-time-picker
|
||||
[size]="size"
|
||||
[label]="size + ' Time Picker'"
|
||||
[placeholder]="'Select time'"
|
||||
[(ngModel)]="sampleValues[size]">
|
||||
</ui-time-picker>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Styles -->
|
||||
<section class="demo-section">
|
||||
<h3>Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ variant | titlecase }}</h4>
|
||||
<ui-time-picker
|
||||
[variant]="variant"
|
||||
[label]="variant + ' Variant'"
|
||||
[placeholder]="'Select time'"
|
||||
[(ngModel)]="variantValues[variant]">
|
||||
</ui-time-picker>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Time Formats -->
|
||||
<section class="demo-section">
|
||||
<h3>Time Formats</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>12-Hour Format</h4>
|
||||
<ui-time-picker
|
||||
label="12-Hour Time"
|
||||
placeholder="Select time"
|
||||
timeFormat="12"
|
||||
helperText="Uses AM/PM format"
|
||||
[(ngModel)]="formatValues['twelve']">
|
||||
</ui-time-picker>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>24-Hour Format</h4>
|
||||
<ui-time-picker
|
||||
label="24-Hour Time"
|
||||
placeholder="Select time"
|
||||
timeFormat="24"
|
||||
helperText="Military time format"
|
||||
[(ngModel)]="formatValues['twentyFour']">
|
||||
</ui-time-picker>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>With Seconds</h4>
|
||||
<ui-time-picker
|
||||
label="Time with Seconds"
|
||||
placeholder="Select time"
|
||||
[showSeconds]="true"
|
||||
helperText="Includes seconds picker"
|
||||
[(ngModel)]="formatValues['withSeconds']">
|
||||
</ui-time-picker>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Custom Steps</h4>
|
||||
<ui-time-picker
|
||||
label="Custom Minute Steps"
|
||||
placeholder="Select time"
|
||||
[minuteStep]="15"
|
||||
[secondStep]="30"
|
||||
[showSeconds]="true"
|
||||
helperText="15-minute and 30-second steps"
|
||||
[(ngModel)]="formatValues['customSteps']">
|
||||
</ui-time-picker>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Default</h4>
|
||||
<ui-time-picker
|
||||
label="Default State"
|
||||
placeholder="Select time"
|
||||
helperText="Choose your preferred time"
|
||||
[(ngModel)]="stateValues['default']">
|
||||
</ui-time-picker>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Error</h4>
|
||||
<ui-time-picker
|
||||
label="Error State"
|
||||
placeholder="Select time"
|
||||
state="error"
|
||||
errorMessage="Please select a valid time"
|
||||
[(ngModel)]="stateValues['error']">
|
||||
</ui-time-picker>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Success</h4>
|
||||
<ui-time-picker
|
||||
label="Success State"
|
||||
placeholder="Select time"
|
||||
state="success"
|
||||
helperText="Perfect timing!"
|
||||
[(ngModel)]="stateValues['success']">
|
||||
</ui-time-picker>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Warning</h4>
|
||||
<ui-time-picker
|
||||
label="Warning State"
|
||||
placeholder="Select time"
|
||||
state="warning"
|
||||
helperText="Time is outside business hours"
|
||||
[(ngModel)]="stateValues['warning']">
|
||||
</ui-time-picker>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Special Features -->
|
||||
<section class="demo-section">
|
||||
<h3>Features</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Required Field</h4>
|
||||
<ui-time-picker
|
||||
label="Required Time"
|
||||
placeholder="Select time"
|
||||
[required]="true"
|
||||
helperText="This field is required"
|
||||
[(ngModel)]="featureValues['required']">
|
||||
</ui-time-picker>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Disabled</h4>
|
||||
<ui-time-picker
|
||||
label="Disabled Time Picker"
|
||||
placeholder="Cannot select"
|
||||
[disabled]="true"
|
||||
helperText="This field is disabled"
|
||||
[(ngModel)]="featureValues['disabled']">
|
||||
</ui-time-picker>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Not Clearable</h4>
|
||||
<ui-time-picker
|
||||
label="No Clear Button"
|
||||
placeholder="Select time"
|
||||
[clearable]="false"
|
||||
helperText="Clear button is hidden"
|
||||
[(ngModel)]="featureValues['notClearable']">
|
||||
</ui-time-picker>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>With Presets</h4>
|
||||
<ui-time-picker
|
||||
label="Quick Time Selection"
|
||||
placeholder="Select time"
|
||||
[presetTimes]="presetTimes"
|
||||
helperText="Includes common time presets"
|
||||
[(ngModel)]="featureValues['presets']">
|
||||
</ui-time-picker>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Example</h3>
|
||||
<div class="demo-interactive">
|
||||
<ui-time-picker
|
||||
label="Meeting Time"
|
||||
placeholder="Select meeting time"
|
||||
helperText="Choose a time for your meeting"
|
||||
[showSeconds]="true"
|
||||
[(ngModel)]="interactiveValue"
|
||||
(timeChange)="onTimeChange($event)">
|
||||
</ui-time-picker>
|
||||
|
||||
<div class="demo-output">
|
||||
<h4>Selected Time:</h4>
|
||||
<p>{{ interactiveValue ? formatTime(interactiveValue) : 'No time selected' }}</p>
|
||||
<p><strong>24-hour format:</strong> {{ interactiveValue ? format24Hour(interactiveValue) : 'N/A' }}</p>
|
||||
<p><strong>Change count:</strong> {{ changeCount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Form Integration -->
|
||||
<section class="demo-section">
|
||||
<h3>Form Integration</h3>
|
||||
<form (ngSubmit)="onSubmit()" #demoForm="ngForm" class="demo-form">
|
||||
<div class="form-row">
|
||||
<ui-time-picker
|
||||
label="Start Time"
|
||||
name="startTime"
|
||||
placeholder="Select start time"
|
||||
[(ngModel)]="formValues.startTime"
|
||||
[required]="true"
|
||||
#startTime="ngModel">
|
||||
</ui-time-picker>
|
||||
|
||||
<ui-time-picker
|
||||
label="End Time"
|
||||
name="endTime"
|
||||
placeholder="Select end time"
|
||||
[(ngModel)]="formValues.endTime"
|
||||
[required]="true"
|
||||
#endTime="ngModel">
|
||||
</ui-time-picker>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<ui-time-picker
|
||||
label="Break Time"
|
||||
name="breakTime"
|
||||
placeholder="Select break time"
|
||||
[presetTimes]="breakPresets"
|
||||
[(ngModel)]="formValues.breakTime"
|
||||
#breakTime="ngModel">
|
||||
</ui-time-picker>
|
||||
|
||||
<ui-time-picker
|
||||
label="Meeting Duration"
|
||||
name="duration"
|
||||
placeholder="Select duration"
|
||||
[showSeconds]="true"
|
||||
[minuteStep]="15"
|
||||
[(ngModel)]="formValues.duration"
|
||||
#duration="ngModel">
|
||||
</ui-time-picker>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" [disabled]="!demoForm.valid" class="submit-btn">
|
||||
Submit Form
|
||||
</button>
|
||||
<button type="button" (click)="resetForm(demoForm)" class="reset-btn">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-status">
|
||||
<p><strong>Form Valid:</strong> {{ demoForm.valid }}</p>
|
||||
<p><strong>Form Values:</strong></p>
|
||||
<pre>{{ formValues | json }}</pre>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './time-picker-demo.component.scss'
|
||||
})
|
||||
export class TimePickerDemoComponent {
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
variants = ['outlined', 'filled', 'underlined'] as const;
|
||||
|
||||
sampleValues: Record<string, TimeValue | null> = {
|
||||
sm: null,
|
||||
md: null,
|
||||
lg: null
|
||||
};
|
||||
|
||||
variantValues: Record<string, TimeValue | null> = {
|
||||
outlined: null,
|
||||
filled: null,
|
||||
underlined: null
|
||||
};
|
||||
|
||||
formatValues: Record<string, TimeValue | null> = {
|
||||
twelve: null,
|
||||
twentyFour: null,
|
||||
withSeconds: null,
|
||||
customSteps: null
|
||||
};
|
||||
|
||||
stateValues: Record<string, TimeValue | null> = {
|
||||
default: null,
|
||||
error: null,
|
||||
success: { hours: 9, minutes: 30, seconds: 0 },
|
||||
warning: null
|
||||
};
|
||||
|
||||
featureValues: Record<string, TimeValue | null> = {
|
||||
required: null,
|
||||
disabled: { hours: 12, minutes: 0, seconds: 0 },
|
||||
notClearable: null,
|
||||
presets: null
|
||||
};
|
||||
|
||||
interactiveValue: TimeValue | null = null;
|
||||
changeCount = 0;
|
||||
|
||||
formValues = {
|
||||
startTime: null as TimeValue | null,
|
||||
endTime: null as TimeValue | null,
|
||||
breakTime: null as TimeValue | null,
|
||||
duration: null as TimeValue | null
|
||||
};
|
||||
|
||||
// Preset times for demo
|
||||
presetTimes = [
|
||||
{ label: '9:00 AM', value: { hours: 9, minutes: 0, seconds: 0 } },
|
||||
{ label: '12:00 PM', value: { hours: 12, minutes: 0, seconds: 0 } },
|
||||
{ label: '1:00 PM', value: { hours: 13, minutes: 0, seconds: 0 } },
|
||||
{ label: '5:00 PM', value: { hours: 17, minutes: 0, seconds: 0 } }
|
||||
];
|
||||
|
||||
breakPresets = [
|
||||
{ label: '15 min', value: { hours: 0, minutes: 15, seconds: 0 } },
|
||||
{ label: '30 min', value: { hours: 0, minutes: 30, seconds: 0 } },
|
||||
{ label: '1 hour', value: { hours: 1, minutes: 0, seconds: 0 } }
|
||||
];
|
||||
|
||||
onTimeChange(time: TimeValue | null): void {
|
||||
this.changeCount++;
|
||||
console.log('Time changed:', time);
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
console.log('Form submitted:', this.formValues);
|
||||
alert('Form submitted! Check console for values.');
|
||||
}
|
||||
|
||||
resetForm(form: any): void {
|
||||
form.resetForm();
|
||||
this.formValues = {
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
breakTime: null,
|
||||
duration: null
|
||||
};
|
||||
}
|
||||
|
||||
formatTime(time: TimeValue): string {
|
||||
const hours = time.hours;
|
||||
const displayHour = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
|
||||
const period = hours >= 12 ? 'PM' : 'AM';
|
||||
const minutes = time.minutes.toString().padStart(2, '0');
|
||||
|
||||
if (time.seconds !== undefined && time.seconds > 0) {
|
||||
const seconds = time.seconds.toString().padStart(2, '0');
|
||||
return `${displayHour}:${minutes}:${seconds} ${period}`;
|
||||
}
|
||||
|
||||
return `${displayHour}:${minutes} ${period}`;
|
||||
}
|
||||
|
||||
format24Hour(time: TimeValue): string {
|
||||
const hours = time.hours.toString().padStart(2, '0');
|
||||
const minutes = time.minutes.toString().padStart(2, '0');
|
||||
|
||||
if (time.seconds !== undefined && time.seconds > 0) {
|
||||
const seconds = time.seconds.toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
return `${hours}:${minutes}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
@use "../../../../../shared-ui/src/styles/semantic" as *;
|
||||
|
||||
|
||||
// ==========================================================================
|
||||
// VIDEO PLAYER DEMO COMPONENT
|
||||
// ==========================================================================
|
||||
// Demonstration component showcasing all video player variants and capabilities
|
||||
// ==========================================================================
|
||||
|
||||
.video-player-demo {
|
||||
padding: $semantic-spacing-layout-lg;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
color: $semantic-color-text-primary;
|
||||
font-family: map-get($semantic-typography-heading-h1, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h1, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h1, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h1, line-height);
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
border-bottom: 2px solid $semantic-color-primary;
|
||||
padding-bottom: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: $semantic-color-text-secondary;
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: $semantic-color-text-tertiary;
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
border: 1px solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
padding: $semantic-spacing-component-xl;
|
||||
background: $semantic-color-surface-secondary;
|
||||
margin-bottom: $semantic-spacing-layout-md;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.variant-demo {
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.player-container {
|
||||
border: 1px solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
overflow: hidden;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-primary;
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
color: $semantic-color-text-tertiary;
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
}
|
||||
|
||||
.controls-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: $semantic-spacing-component-md;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-container;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
cursor: pointer;
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
user-select: none;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin: 0;
|
||||
accent-color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.select-controls {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.select-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
.demo-select {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
border: 1px solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $semantic-color-primary;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.event-display {
|
||||
margin-top: $semantic-spacing-component-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-primary-container;
|
||||
border: 1px solid $semantic-color-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
color: $semantic-color-text-primary;
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
}
|
||||
|
||||
.code-examples {
|
||||
background: $semantic-color-surface-container;
|
||||
padding: $semantic-spacing-component-xl;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border-left: 4px solid $semantic-color-primary;
|
||||
}
|
||||
|
||||
.code-example {
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: $semantic-color-surface-primary;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
overflow-x: auto;
|
||||
border: 1px solid $semantic-color-border-secondary;
|
||||
|
||||
code {
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: map-get($semantic-typography-code-block, font-size);
|
||||
line-height: map-get($semantic-typography-code-block, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.video-player-demo {
|
||||
padding: $semantic-spacing-layout-md;
|
||||
|
||||
.controls-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.select-controls {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { VideoPlayerComponent, VideoSource, VideoTrack, VideoPlayerSize, VideoPlayerVariant } from '../../../../../ui-essentials/src/lib/components/media/video-player/video-player.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-video-player-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
VideoPlayerComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="video-player-demo">
|
||||
<h2>Video Player Component Showcase</h2>
|
||||
|
||||
<!-- Video Player Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Video Player Variants</h3>
|
||||
|
||||
<div class="variant-demo">
|
||||
<h4>Default Player</h4>
|
||||
<div class="player-container">
|
||||
<ui-video-player
|
||||
[sources]="basicVideoSources()"
|
||||
[tracks]="videoTracks()"
|
||||
poster="https://images.unsplash.com/photo-1574717024653-61fd2cf4d44d?w=800&h=450&fit=crop"
|
||||
variant="default"
|
||||
size="md"
|
||||
[showControls]="true"
|
||||
[showVolumeControl]="true"
|
||||
[allowFullscreen]="true"
|
||||
ariaLabel="Demo video player"
|
||||
(play)="handleVideoEvent('Default player started playing')"
|
||||
(pause)="handleVideoEvent('Default player paused')"
|
||||
(ended)="handleVideoEvent('Default player ended')"
|
||||
(timeUpdate)="handleTimeUpdate($event)"
|
||||
(volumeChange)="handleVolumeChange($event)"
|
||||
(fullscreenChange)="handleFullscreenChange($event)"
|
||||
(error)="handleVideoError($event)">
|
||||
</ui-video-player>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="variant-demo">
|
||||
<h4>Minimal Player</h4>
|
||||
<div class="player-container">
|
||||
<ui-video-player
|
||||
[sources]="basicVideoSources()"
|
||||
poster="https://images.unsplash.com/photo-1611162617474-5b21e879e113?w=800&h=450&fit=crop"
|
||||
variant="minimal"
|
||||
size="sm"
|
||||
[showControls]="true"
|
||||
[showVolumeControl]="false"
|
||||
[allowFullscreen]="false"
|
||||
ariaLabel="Minimal video player">
|
||||
</ui-video-player>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="variant-demo">
|
||||
<h4>Theater Player</h4>
|
||||
<div class="player-container">
|
||||
<ui-video-player
|
||||
[sources]="basicVideoSources()"
|
||||
poster="https://images.unsplash.com/photo-1440404653325-ab127d49abc1?w=800&h=450&fit=crop"
|
||||
variant="theater"
|
||||
size="lg"
|
||||
[showControls]="true"
|
||||
[showVolumeControl]="true"
|
||||
[allowFullscreen]="true"
|
||||
[autoplay]="false"
|
||||
[muted]="true"
|
||||
ariaLabel="Theater video player">
|
||||
</ui-video-player>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
|
||||
<div class="variant-demo">
|
||||
<h4>Small (sm)</h4>
|
||||
<div class="player-container">
|
||||
<ui-video-player
|
||||
[sources]="basicVideoSources()"
|
||||
poster="https://images.unsplash.com/photo-1598300042247-d088f8ab3a91?w=400&h=225&fit=crop"
|
||||
size="sm"
|
||||
[showControls]="true"
|
||||
ariaLabel="Small video player">
|
||||
</ui-video-player>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="variant-demo">
|
||||
<h4>Medium (md) - Default</h4>
|
||||
<div class="player-container">
|
||||
<ui-video-player
|
||||
[sources]="basicVideoSources()"
|
||||
poster="https://images.unsplash.com/photo-1574717024653-61fd2cf4d44d?w=600&h=337&fit=crop"
|
||||
size="md"
|
||||
[showControls]="true"
|
||||
ariaLabel="Medium video player">
|
||||
</ui-video-player>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="variant-demo">
|
||||
<h4>Large (lg)</h4>
|
||||
<div class="player-container">
|
||||
<ui-video-player
|
||||
[sources]="basicVideoSources()"
|
||||
poster="https://images.unsplash.com/photo-1440404653325-ab127d49abc1?w=800&h=450&fit=crop"
|
||||
size="lg"
|
||||
[showControls]="true"
|
||||
ariaLabel="Large video player">
|
||||
</ui-video-player>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Feature Demonstrations -->
|
||||
<section class="demo-section">
|
||||
<h3>Feature Demonstrations</h3>
|
||||
|
||||
<div class="variant-demo">
|
||||
<h4>Player with Subtitles</h4>
|
||||
<div class="player-container">
|
||||
<ui-video-player
|
||||
[sources]="basicVideoSources()"
|
||||
[tracks]="videoTracks()"
|
||||
poster="https://images.unsplash.com/photo-1611162617474-5b21e879e113?w=800&h=450&fit=crop"
|
||||
[showControls]="true"
|
||||
ariaLabel="Video player with subtitles">
|
||||
</ui-video-player>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="variant-demo">
|
||||
<h4>Autoplay Player (Muted)</h4>
|
||||
<div class="player-container">
|
||||
<ui-video-player
|
||||
[sources]="basicVideoSources()"
|
||||
poster="https://images.unsplash.com/photo-1598300042247-d088f8ab3a91?w=800&h=450&fit=crop"
|
||||
[autoplay]="true"
|
||||
[muted]="true"
|
||||
[loop]="true"
|
||||
[showControls]="true"
|
||||
ariaLabel="Autoplay video player">
|
||||
</ui-video-player>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="variant-demo">
|
||||
<h4>Player without Volume Control</h4>
|
||||
<div class="player-container">
|
||||
<ui-video-player
|
||||
[sources]="basicVideoSources()"
|
||||
poster="https://images.unsplash.com/photo-1574717024653-61fd2cf4d44d?w=800&h=450&fit=crop"
|
||||
[showControls]="true"
|
||||
[showVolumeControl]="false"
|
||||
ariaLabel="Video player without volume control">
|
||||
</ui-video-player>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="variant-demo">
|
||||
<h4>Disabled Player</h4>
|
||||
<div class="player-container">
|
||||
<ui-video-player
|
||||
[sources]="basicVideoSources()"
|
||||
poster="https://images.unsplash.com/photo-1440404653325-ab127d49abc1?w=800&h=450&fit=crop"
|
||||
[disabled]="true"
|
||||
[showControls]="true"
|
||||
ariaLabel="Disabled video player">
|
||||
</ui-video-player>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Demo -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Demo</h3>
|
||||
<p class="demo-description">
|
||||
Customize the video player below by toggling different options:
|
||||
</p>
|
||||
|
||||
<div class="controls-grid">
|
||||
<label class="control-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="demoConfig().autoplay"
|
||||
(change)="updateDemoConfig('autoplay', $event)">
|
||||
Autoplay (muted)
|
||||
</label>
|
||||
<label class="control-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="demoConfig().loop"
|
||||
(change)="updateDemoConfig('loop', $event)">
|
||||
Loop
|
||||
</label>
|
||||
<label class="control-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="demoConfig().showControls"
|
||||
(change)="updateDemoConfig('showControls', $event)">
|
||||
Show Controls
|
||||
</label>
|
||||
<label class="control-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="demoConfig().showVolumeControl"
|
||||
(change)="updateDemoConfig('showVolumeControl', $event)">
|
||||
Volume Control
|
||||
</label>
|
||||
<label class="control-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="demoConfig().allowFullscreen"
|
||||
(change)="updateDemoConfig('allowFullscreen', $event)">
|
||||
Fullscreen
|
||||
</label>
|
||||
<label class="control-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="demoConfig().disabled"
|
||||
(change)="updateDemoConfig('disabled', $event)">
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="select-controls">
|
||||
<label class="select-label">
|
||||
Size:
|
||||
<select
|
||||
[value]="demoConfig().size"
|
||||
(change)="updateDemoSize($event)"
|
||||
class="demo-select">
|
||||
<option value="sm">Small</option>
|
||||
<option value="md">Medium</option>
|
||||
<option value="lg">Large</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="select-label">
|
||||
Variant:
|
||||
<select
|
||||
[value]="demoConfig().variant"
|
||||
(change)="updateDemoVariant($event)"
|
||||
class="demo-select">
|
||||
<option value="default">Default</option>
|
||||
<option value="minimal">Minimal</option>
|
||||
<option value="theater">Theater</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="player-container">
|
||||
<ui-video-player
|
||||
[sources]="basicVideoSources()"
|
||||
[tracks]="videoTracks()"
|
||||
[poster]="demoConfig().poster"
|
||||
[size]="demoConfig().size"
|
||||
[variant]="demoConfig().variant"
|
||||
[autoplay]="demoConfig().autoplay"
|
||||
[loop]="demoConfig().loop"
|
||||
[muted]="demoConfig().autoplay"
|
||||
[showControls]="demoConfig().showControls"
|
||||
[showVolumeControl]="demoConfig().showVolumeControl"
|
||||
[allowFullscreen]="demoConfig().allowFullscreen"
|
||||
[disabled]="demoConfig().disabled"
|
||||
ariaLabel="Interactive demo video player"
|
||||
(play)="handleVideoEvent('Interactive player started playing')"
|
||||
(pause)="handleVideoEvent('Interactive player paused')"
|
||||
(ended)="handleVideoEvent('Interactive player ended')"
|
||||
(timeUpdate)="handleTimeUpdate($event)"
|
||||
(volumeChange)="handleVolumeChange($event)"
|
||||
(fullscreenChange)="handleFullscreenChange($event)"
|
||||
(error)="handleVideoError($event)">
|
||||
</ui-video-player>
|
||||
</div>
|
||||
|
||||
@if (lastEvent()) {
|
||||
<div class="event-display">
|
||||
<strong>Last event:</strong> {{ lastEvent() }}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Real-world Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Real-world Examples</h3>
|
||||
|
||||
<div class="variant-demo">
|
||||
<h4>Course Video Player</h4>
|
||||
<p class="demo-description">
|
||||
Typical setup for educational content with subtitles and chapter markers
|
||||
</p>
|
||||
<div class="player-container">
|
||||
<ui-video-player
|
||||
[sources]="basicVideoSources()"
|
||||
[tracks]="videoTracks()"
|
||||
poster="https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&h=450&fit=crop"
|
||||
size="lg"
|
||||
[showControls]="true"
|
||||
[showVolumeControl]="true"
|
||||
[allowFullscreen]="true"
|
||||
preload="metadata"
|
||||
ariaLabel="Course video player">
|
||||
</ui-video-player>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="variant-demo">
|
||||
<h4>Product Demo Player</h4>
|
||||
<p class="demo-description">
|
||||
Autoplay setup for product demonstrations and marketing content
|
||||
</p>
|
||||
<div class="player-container">
|
||||
<ui-video-player
|
||||
[sources]="basicVideoSources()"
|
||||
poster="https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=800&h=450&fit=crop"
|
||||
variant="minimal"
|
||||
size="md"
|
||||
[autoplay]="true"
|
||||
[muted]="true"
|
||||
[loop]="true"
|
||||
[showControls]="true"
|
||||
[showVolumeControl]="false"
|
||||
[allowFullscreen]="false"
|
||||
ariaLabel="Product demo video player">
|
||||
</ui-video-player>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="variant-demo">
|
||||
<h4>Media Gallery Player</h4>
|
||||
<p class="demo-description">
|
||||
Compact player for media galleries and previews
|
||||
</p>
|
||||
<div class="player-container">
|
||||
<ui-video-player
|
||||
[sources]="basicVideoSources()"
|
||||
poster="https://images.unsplash.com/photo-1598300042247-d088f8ab3a91?w=800&h=450&fit=crop"
|
||||
size="sm"
|
||||
[showControls]="true"
|
||||
[showVolumeControl]="false"
|
||||
[allowFullscreen]="true"
|
||||
preload="none"
|
||||
ariaLabel="Media gallery video player">
|
||||
</ui-video-player>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Error Handling Demo -->
|
||||
<section class="demo-section">
|
||||
<h3>Error Handling</h3>
|
||||
|
||||
<div class="variant-demo">
|
||||
<h4>Invalid Video Source</h4>
|
||||
<p class="demo-description">
|
||||
Demonstrates error handling when video fails to load
|
||||
</p>
|
||||
<div class="player-container">
|
||||
<ui-video-player
|
||||
[sources]="errorVideoSources()"
|
||||
poster="https://images.unsplash.com/photo-1574717024653-61fd2cf4d44d?w=800&h=450&fit=crop"
|
||||
[showControls]="true"
|
||||
ariaLabel="Error demo video player"
|
||||
(error)="handleVideoError($event)">
|
||||
</ui-video-player>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Code Examples</h3>
|
||||
<div class="code-examples">
|
||||
<div class="code-example">
|
||||
<h4>Basic Video Player:</h4>
|
||||
<pre><code><ui-video-player
|
||||
[sources]="videoSources"
|
||||
poster="poster-image.jpg"
|
||||
[showControls]="true"
|
||||
ariaLabel="Demo video player">
|
||||
</ui-video-player></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
<h4>Video Player with Subtitles:</h4>
|
||||
<pre><code><ui-video-player
|
||||
[sources]="videoSources"
|
||||
[tracks]="subtitleTracks"
|
||||
poster="poster-image.jpg"
|
||||
size="lg"
|
||||
[showControls]="true"
|
||||
[allowFullscreen]="true"
|
||||
(play)="onVideoPlay()"
|
||||
(pause)="onVideoPause()">
|
||||
</ui-video-player></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
<h4>Autoplay Video (Marketing/Demo):</h4>
|
||||
<pre><code><ui-video-player
|
||||
[sources]="videoSources"
|
||||
variant="minimal"
|
||||
size="md"
|
||||
[autoplay]="true"
|
||||
[muted]="true"
|
||||
[loop]="true"
|
||||
[showVolumeControl]="false"
|
||||
[allowFullscreen]="false">
|
||||
</ui-video-player></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './video-player-demo.component.scss'
|
||||
})
|
||||
export class VideoPlayerDemoComponent {
|
||||
// State signals
|
||||
lastEvent = signal('');
|
||||
|
||||
// Demo video sources - using sample videos from the web
|
||||
basicVideoSources = signal<VideoSource[]>([
|
||||
{
|
||||
src: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
||||
type: 'video/mp4',
|
||||
quality: '1080p',
|
||||
label: 'HD'
|
||||
},
|
||||
{
|
||||
src: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.webm',
|
||||
type: 'video/webm',
|
||||
quality: '1080p',
|
||||
label: 'HD WebM'
|
||||
}
|
||||
]);
|
||||
|
||||
errorVideoSources = signal<VideoSource[]>([
|
||||
{
|
||||
src: 'https://invalid-url.example.com/nonexistent.mp4',
|
||||
type: 'video/mp4',
|
||||
quality: '1080p',
|
||||
label: 'Invalid Source'
|
||||
}
|
||||
]);
|
||||
|
||||
videoTracks = signal<VideoTrack[]>([
|
||||
{
|
||||
src: 'data:text/vtt;base64,V0VCVlRUCgowMDowMDowMC4wMDAgLS0+IDAwOjAwOjA1LjAwMAo8Zm9udCBjb2xvcj0iI2ZmZmZmZiI+U2FtcGxlIHN1YnRpdGxlIHRleHQ8L2ZvbnQ+CgowMDowMDowNS4wMDAgLS0+IDAwOjAwOjEwLjAwMAo8Zm9udCBjb2xvcj0iI2ZmZmZmZiI+VGhpcyBpcyBhIGRlbW9uc3RyYXRpb24gb2Ygc3VidGl0bGVzPC9mb250Pg==',
|
||||
kind: 'subtitles',
|
||||
srclang: 'en',
|
||||
label: 'English',
|
||||
default: true
|
||||
}
|
||||
]);
|
||||
|
||||
// Demo configuration
|
||||
demoConfig = signal({
|
||||
size: 'md' as VideoPlayerSize,
|
||||
variant: 'default' as VideoPlayerVariant,
|
||||
autoplay: false,
|
||||
loop: false,
|
||||
showControls: true,
|
||||
showVolumeControl: true,
|
||||
allowFullscreen: true,
|
||||
disabled: false,
|
||||
poster: 'https://images.unsplash.com/photo-1574717024653-61fd2cf4d44d?w=800&h=450&fit=crop'
|
||||
});
|
||||
|
||||
handleVideoEvent(event: string): void {
|
||||
this.lastEvent.set(`${event} at ${new Date().toLocaleTimeString()}`);
|
||||
console.log(`Video event: ${event}`);
|
||||
}
|
||||
|
||||
handleTimeUpdate(time: number): void {
|
||||
// Only log significant time updates to avoid console spam
|
||||
if (Math.floor(time) % 10 === 0) {
|
||||
console.log(`Time update: ${this.formatTime(time)}`);
|
||||
}
|
||||
}
|
||||
|
||||
handleVolumeChange(volume: number): void {
|
||||
this.lastEvent.set(`Volume changed to ${Math.round(volume * 100)}% at ${new Date().toLocaleTimeString()}`);
|
||||
console.log(`Volume changed: ${volume}`);
|
||||
}
|
||||
|
||||
handleFullscreenChange(isFullscreen: boolean): void {
|
||||
this.lastEvent.set(`${isFullscreen ? 'Entered' : 'Exited'} fullscreen at ${new Date().toLocaleTimeString()}`);
|
||||
console.log(`Fullscreen: ${isFullscreen}`);
|
||||
}
|
||||
|
||||
handleVideoError(event: Event): void {
|
||||
this.lastEvent.set(`Video error occurred at ${new Date().toLocaleTimeString()}`);
|
||||
console.error('Video error:', event);
|
||||
}
|
||||
|
||||
updateDemoConfig(key: string, event: Event): void {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const value = target.checked;
|
||||
|
||||
this.demoConfig.update(config => ({
|
||||
...config,
|
||||
[key]: value
|
||||
}));
|
||||
}
|
||||
|
||||
updateDemoSize(event: Event): void {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
const size = target.value as VideoPlayerSize;
|
||||
|
||||
this.demoConfig.update(config => ({
|
||||
...config,
|
||||
size
|
||||
}));
|
||||
}
|
||||
|
||||
updateDemoVariant(event: Event): void {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
const variant = target.value as VideoPlayerVariant;
|
||||
|
||||
this.demoConfig.update(config => ({
|
||||
...config,
|
||||
variant
|
||||
}));
|
||||
}
|
||||
|
||||
private formatTime(seconds: number): string {
|
||||
if (isNaN(seconds)) return '0:00';
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DashboardSidebarComponent, SidebarMenuItem } from "./dashboard.sidebar.component";
|
||||
import { LayoutContainerComponent } from "../../shared/layout-containers/layout-container.component";
|
||||
import {
|
||||
faWindowMaximize, faUserCircle, faCertificate, faHandPointer, faIdCard, faTags,
|
||||
faCheckSquare, faKeyboard, faThLarge, faList, faDotCircle, faSearch, faToggleOn,
|
||||
faTable, faImage, faImages, faPlay, faBars, faEdit, faEye, faCompass,
|
||||
faVideo, faComment, faMousePointer, faLayerGroup, faSquare, faCalendarDays, faClock,
|
||||
faGripVertical, faArrowsAlt, faBoxOpen, faChevronLeft, faSpinner, faExclamationTriangle,
|
||||
faCloudUploadAlt, faFileText, faListAlt, faCircle, faExpandArrowsAlt
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { DemoRoutes } from '../../demos';
|
||||
import { ScrollContainerComponent } from '../../../../../ui-essentials/src/lib/layouts';
|
||||
// import { DemoRoutes } from "../../../../../ui-essentials/src/public-api";
|
||||
|
||||
@Component({
|
||||
selector: 'skyui-dashboard',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
DashboardSidebarComponent,
|
||||
LayoutContainerComponent,
|
||||
ScrollContainerComponent,
|
||||
DemoRoutes
|
||||
],
|
||||
template: `
|
||||
|
||||
|
||||
<ui-layout-container [topBarHeight]=64 [leftNavigationState]="'full'"
|
||||
[showTopBar]=false [customLeftWidth]=260>
|
||||
<div slot=left-navigation style="height: 100vh;">
|
||||
<ui-scroll-container scrollbarVisibility="never">
|
||||
<skyui-dashboard-sidebar [data]=menuItems (selected)="menuClick($event)"></skyui-dashboard-sidebar>
|
||||
</ui-scroll-container>
|
||||
</div>
|
||||
|
||||
<div slot=content style="height: 100vh;">
|
||||
<ui-scroll-container>
|
||||
<ui-demo-routes [route]=route></ui-demo-routes>
|
||||
</ui-scroll-container>
|
||||
</div>
|
||||
</ui-layout-container>
|
||||
|
||||
`,
|
||||
})
|
||||
export class DashboardComponent {
|
||||
// Sidebar state
|
||||
sidebarCollapsed = false;
|
||||
route: string = "image-container"
|
||||
faBars = faBars;
|
||||
|
||||
// FontAwesome icons
|
||||
faWindowMaximize = faWindowMaximize;
|
||||
faUserCircle = faUserCircle;
|
||||
faCertificate = faCertificate;
|
||||
faHandPointer = faHandPointer;
|
||||
faIdCard = faIdCard;
|
||||
faTags = faTags;
|
||||
faCheckSquare = faCheckSquare;
|
||||
faKeyboard = faKeyboard;
|
||||
faThLarge = faThLarge;
|
||||
faList = faList;
|
||||
faDotCircle = faDotCircle;
|
||||
faSearch = faSearch;
|
||||
faToggleOn = faToggleOn;
|
||||
faTable = faTable;
|
||||
faImage = faImage;
|
||||
faImages = faImages;
|
||||
faPlay = faPlay;
|
||||
faEdit = faEdit;
|
||||
faEye = faEye;
|
||||
faCompass = faCompass;
|
||||
faVideo = faVideo;
|
||||
faComment = faComment;
|
||||
faMousePointer = faMousePointer;
|
||||
faLayerGroup = faLayerGroup;
|
||||
faSquare = faSquare;
|
||||
faCalendarDays = faCalendarDays;
|
||||
faClock = faClock;
|
||||
faGripVertical = faGripVertical;
|
||||
faArrowsAlt = faArrowsAlt;
|
||||
faBoxOpen = faBoxOpen;
|
||||
faChevronLeft = faChevronLeft;
|
||||
faSpinner = faSpinner;
|
||||
faExclamationTriangle = faExclamationTriangle;
|
||||
faCloudUploadAlt = faCloudUploadAlt;
|
||||
faFileText = faFileText;
|
||||
faListAlt = faListAlt;
|
||||
faCircle = faCircle;
|
||||
faExpandArrowsAlt = faExpandArrowsAlt;
|
||||
|
||||
menuItems: any = []
|
||||
|
||||
// Toggle sidebar method
|
||||
toggleSidebar() {
|
||||
this.sidebarCollapsed = !this.sidebarCollapsed;
|
||||
}
|
||||
|
||||
addMenuItem(id: string, label: string, icon?: any, children?: SidebarMenuItem[]): void {
|
||||
const menuItem: SidebarMenuItem = {
|
||||
id,
|
||||
label,
|
||||
icon,
|
||||
active: false,
|
||||
children,
|
||||
isParent: !!children,
|
||||
expanded: false
|
||||
};
|
||||
this.menuItems.push(menuItem);
|
||||
}
|
||||
|
||||
createChildItem(id: string, label: string, icon?: any): SidebarMenuItem {
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
icon,
|
||||
active: false,
|
||||
isParent: false
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
// Forms category
|
||||
const formsChildren = [
|
||||
this.createChildItem("input", "Input Fields", this.faKeyboard),
|
||||
this.createChildItem("checkbox", "Checkboxes", this.faCheckSquare),
|
||||
this.createChildItem("radio", "Radio Buttons", this.faDotCircle),
|
||||
this.createChildItem("search", "Search Bars", this.faSearch),
|
||||
this.createChildItem("switch", "Switches", this.faToggleOn),
|
||||
this.createChildItem("autocomplete", "Autocomplete", this.faListAlt),
|
||||
this.createChildItem("date-picker", "Date Picker", this.faCalendarDays),
|
||||
this.createChildItem("time-picker", "Time Picker", this.faClock),
|
||||
this.createChildItem("file-upload", "File Upload", this.faCloudUploadAlt),
|
||||
this.createChildItem("form-field", "Form Field", this.faFileText)
|
||||
];
|
||||
this.addMenuItem("forms", "Forms", this.faEdit, formsChildren);
|
||||
|
||||
// Data Display category
|
||||
const dataDisplayChildren = [
|
||||
this.createChildItem("table", "Tables", this.faTable),
|
||||
this.createChildItem("lists", "Lists", this.faList),
|
||||
this.createChildItem("progress", "Progress Bars", this.faCheckSquare),
|
||||
this.createChildItem("badge", "Badges", this.faCertificate),
|
||||
this.createChildItem("avatar", "Avatars", this.faUserCircle),
|
||||
this.createChildItem("cards", "Cards", this.faIdCard)
|
||||
];
|
||||
this.addMenuItem("data-display", "Data Display", this.faEye, dataDisplayChildren);
|
||||
|
||||
// Navigation category
|
||||
const navigationChildren = [
|
||||
this.createChildItem("appbar", "App Bars", this.faWindowMaximize),
|
||||
this.createChildItem("menu", "Menus", this.faBars),
|
||||
this.createChildItem("pagination", "Pagination", this.faChevronLeft)
|
||||
];
|
||||
this.addMenuItem("navigation", "Navigation", this.faCompass, navigationChildren);
|
||||
|
||||
// Media category
|
||||
const mediaChildren = [
|
||||
this.createChildItem("image-container", "Image Container", this.faImage),
|
||||
this.createChildItem("carousel", "Carousel", this.faImages),
|
||||
this.createChildItem("video-player", "Video Player", this.faPlay)
|
||||
];
|
||||
this.addMenuItem("media", "Media", this.faVideo, mediaChildren);
|
||||
|
||||
// Actions category
|
||||
const actionsChildren = [
|
||||
this.createChildItem("buttons", "Buttons", this.faMousePointer)
|
||||
];
|
||||
this.addMenuItem("actions", "Actions", this.faMousePointer, actionsChildren);
|
||||
|
||||
// Feedback category
|
||||
const feedbackChildren = [
|
||||
this.createChildItem("chips", "Chips", this.faTags),
|
||||
this.createChildItem("loading-spinner", "Loading Spinner", this.faSpinner),
|
||||
this.createChildItem("skeleton-loader", "Skeleton Loader", this.faSpinner),
|
||||
this.createChildItem("empty-state", "Empty State", this.faExclamationTriangle)
|
||||
];
|
||||
this.addMenuItem("feedback", "Feedback", this.faComment, feedbackChildren);
|
||||
|
||||
// Layout category
|
||||
const layoutChildren = [
|
||||
this.createChildItem("layout", "Layout Demos", this.faThLarge),
|
||||
this.createChildItem("grid-system", "Grid System", this.faGripVertical),
|
||||
this.createChildItem("spacer", "Spacer", this.faArrowsAlt),
|
||||
this.createChildItem("container", "Container", this.faBoxOpen)
|
||||
];
|
||||
this.addMenuItem("layouts", "Layouts", this.faLayerGroup, layoutChildren);
|
||||
|
||||
// Overlays category
|
||||
const overlaysChildren = [
|
||||
this.createChildItem("modal", "Modal/Dialog", this.faSquare),
|
||||
this.createChildItem("drawer", "Drawer/Sidebar", this.faBars),
|
||||
this.createChildItem("backdrop", "Backdrop", this.faCircle),
|
||||
this.createChildItem("overlay-container", "Overlay Container", this.faExpandArrowsAlt)
|
||||
];
|
||||
this.addMenuItem("overlays", "Overlays", this.faLayerGroup, overlaysChildren);
|
||||
|
||||
// Utilities (standalone)
|
||||
this.addMenuItem("fontawesome", "FontAwesome Icons", this.faCheckSquare);
|
||||
}
|
||||
|
||||
menuClick(id: string) {
|
||||
console.log(JSON.stringify(id))
|
||||
|
||||
this.route = id.toString()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
@use '../../../../../shared-ui/src/styles/semantic' as semantic;
|
||||
|
||||
.sidebar-container {
|
||||
padding: 1rem 0;
|
||||
|
||||
.menu-item-wrapper {
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
.parent-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
margin: 0 0.5rem;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: semantic.$semantic-color-surface-interactive;
|
||||
}
|
||||
|
||||
.parent-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
|
||||
.parent-icon {
|
||||
font-size: 1rem;
|
||||
color: semantic.$semantic-color-text-secondary;
|
||||
}
|
||||
|
||||
.parent-label {
|
||||
font-weight: 500;
|
||||
color: semantic.$semantic-color-text-primary;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
font-size: 0.75rem;
|
||||
color: semantic.$semantic-color-text-tertiary;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.children-container {
|
||||
margin-left: 1rem;
|
||||
padding-left: 1rem;
|
||||
border-left: 2px solid semantic.$semantic-color-border-secondary;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
::ng-deep .child-menu-item {
|
||||
margin-bottom: 0.125rem;
|
||||
|
||||
ui-menu-item {
|
||||
--menu-item-padding: 0.5rem 1rem;
|
||||
--menu-item-font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Component, EventEmitter, Output, Input, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { MenuItemComponent } from '../../../../../ui-essentials/src/lib/components/navigation/menu';
|
||||
import { faChevronRight, faChevronDown } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export interface SidebarMenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: any; // FontAwesome icon definition
|
||||
active: boolean;
|
||||
children?: SidebarMenuItem[];
|
||||
expanded?: boolean;
|
||||
isParent?: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'skyui-dashboard-sidebar',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FontAwesomeModule,
|
||||
MenuItemComponent
|
||||
],
|
||||
template: `
|
||||
<div class="sidebar-container">
|
||||
@for(item of data; track item.id) {
|
||||
<div class="menu-item-wrapper">
|
||||
@if(item.isParent) {
|
||||
<div class="parent-menu-item" (click)="toggleExpanded(item)">
|
||||
<div class="parent-content">
|
||||
<fa-icon [icon]="item.icon" class="parent-icon"></fa-icon>
|
||||
<span class="parent-label">{{ item.label }}</span>
|
||||
</div>
|
||||
<fa-icon
|
||||
[icon]="item.expanded ? faChevronDown : faChevronRight"
|
||||
class="chevron-icon">
|
||||
</fa-icon>
|
||||
</div>
|
||||
@if(item.expanded && item.children) {
|
||||
<div class="children-container">
|
||||
@for(child of item.children; track child.id) {
|
||||
<ui-menu-item
|
||||
[data]="{ label: child.label, icon: child.icon, active: child.active }"
|
||||
[size]="'md'"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick(child.id)"
|
||||
class="child-menu-item"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<ui-menu-item
|
||||
[data]="{ label: item.label, icon: item.icon, active: item.active }"
|
||||
[size]="'md'"
|
||||
iconPosition="left"
|
||||
(itemClick)="handleMenuClick(item.id)"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./dashboard.sidebar.component.scss']
|
||||
|
||||
})
|
||||
export class DashboardSidebarComponent implements OnInit {
|
||||
|
||||
@Input() data: SidebarMenuItem[] = [];
|
||||
@Input() set active(value: string) { this.setActive(value) }
|
||||
@Output() selected = new EventEmitter<string>();
|
||||
|
||||
faChevronRight = faChevronRight;
|
||||
faChevronDown = faChevronDown;
|
||||
|
||||
handleMenuClick(id: string): void {
|
||||
this.setActive(id);
|
||||
this.selected.emit(id);
|
||||
}
|
||||
|
||||
setActive(id: string): void {
|
||||
this.setActiveRecursive(this.data, id);
|
||||
}
|
||||
|
||||
private setActiveRecursive(items: SidebarMenuItem[], id: string): void {
|
||||
items.forEach(item => {
|
||||
item.active = item.id === id;
|
||||
if (item.children) {
|
||||
this.setActiveRecursive(item.children, id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getActiveMenuItem(): SidebarMenuItem | undefined {
|
||||
return this.getActiveMenuItemRecursive(this.data);
|
||||
}
|
||||
|
||||
private getActiveMenuItemRecursive(items: SidebarMenuItem[]): SidebarMenuItem | undefined {
|
||||
for (const item of items) {
|
||||
if (item.active) return item;
|
||||
if (item.children) {
|
||||
const found = this.getActiveMenuItemRecursive(item.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
clearSelection(): void {
|
||||
this.clearSelectionRecursive(this.data);
|
||||
}
|
||||
|
||||
private clearSelectionRecursive(items: SidebarMenuItem[]): void {
|
||||
items.forEach(item => {
|
||||
item.active = false;
|
||||
if (item.children) {
|
||||
this.clearSelectionRecursive(item.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleExpanded(item: SidebarMenuItem): void {
|
||||
if (item.isParent) {
|
||||
item.expanded = !item.expanded;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// Layout Container Component - Main application shell layout
|
||||
.ui-layout-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
flex-flow: column;
|
||||
|
||||
// Top navigation bar
|
||||
&__top-bar {
|
||||
order: 1;
|
||||
flex: 0 0 64px; // Standard app bar height
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
transition: all 300ms cubic-bezier(0.2, 0.0, 0, 1.0);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Main content wrapper
|
||||
&__centre-wrapper {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
order: 2;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
|
||||
// Left sidebar navigation
|
||||
.left-navigation {
|
||||
order: 1;
|
||||
flex: 0 0 64px; // Collapsed sidebar width
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
transition: all 300ms cubic-bezier(0.2, 0.0, 0, 1.0);
|
||||
|
||||
// Navigation states
|
||||
&--hide {
|
||||
flex: 0 0 0;
|
||||
}
|
||||
|
||||
&--rail {
|
||||
flex: 0 0 80px; // Rail navigation width
|
||||
}
|
||||
|
||||
&--full {
|
||||
flex: 0 0 320px; // Expanded sidebar width
|
||||
}
|
||||
}
|
||||
|
||||
// Main content area
|
||||
.main-body {
|
||||
order: 2;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
// Inner header section
|
||||
&__header {
|
||||
order: 1;
|
||||
flex: 0 0 0;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
transition: all 300ms cubic-bezier(0.2, 0.0, 0, 1.0);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Inner content body
|
||||
&__body {
|
||||
order: 2;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// Inner footer section
|
||||
&__footer {
|
||||
order: 3;
|
||||
flex: 0 0 0;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
transition: all 300ms cubic-bezier(0.2, 0.0, 0, 1.0);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
// Right sidebar navigation
|
||||
.right-navigation {
|
||||
order: 3;
|
||||
flex: 0 0 320px; // Right sidebar width
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
transition: all 300ms cubic-bezier(0.2, 0.0, 0, 1.0);
|
||||
|
||||
// Right navigation states
|
||||
&--hide {
|
||||
flex: 0 0 0;
|
||||
}
|
||||
|
||||
&--show {
|
||||
flex: 0 0 320px; // Shown right sidebar width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom bar section
|
||||
&__bottom-bar {
|
||||
order: 3;
|
||||
flex: 0 0 0;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
transition: all 300ms cubic-bezier(0.2, 0.0, 0, 1.0);
|
||||
}
|
||||
|
||||
// Responsive design for smaller screens
|
||||
@media (max-width: 768px) {
|
||||
&__centre-wrapper {
|
||||
.left-navigation {
|
||||
&--full {
|
||||
flex: 0 0 280px; // Smaller full width on mobile
|
||||
}
|
||||
}
|
||||
|
||||
.right-navigation {
|
||||
&--show {
|
||||
flex: 0 0 280px; // Smaller right sidebar on mobile
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile breakpoint - stack navigation vertically
|
||||
@media (max-width: 640px) {
|
||||
&__centre-wrapper {
|
||||
flex-direction: column;
|
||||
|
||||
.left-navigation,
|
||||
.right-navigation {
|
||||
&--full,
|
||||
&--show {
|
||||
flex: 0 0 64px; // Navigation becomes horizontal bar on mobile
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Component, ChangeDetectionStrategy, input, computed } from '@angular/core';
|
||||
|
||||
export type NavigationState = 'hide' | 'rail' | 'full';
|
||||
export type SidebarState = 'hide' | 'show';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-layout-container',
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
styleUrl: './layout-container.component.scss',
|
||||
template: `
|
||||
<div class="ui-layout-container">
|
||||
<!-- Top navigation bar -->
|
||||
<div class="ui-layout-container__top-bar"
|
||||
[style.flex-basis.px]="effectiveTopBarHeight()">
|
||||
<ng-content select="[slot=top-bar]"></ng-content>
|
||||
</div>
|
||||
|
||||
<!-- Main content wrapper -->
|
||||
<div class="ui-layout-container__centre-wrapper">
|
||||
<!-- Left sidebar navigation -->
|
||||
<div class="left-navigation"
|
||||
[class]="leftNavigationClasses()"
|
||||
[style.flex-basis.px]="leftNavigationWidth()">
|
||||
<ng-content select="[slot=left-navigation]"></ng-content>
|
||||
</div>
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="main-body">
|
||||
<!-- Inner header -->
|
||||
<div class="main-body__header"
|
||||
[style.flex-basis.px]="effectiveInnerHeaderHeight()">
|
||||
<ng-content select="[slot=inner-header]"></ng-content>
|
||||
</div>
|
||||
|
||||
<!-- Inner content body -->
|
||||
<div class="main-body__body">
|
||||
<ng-content select="[slot=content]"></ng-content>
|
||||
</div>
|
||||
|
||||
<!-- Inner footer -->
|
||||
<div class="main-body__footer"
|
||||
[style.flex-basis.px]="effectiveInnerFooterHeight()">
|
||||
<ng-content select="[slot=inner-footer]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right sidebar navigation -->
|
||||
<div class="right-navigation"
|
||||
[class]="rightNavigationClasses()"
|
||||
[style.flex-basis.px]="rightNavigationWidth()">
|
||||
<ng-content select="[slot=right-navigation]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom bar -->
|
||||
<div class="ui-layout-container__bottom-bar"
|
||||
[style.flex-basis.px]="effectiveBottomBarHeight()">
|
||||
<ng-content select="[slot=bottom-bar]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class LayoutContainerComponent {
|
||||
// Input signals for layout configuration
|
||||
leftNavigationState = input<NavigationState>('rail');
|
||||
rightNavigationState = input<SidebarState>('hide');
|
||||
|
||||
// Show/hide inputs
|
||||
showTopBar = input<boolean>(false);
|
||||
showInnerHeader = input<boolean>(false);
|
||||
showInnerFooter = input<boolean>(false);
|
||||
showBottomBar = input<boolean>(false);
|
||||
|
||||
// Size inputs (in pixels) - with sensible defaults
|
||||
topBarHeight = input<number>(64);
|
||||
innerHeaderHeight = input<number>(56);
|
||||
innerFooterHeight = input<number>(48);
|
||||
bottomBarHeight = input<number>(56);
|
||||
|
||||
// Custom width overrides
|
||||
customLeftWidth = input<number | null>(null);
|
||||
customRightWidth = input<number | null>(null);
|
||||
|
||||
// Computed CSS classes for navigation states
|
||||
leftNavigationClasses = computed(() => {
|
||||
const state = this.leftNavigationState();
|
||||
return `left-navigation--${state}`;
|
||||
});
|
||||
|
||||
rightNavigationClasses = computed(() => {
|
||||
const state = this.rightNavigationState();
|
||||
return `right-navigation--${state}`;
|
||||
});
|
||||
|
||||
// Computed widths based on state and custom overrides
|
||||
leftNavigationWidth = computed(() => {
|
||||
const customWidth = this.customLeftWidth();
|
||||
if (customWidth !== null) return customWidth;
|
||||
|
||||
const state = this.leftNavigationState();
|
||||
switch (state) {
|
||||
case 'hide': return 0;
|
||||
case 'rail': return 81;
|
||||
case 'full': return 320;
|
||||
default: return 64;
|
||||
}
|
||||
});
|
||||
|
||||
rightNavigationWidth = computed(() => {
|
||||
const customWidth = this.customRightWidth();
|
||||
if (customWidth !== null) return customWidth;
|
||||
|
||||
const state = this.rightNavigationState();
|
||||
switch (state) {
|
||||
case 'hide': return 0;
|
||||
case 'show': return 320;
|
||||
default: return 0;
|
||||
}
|
||||
});
|
||||
|
||||
// Computed effective heights (0 when hidden, configured height when shown)
|
||||
effectiveTopBarHeight = computed(() => {
|
||||
return this.showTopBar() ? this.topBarHeight() : 0;
|
||||
});
|
||||
|
||||
effectiveInnerHeaderHeight = computed(() => {
|
||||
return this.showInnerHeader() ? this.innerHeaderHeight() : 0;
|
||||
});
|
||||
|
||||
effectiveInnerFooterHeight = computed(() => {
|
||||
return this.showInnerFooter() ? this.innerFooterHeight() : 0;
|
||||
});
|
||||
|
||||
effectiveBottomBarHeight = computed(() => {
|
||||
return this.showBottomBar() ? this.bottomBarHeight() : 0;
|
||||
});
|
||||
}
|
||||
81
projects/demo-ui-essentials/src/scss/_variables.scss
Normal file
81
projects/demo-ui-essentials/src/scss/_variables.scss
Normal file
@@ -0,0 +1,81 @@
|
||||
// ==========================================================================
|
||||
// SKYUI PROJECT VARIABLES
|
||||
// ==========================================================================
|
||||
// Project-specific variable overrides that build upon the shared-ui token system
|
||||
// Import order: base tokens → semantic tokens → project overrides
|
||||
// NOTE: Semantic tokens are imported in styles.scss and available here
|
||||
// ==========================================================================
|
||||
|
||||
// ==========================================================================
|
||||
// FONT FAMILY OVERRIDES
|
||||
// ==========================================================================
|
||||
// Override base font families to use Comfortaa
|
||||
$base-typography-font-family-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
||||
$base-typography-font-family-display: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
||||
|
||||
@import '../../../shared-ui/src/styles/semantic';
|
||||
|
||||
// These overrides will cascade through all semantic typography tokens
|
||||
// that use $base-typography-font-family-sans and $base-typography-font-family-display
|
||||
|
||||
// ==========================================================================
|
||||
// PROJECT-SPECIFIC OVERRIDES
|
||||
// ==========================================================================
|
||||
// Custom values that override semantic tokens for SkyUI project needs
|
||||
// Use semantic tokens as base values to maintain design system consistency
|
||||
|
||||
// BRAND CUSTOMIZATION
|
||||
// Override brand colors while maintaining semantic structure
|
||||
$skyui-brand-primary: $semantic-color-brand-primary !default;
|
||||
$skyui-brand-secondary: $semantic-color-brand-secondary !default;
|
||||
$skyui-brand-accent: $semantic-color-brand-tertiary !default;
|
||||
|
||||
// SURFACE CUSTOMIZATION
|
||||
// Project-specific surface treatments
|
||||
$skyui-surface-elevated: $semantic-color-surface-elevated !default;
|
||||
$skyui-surface-sunken: $semantic-color-surface-sunken !default;
|
||||
$skyui-surface-interactive: $semantic-color-surface-interactive !default;
|
||||
|
||||
// TYPOGRAPHY SCALE ADJUSTMENTS
|
||||
// Semantic typography tokens can be overridden for project-specific needs
|
||||
$skyui-heading-display: $semantic-typography-heading-display !default;
|
||||
$skyui-heading-h1: $semantic-typography-heading-h1 !default;
|
||||
$skyui-heading-h2: $semantic-typography-heading-h2 !default;
|
||||
$skyui-body-large: $semantic-typography-body-large !default;
|
||||
$skyui-body-medium: $semantic-typography-body-medium !default;
|
||||
|
||||
// SPACING CUSTOMIZATION
|
||||
// Build on semantic spacing tokens
|
||||
$skyui-space-component: $semantic-spacing-component-md !default;
|
||||
$skyui-space-layout: $semantic-spacing-layout-lg !default;
|
||||
$skyui-space-section: $semantic-spacing-section-xl !default;
|
||||
|
||||
// INTERACTIVE ELEMENTS
|
||||
// Button and form element customizations
|
||||
$skyui-interactive-primary: $semantic-color-interactive-primary !default;
|
||||
$skyui-interactive-secondary: $semantic-color-interactive-secondary !default;
|
||||
$skyui-border-radius-component: $semantic-border-radius-md !default;
|
||||
$skyui-border-radius-container: $semantic-border-radius-lg !default;
|
||||
|
||||
// ELEVATION & SHADOWS
|
||||
// Project-specific shadow treatments
|
||||
$skyui-shadow-elevated: $semantic-shadow-elevated !default;
|
||||
$skyui-shadow-floating: $semantic-shadow-floating !default;
|
||||
|
||||
// ==========================================================================
|
||||
// COMPONENT-SPECIFIC OVERRIDES
|
||||
// ==========================================================================
|
||||
// Variables for specific component customizations
|
||||
|
||||
// Dashboard specific
|
||||
$skyui-dashboard-grid-gap: $semantic-spacing-layout-md !default;
|
||||
$skyui-dashboard-card-padding: $semantic-spacing-component-lg !default;
|
||||
|
||||
// Navigation specific
|
||||
$skyui-nav-height: 64px !default;
|
||||
$skyui-sidebar-width: 280px !default;
|
||||
$skyui-sidebar-collapsed-width: 72px !default;
|
||||
|
||||
// Form specific
|
||||
$skyui-input-border-radius: $semantic-border-radius-sm !default;
|
||||
$skyui-input-padding: $semantic-spacing-component-sm !default;
|
||||
380
projects/demo-ui-essentials/src/scss/overrides/_colors.scss
Normal file
380
projects/demo-ui-essentials/src/scss/overrides/_colors.scss
Normal file
@@ -0,0 +1,380 @@
|
||||
// ==========================================================================
|
||||
// SKYUI COLOR OVERRIDES
|
||||
// ==========================================================================
|
||||
// Project-specific color customizations built on semantic color tokens
|
||||
// Extends the shared design system with SkyUI-specific color patterns
|
||||
// ==========================================================================
|
||||
|
||||
// ==========================================================================
|
||||
// THEME COLOR UTILITIES
|
||||
// ==========================================================================
|
||||
// Utility classes for applying semantic colors consistently
|
||||
|
||||
.skyui-color {
|
||||
// Brand color utilities
|
||||
&--brand-primary {
|
||||
color: $semantic-color-brand-primary;
|
||||
}
|
||||
|
||||
&--brand-secondary {
|
||||
color: $semantic-color-brand-secondary;
|
||||
}
|
||||
|
||||
&--brand-accent {
|
||||
color: $semantic-color-brand-accent;
|
||||
}
|
||||
|
||||
// Text color utilities
|
||||
&--text-primary {
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
&--text-secondary {
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
|
||||
&--text-tertiary {
|
||||
color: $semantic-color-text-tertiary;
|
||||
}
|
||||
|
||||
&--text-inverse {
|
||||
color: $semantic-color-text-inverse;
|
||||
}
|
||||
|
||||
// Feedback colors
|
||||
&--success {
|
||||
color: $semantic-color-success;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
color: $semantic-color-warning;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
color: $semantic-color-danger;
|
||||
}
|
||||
|
||||
&--info {
|
||||
color: $semantic-color-info;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// BACKGROUND COLOR UTILITIES
|
||||
// ==========================================================================
|
||||
// Background color utilities using semantic tokens
|
||||
|
||||
.skyui-bg {
|
||||
// Surface backgrounds
|
||||
&--surface-primary {
|
||||
background-color: $semantic-color-surface-primary;
|
||||
}
|
||||
|
||||
&--surface-secondary {
|
||||
background-color: $semantic-color-surface-secondary;
|
||||
}
|
||||
|
||||
&--surface-elevated {
|
||||
background-color: $semantic-color-surface-elevated;
|
||||
}
|
||||
|
||||
&--surface-sunken {
|
||||
background-color: $semantic-color-surface-sunken;
|
||||
}
|
||||
|
||||
&--surface-interactive {
|
||||
background-color: $semantic-color-surface-interactive;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-surface-interactive-hover, $semantic-color-surface-interactive);
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
// Brand backgrounds
|
||||
&--brand-primary {
|
||||
background-color: $semantic-color-brand-primary;
|
||||
color: $semantic-color-on-brand-primary;
|
||||
}
|
||||
|
||||
&--brand-secondary {
|
||||
background-color: $semantic-color-brand-secondary;
|
||||
color: $semantic-color-on-brand-secondary;
|
||||
}
|
||||
|
||||
// Container backgrounds
|
||||
&--container-primary {
|
||||
background-color: $semantic-color-container-primary;
|
||||
color: $semantic-color-on-container-primary;
|
||||
}
|
||||
|
||||
&--container-secondary {
|
||||
background-color: $semantic-color-container-secondary;
|
||||
color: $semantic-color-on-container-secondary;
|
||||
}
|
||||
|
||||
// Feedback backgrounds
|
||||
&--success {
|
||||
background-color: var(--color-success-light, $semantic-color-success);
|
||||
filter: brightness(1.4) saturate(0.6);
|
||||
color: var(--color-success-dark, $semantic-color-success);
|
||||
border: 1px solid var(--color-success-border, $semantic-color-success);
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background-color: var(--color-warning-light, $semantic-color-warning);
|
||||
filter: brightness(1.4) saturate(0.6);
|
||||
color: var(--color-warning-dark, $semantic-color-warning);
|
||||
border: 1px solid var(--color-warning-border, $semantic-color-warning);
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background-color: var(--color-danger-light, $semantic-color-danger);
|
||||
filter: brightness(1.4) saturate(0.6);
|
||||
color: var(--color-danger-dark, $semantic-color-danger);
|
||||
border: 1px solid var(--color-danger-border, $semantic-color-danger);
|
||||
}
|
||||
|
||||
&--info {
|
||||
background-color: var(--color-info-light, $semantic-color-info);
|
||||
filter: brightness(1.4) saturate(0.6);
|
||||
color: var(--color-info-dark, $semantic-color-info);
|
||||
border: 1px solid var(--color-info-border, $semantic-color-info);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// BORDER COLOR UTILITIES
|
||||
// ==========================================================================
|
||||
// Border color utilities using semantic border tokens
|
||||
|
||||
.skyui-border {
|
||||
// Standard borders
|
||||
&--primary {
|
||||
border-color: $semantic-color-border-primary;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
border-color: $semantic-color-border-secondary;
|
||||
}
|
||||
|
||||
&--focus {
|
||||
border-color: $semantic-color-border-focus;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&--error {
|
||||
border-color: $semantic-color-border-error;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
border-color: $semantic-color-border-disabled;
|
||||
}
|
||||
|
||||
// Border styles
|
||||
&--solid {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
&--dashed {
|
||||
border-style: dashed;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
&--thick {
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// INTERACTIVE STATE COLORS
|
||||
// ==========================================================================
|
||||
// Color variations for interactive elements
|
||||
|
||||
.skyui-interactive {
|
||||
// Primary interactive states
|
||||
&--primary {
|
||||
color: $semantic-color-interactive-primary;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary-hover, $semantic-color-interactive-primary);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-primary-active, $semantic-color-interactive-primary);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: $semantic-color-interactive-primary;
|
||||
outline: 2px solid $semantic-color-focus-ring;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary interactive states
|
||||
&--secondary {
|
||||
color: $semantic-color-interactive-secondary;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-secondary-hover, $semantic-color-interactive-secondary);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-secondary-active, $semantic-color-interactive-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
// Tertiary interactive states
|
||||
&--tertiary {
|
||||
color: $semantic-color-interactive-tertiary;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-tertiary-hover, $semantic-color-interactive-tertiary);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-tertiary-active, $semantic-color-interactive-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
// Neutral interactive states
|
||||
&--neutral {
|
||||
color: $semantic-color-text-secondary;
|
||||
|
||||
&:hover {
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// COMPONENT-SPECIFIC COLORS
|
||||
// ==========================================================================
|
||||
// Color patterns for specific SkyUI components
|
||||
|
||||
// Dashboard specific colors
|
||||
.skyui-dashboard {
|
||||
&__card {
|
||||
background-color: $semantic-color-surface-elevated;
|
||||
border: 1px solid $semantic-color-border-secondary;
|
||||
box-shadow: $semantic-shadow-elevated;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-floating;
|
||||
}
|
||||
}
|
||||
|
||||
&__metric {
|
||||
&--positive {
|
||||
color: $semantic-color-success;
|
||||
}
|
||||
|
||||
&--negative {
|
||||
color: $semantic-color-danger;
|
||||
}
|
||||
|
||||
&--neutral {
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation specific colors
|
||||
.skyui-navigation {
|
||||
&__item {
|
||||
color: $semantic-color-text-secondary;
|
||||
|
||||
&:hover {
|
||||
color: $semantic-color-text-primary;
|
||||
background-color: $semantic-color-surface-interactive;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $semantic-color-brand-primary;
|
||||
background-color: var(--color-brand-primary-light, $semantic-color-brand-primary);
|
||||
font-weight: $base-typography-font-weight-medium;
|
||||
}
|
||||
}
|
||||
|
||||
&__divider {
|
||||
background-color: $semantic-color-border-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
// Form specific colors
|
||||
.skyui-form {
|
||||
&__input {
|
||||
background-color: $semantic-color-surface-primary;
|
||||
border-color: $semantic-color-border-secondary;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
&:focus {
|
||||
border-color: $semantic-color-border-focus;
|
||||
box-shadow: 0 0 0 3px $semantic-color-focus-ring;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: $semantic-color-border-error;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: $semantic-color-surface-disabled;
|
||||
border-color: $semantic-color-border-disabled;
|
||||
color: $semantic-color-text-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
&.required {
|
||||
&::after {
|
||||
color: $semantic-color-danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// STATUS AND NOTIFICATION COLORS
|
||||
// ==========================================================================
|
||||
// Color patterns for status indicators and notifications
|
||||
|
||||
.skyui-status {
|
||||
&--online {
|
||||
color: $semantic-color-success;
|
||||
|
||||
&::before {
|
||||
content: '●';
|
||||
margin-right: $semantic-spacing-component-2xs;
|
||||
}
|
||||
}
|
||||
|
||||
&--offline {
|
||||
color: $semantic-color-danger;
|
||||
|
||||
&::before {
|
||||
content: '●';
|
||||
margin-right: $semantic-spacing-component-2xs;
|
||||
}
|
||||
}
|
||||
|
||||
&--pending {
|
||||
color: $semantic-color-warning;
|
||||
|
||||
&::before {
|
||||
content: '●';
|
||||
margin-right: $semantic-spacing-component-2xs;
|
||||
}
|
||||
}
|
||||
|
||||
&--inactive {
|
||||
color: $semantic-color-text-tertiary;
|
||||
|
||||
&::before {
|
||||
content: '●';
|
||||
margin-right: $semantic-spacing-component-2xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
projects/demo-ui-essentials/src/scss/overrides/_index.scss
Normal file
12
projects/demo-ui-essentials/src/scss/overrides/_index.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
// ==========================================================================
|
||||
// SKYUI OVERRIDES INDEX
|
||||
// ==========================================================================
|
||||
// Centralized import for all SkyUI project overrides
|
||||
// Import order maintains proper cascade and dependency resolution
|
||||
// ==========================================================================
|
||||
|
||||
// Typography overrides - semantic typography token extensions
|
||||
@import 'typography';
|
||||
|
||||
// Color overrides - semantic color token extensions
|
||||
@import 'colors';
|
||||
256
projects/demo-ui-essentials/src/scss/overrides/_typography.scss
Normal file
256
projects/demo-ui-essentials/src/scss/overrides/_typography.scss
Normal file
@@ -0,0 +1,256 @@
|
||||
// ==========================================================================
|
||||
// SKYUI TYPOGRAPHY OVERRIDES
|
||||
// ==========================================================================
|
||||
// Project-specific typography customizations built on semantic typography tokens
|
||||
// Extends the shared design system with SkyUI-specific typography patterns
|
||||
// ==========================================================================
|
||||
|
||||
// Typography utility mixins that use semantic tokens
|
||||
@mixin apply-typography($typography-map) {
|
||||
font-family: map-get($typography-map, font-family);
|
||||
font-size: map-get($typography-map, font-size);
|
||||
font-weight: map-get($typography-map, font-weight);
|
||||
line-height: map-get($typography-map, line-height);
|
||||
letter-spacing: map-get($typography-map, letter-spacing);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// HEADING STYLES
|
||||
// ==========================================================================
|
||||
// Custom heading treatments that build on semantic typography
|
||||
|
||||
.skyui-heading {
|
||||
// Display heading for hero sections
|
||||
&--display {
|
||||
@include apply-typography($semantic-typography-heading-display);
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
// Project-specific enhancements
|
||||
text-align: center;
|
||||
margin-bottom: $semantic-spacing-component-xl;
|
||||
|
||||
@media (max-width: $base-breakpoint-md) {
|
||||
font-size: $base-typography-font-size-2xl;
|
||||
}
|
||||
}
|
||||
|
||||
// Primary headings
|
||||
&--h1 {
|
||||
@include apply-typography($semantic-typography-heading-h1);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
|
||||
// SkyUI specific styling
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -#{$semantic-spacing-component-xs};
|
||||
left: 0;
|
||||
width: 60px;
|
||||
height: 3px;
|
||||
background-color: $semantic-color-brand-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary headings
|
||||
&--h2 {
|
||||
@include apply-typography($semantic-typography-heading-h2);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
margin-top: $semantic-spacing-component-xl;
|
||||
}
|
||||
|
||||
// Tertiary headings
|
||||
&--h3 {
|
||||
@include apply-typography($semantic-typography-heading-h3);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
margin-top: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
// Quaternary headings
|
||||
&--h4 {
|
||||
@include apply-typography($semantic-typography-heading-h4);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
margin-top: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// BODY TEXT STYLES
|
||||
// ==========================================================================
|
||||
// Content text styles using semantic typography tokens
|
||||
|
||||
.skyui-text {
|
||||
// Large body text for important content
|
||||
&--large {
|
||||
@include apply-typography($semantic-typography-body-large);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
// Standard body text
|
||||
&--medium {
|
||||
@include apply-typography($semantic-typography-body-medium);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
// Small body text for captions, etc.
|
||||
&--small {
|
||||
@include apply-typography($semantic-typography-body-small);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
// Caption text
|
||||
&--caption {
|
||||
@include apply-typography($semantic-typography-caption);
|
||||
color: $semantic-color-text-tertiary;
|
||||
}
|
||||
|
||||
// Muted/secondary text
|
||||
&--muted {
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
|
||||
// Emphasized text
|
||||
&--emphasis {
|
||||
color: $semantic-color-brand-primary;
|
||||
font-weight: $base-typography-font-weight-medium;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// INTERACTIVE TEXT STYLES
|
||||
// ==========================================================================
|
||||
// Text styles for buttons, links, and interactive elements
|
||||
|
||||
.skyui-interactive-text {
|
||||
// Button text styles
|
||||
&--button-large {
|
||||
@include apply-typography($semantic-typography-button-large);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&--button-medium {
|
||||
@include apply-typography($semantic-typography-button-medium);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&--button-small {
|
||||
@include apply-typography($semantic-typography-button-small);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
// Link styles
|
||||
&--link {
|
||||
@include apply-typography($semantic-typography-body-medium);
|
||||
color: $semantic-color-interactive-primary;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary-hover, $semantic-color-interactive-primary);
|
||||
filter: brightness(0.9);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $semantic-color-border-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation text
|
||||
&--nav-primary {
|
||||
@include apply-typography($semantic-typography-nav-primary);
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
&.active {
|
||||
color: $semantic-color-brand-primary;
|
||||
font-weight: $base-typography-font-weight-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
&--nav-secondary {
|
||||
@include apply-typography($semantic-typography-nav-secondary);
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// FORM TEXT STYLES
|
||||
// ==========================================================================
|
||||
// Text styles for form elements using semantic tokens
|
||||
|
||||
.skyui-form-text {
|
||||
// Labels
|
||||
&--label {
|
||||
@include apply-typography($semantic-typography-label);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
display: block;
|
||||
|
||||
&.required::after {
|
||||
content: ' *';
|
||||
color: $semantic-color-danger;
|
||||
}
|
||||
}
|
||||
|
||||
// Input text
|
||||
&--input {
|
||||
@include apply-typography($semantic-typography-input);
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
&::placeholder {
|
||||
@include apply-typography($semantic-typography-placeholder);
|
||||
color: $semantic-color-text-tertiary;
|
||||
}
|
||||
}
|
||||
|
||||
// Help text
|
||||
&--help {
|
||||
@include apply-typography($semantic-typography-caption);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-top: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
// Error text
|
||||
&--error {
|
||||
@include apply-typography($semantic-typography-caption);
|
||||
color: $semantic-color-danger;
|
||||
margin-top: $semantic-spacing-component-xs;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// CODE TEXT STYLES
|
||||
// ==========================================================================
|
||||
// Code and monospace text styles
|
||||
|
||||
.skyui-code {
|
||||
// Inline code
|
||||
&--inline {
|
||||
@include apply-typography($semantic-typography-code-inline);
|
||||
background-color: $semantic-color-surface-secondary;
|
||||
color: $semantic-color-text-primary;
|
||||
padding: $semantic-spacing-component-2xs $semantic-spacing-component-xs;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
border: 1px solid $semantic-color-border-secondary;
|
||||
}
|
||||
|
||||
// Code block
|
||||
&--block {
|
||||
@include apply-typography($semantic-typography-code-block);
|
||||
background-color: $semantic-color-surface-sunken;
|
||||
color: $semantic-color-text-primary;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: 1px solid $semantic-color-border-secondary;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
@@ -1 +1,35 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
|
||||
// Set font paths before importing fontfaces
|
||||
$inter-font-path: "/fonts/inter/" !default;
|
||||
$comfortaa-font-path: "/fonts/comfortaa/" !default;
|
||||
|
||||
// Import specific fonts we need for the demo
|
||||
@import '../../shared-ui/src/styles/fontfaces/inter';
|
||||
@import '../../shared-ui/src/styles/fontfaces/comfortaa';
|
||||
|
||||
// Import project variables (which now has semantic tokens available)
|
||||
@import 'scss/_variables';
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
font-size: 16px; // Base font size for rem calculations
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
background-color: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
// Apply base typography using semantic tokens
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
|
||||
// Improve text rendering
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ $base-spacing-2-5: 0.625rem;
|
||||
$base-spacing-3: 0.75rem;
|
||||
$base-spacing-3-5: 0.875rem;
|
||||
$base-spacing-4: 1rem;
|
||||
$base-spacing-4-5: 1.125rem;
|
||||
$base-spacing-5: 1.25rem;
|
||||
$base-spacing-6: 1.5rem;
|
||||
$base-spacing-7: 1.75rem;
|
||||
|
||||
@@ -9,19 +9,19 @@
|
||||
@forward '../base';
|
||||
|
||||
// Forward semantic token categories
|
||||
@forward 'colors/index';
|
||||
@forward 'typography/index';
|
||||
@forward 'spacing/index';
|
||||
@forward 'motion/index';
|
||||
@forward 'colors';
|
||||
@forward 'typography';
|
||||
@forward 'spacing';
|
||||
@forward 'motion';
|
||||
//@forward 'elevation/index';
|
||||
//@forward 'layout/index';
|
||||
//@forward 'breakpoints/index';
|
||||
|
||||
// Forward additional semantic categories
|
||||
@forward 'borders/index';
|
||||
@forward 'shadows/index';
|
||||
@forward 'sizing/index';
|
||||
@forward 'glass/index';
|
||||
@forward 'z-index/index';
|
||||
@forward 'opacity/index';
|
||||
@forward 'borders';
|
||||
@forward 'shadows';
|
||||
@forward 'sizing';
|
||||
@forward 'glass';
|
||||
@forward 'z-index';
|
||||
@forward 'opacity';
|
||||
|
||||
|
||||
@@ -7,4 +7,4 @@
|
||||
// Note: base tokens imported at semantic/index level
|
||||
|
||||
// Semantic spacing mappings
|
||||
@import 'semantic-spacing';
|
||||
@forward 'semantic-spacing';
|
||||
@@ -114,11 +114,12 @@ $semantic-spacing-2-5: $base-spacing-2-5 !default;
|
||||
$semantic-spacing-3: $base-spacing-3 !default;
|
||||
$semantic-spacing-3-5: $base-spacing-3-5 !default;
|
||||
$semantic-spacing-4: $base-spacing-4 !default;
|
||||
$semantic-spacing-4-5: $base-spacing-4-5 !default;
|
||||
$semantic-spacing-5: $base-spacing-5 !default;
|
||||
$semantic-spacing-6: $base-spacing-6 !default;
|
||||
$semantic-spacing-7: $base-spacing-7 !default;
|
||||
$semantic-spacing-8: $base-spacing-8 !default;
|
||||
$semantic-spacing-9: $base-spacing-9 !default;
|
||||
$semantic-spacing-9: $base-spacing-9 !default;
|
||||
$semantic-spacing-10: $base-spacing-10 !default;
|
||||
$semantic-spacing-11: $base-spacing-11 !default;
|
||||
$semantic-spacing-12: $base-spacing-12 !default;
|
||||
|
||||
@@ -11,4 +11,4 @@
|
||||
// ==========================================================================
|
||||
|
||||
// Forward complete semantic layer (includes base tokens)
|
||||
@forward 'semantic/index";
|
||||
@forward 'semantic/index';
|
||||
@@ -0,0 +1,303 @@
|
||||
@use "../../../../../shared-ui/src/styles/semantic/index" as *;
|
||||
.ui-fab {
|
||||
// Reset and base styles
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// Interaction states
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
box-sizing: border-box;
|
||||
|
||||
// Transitions
|
||||
transition: all $semantic-motion-duration-normal $semantic-motion-easing-ease-out;
|
||||
|
||||
// Size variants
|
||||
&--small {
|
||||
width: $semantic-spacing-10; // 2.5rem
|
||||
height: $semantic-spacing-10; // 2.5rem
|
||||
box-shadow: $semantic-shadow-card-hover;
|
||||
|
||||
.fab-icon {
|
||||
font-size: $semantic-typography-font-size-lg; // 1.125rem
|
||||
}
|
||||
|
||||
.fab-touch-overlay {
|
||||
width: $semantic-spacing-4; // 1rem
|
||||
height: $semantic-spacing-4; // 1rem
|
||||
top: $semantic-spacing-3; // 0.75rem
|
||||
left: $semantic-spacing-3; // 0.75rem
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
}
|
||||
|
||||
.fab-spinner {
|
||||
width: $semantic-spacing-4; // 1rem
|
||||
height: $semantic-spacing-4; // 1rem
|
||||
border-width: $semantic-border-width-2; // 2px (closest to 1.5px)
|
||||
}
|
||||
}
|
||||
|
||||
&--medium {
|
||||
width: $semantic-spacing-14; // 3.5rem
|
||||
height: $semantic-spacing-14; // 3.5rem
|
||||
box-shadow: $semantic-shadow-card-active;
|
||||
|
||||
.fab-icon {
|
||||
font-size: $semantic-typography-font-size-2xl; // 1.5rem
|
||||
}
|
||||
|
||||
.fab-touch-overlay {
|
||||
width: $semantic-spacing-5; // 1.25rem
|
||||
height: $semantic-spacing-5; // 1.25rem
|
||||
top: $semantic-spacing-4-5; // ~1.125rem (closest available)
|
||||
left: $semantic-spacing-4-5; // ~1.125rem (closest available)
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
}
|
||||
|
||||
.fab-spinner {
|
||||
width: $semantic-spacing-5; // 1.25rem
|
||||
height: $semantic-spacing-5; // 1.25rem
|
||||
border-width: $semantic-border-width-2;
|
||||
}
|
||||
}
|
||||
|
||||
&--large {
|
||||
width: $semantic-spacing-24; // 6rem
|
||||
height: $semantic-spacing-24; // 6rem
|
||||
box-shadow: $semantic-shadow-card-featured;
|
||||
|
||||
.fab-icon {
|
||||
font-size: $semantic-typography-font-size-5xl; // 3rem (closest to 2.5rem)
|
||||
}
|
||||
|
||||
.fab-touch-overlay {
|
||||
width: $semantic-spacing-10; // 2.5rem
|
||||
height: $semantic-spacing-10; // 2.5rem
|
||||
top: $semantic-spacing-7; // 1.75rem
|
||||
left: $semantic-spacing-7; // 1.75rem
|
||||
border-radius: $semantic-border-radius-xl;
|
||||
}
|
||||
|
||||
.fab-spinner {
|
||||
width: $semantic-spacing-8; // 2rem
|
||||
height: $semantic-spacing-8; // 2rem
|
||||
border-width: $semantic-border-width-3;
|
||||
}
|
||||
}
|
||||
|
||||
// Color variants
|
||||
&--primary {
|
||||
background-color: $semantic-color-interactive-primary;
|
||||
color: $semantic-color-on-brand-primary;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $semantic-color-brand-primary;
|
||||
transform: $semantic-motion-hover-transform-scale-md;
|
||||
box-shadow: $semantic-shadow-elevation-4;
|
||||
}
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background-color: $semantic-color-interactive-secondary;
|
||||
color: $semantic-color-on-brand-secondary;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $semantic-color-brand-secondary;
|
||||
transform: $semantic-motion-hover-transform-scale-md;
|
||||
box-shadow: $semantic-shadow-elevation-4;
|
||||
}
|
||||
}
|
||||
|
||||
&--tertiary {
|
||||
background-color: $semantic-color-interactive-tertiary;
|
||||
color: $semantic-color-on-brand-tertiary;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $semantic-color-brand-tertiary;
|
||||
transform: $semantic-motion-hover-transform-scale-md;
|
||||
box-shadow: $semantic-shadow-elevation-4;
|
||||
}
|
||||
}
|
||||
|
||||
// Position variants
|
||||
&--bottom-right {
|
||||
position: fixed;
|
||||
bottom: $semantic-spacing-8; // 2rem
|
||||
right: $semantic-spacing-6; // 1.5rem
|
||||
z-index: $semantic-z-index-toast; // 1080
|
||||
}
|
||||
|
||||
&--bottom-left {
|
||||
position: fixed;
|
||||
bottom: $semantic-spacing-8; // 2rem
|
||||
left: $semantic-spacing-6; // 1.5rem
|
||||
z-index: $semantic-z-index-toast; // 1080
|
||||
}
|
||||
|
||||
&--top-right {
|
||||
position: fixed;
|
||||
top: $semantic-spacing-8; // 2rem
|
||||
right: $semantic-spacing-6; // 1.5rem
|
||||
z-index: $semantic-z-index-toast; // 1080
|
||||
}
|
||||
|
||||
&--top-left {
|
||||
position: fixed;
|
||||
top: $semantic-spacing-8; // 2rem
|
||||
left: $semantic-spacing-6; // 1.5rem
|
||||
z-index: $semantic-z-index-toast; // 1080
|
||||
}
|
||||
|
||||
// States
|
||||
&:active:not(:disabled) {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow:
|
||||
$semantic-shadow-button-focus,
|
||||
$semantic-shadow-card-active;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: $semantic-opacity-disabled;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
|
||||
&--loading {
|
||||
cursor: wait;
|
||||
|
||||
.fab-icon {
|
||||
opacity: $semantic-opacity-invisible;
|
||||
}
|
||||
}
|
||||
|
||||
// Pulse effect
|
||||
&--pulse {
|
||||
overflow: visible;
|
||||
|
||||
.fab-pulse {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: inherit;
|
||||
border-radius: inherit;
|
||||
animation: fab-pulse 2s infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fab-pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.4);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Expand animation
|
||||
&--expanding {
|
||||
animation: fab-expand 300ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@keyframes fab-expand {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Touch overlay for ripple effect
|
||||
.fab-touch-overlay {
|
||||
position: absolute;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Icon container
|
||||
.fab-icon {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
// Loader styles
|
||||
.fab-loader {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.fab-spinner {
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 2px solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// Ripple effect
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, transparent 70%);
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
&:active:not(:disabled)::after {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
// Extended FAB support (for future use)
|
||||
&--extended {
|
||||
border-radius: $semantic-border-radius-pill; // 1.75rem equivalent
|
||||
padding: 0 $semantic-spacing-4; // 1rem
|
||||
width: auto;
|
||||
min-width: $semantic-spacing-20; // 5rem
|
||||
height: $semantic-spacing-14; // 3.5rem
|
||||
|
||||
.fab-icon {
|
||||
margin-right: $semantic-spacing-1-5; // 0.375rem
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
export type FabSize = 'small' | 'medium' | 'large';
|
||||
export type FabVariant = 'primary' | 'secondary' | 'tertiary';
|
||||
export type FabPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'static';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-fab',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<button
|
||||
[class]="fabClasses"
|
||||
[disabled]="disabled || loading"
|
||||
[type]="type"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
@if (loading) {
|
||||
<div class="fab-loader">
|
||||
<div class="fab-spinner"></div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="fab-icon">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (pulse) {
|
||||
<div class="fab-pulse"></div>
|
||||
}
|
||||
|
||||
<div class="fab-touch-overlay"></div>
|
||||
</button>
|
||||
`,
|
||||
styleUrl: './fab.component.scss'
|
||||
})
|
||||
export class FabComponent {
|
||||
@Input() variant: FabVariant = 'primary';
|
||||
@Input() size: FabSize = 'medium';
|
||||
@Input() disabled: boolean = false;
|
||||
@Input() loading: boolean = false;
|
||||
@Input() type: 'button' | 'submit' | 'reset' = 'button';
|
||||
@Input() position: FabPosition = 'static';
|
||||
@Input() pulse: boolean = false;
|
||||
@Input() expandOnClick: boolean = false;
|
||||
@Input() class: string = '';
|
||||
|
||||
@Output() clicked = new EventEmitter<Event>();
|
||||
|
||||
get fabClasses(): string {
|
||||
return [
|
||||
'ui-fab',
|
||||
`ui-fab--${this.variant}`,
|
||||
`ui-fab--${this.size}`,
|
||||
`ui-fab--${this.position}`,
|
||||
this.disabled ? 'ui-fab--disabled' : '',
|
||||
this.loading ? 'ui-fab--loading' : '',
|
||||
this.pulse ? 'ui-fab--pulse' : '',
|
||||
this.expandOnClick ? 'ui-fab--expandable' : '',
|
||||
this.class
|
||||
].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
handleClick(event: Event): void {
|
||||
if (!this.disabled && !this.loading) {
|
||||
this.clicked.emit(event);
|
||||
|
||||
if (this.expandOnClick) {
|
||||
// Add animation class for expand effect
|
||||
(event.target as HTMLElement).classList.add('ui-fab--expanding');
|
||||
setTimeout(() => {
|
||||
(event.target as HTMLElement).classList.remove('ui-fab--expanding');
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
@use "../../../../../shared-ui/src/styles/semantic/index" as *;
|
||||
.ui-ghost-button {
|
||||
// Reset and base styles
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: $semantic-border-width-1 solid transparent;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
|
||||
// Typography
|
||||
font-family: $semantic-typography-font-family-sans;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
text-align: center;
|
||||
text-transform: none;
|
||||
letter-spacing: $semantic-typography-letter-spacing-wide;
|
||||
white-space: nowrap;
|
||||
color: $semantic-color-text-secondary;
|
||||
|
||||
// Interaction states
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
box-sizing: border-box;
|
||||
|
||||
// Transitions
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
|
||||
|
||||
// Size variants
|
||||
&--small {
|
||||
height: $semantic-spacing-9; // 2.25rem
|
||||
padding: 0 $semantic-spacing-2; // 0.5rem
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
line-height: $semantic-typography-line-height-normal;
|
||||
min-width: $semantic-spacing-16; // 4rem
|
||||
}
|
||||
|
||||
&--medium {
|
||||
height: $semantic-spacing-11; // 2.75rem
|
||||
padding: 0 $semantic-spacing-4; // 1rem
|
||||
font-size: $semantic-typography-font-size-md;
|
||||
line-height: $semantic-typography-line-height-normal;
|
||||
min-width: $semantic-spacing-20; // 5rem
|
||||
}
|
||||
|
||||
&--large {
|
||||
height: $semantic-spacing-12; // 3rem (closest to 3.25rem)
|
||||
padding: 0 $semantic-spacing-6; // 1.5rem
|
||||
font-size: $semantic-typography-font-size-lg;
|
||||
line-height: $semantic-typography-line-height-normal;
|
||||
min-width: $semantic-spacing-24; // 6rem
|
||||
}
|
||||
|
||||
// Hover states
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $semantic-color-surface-container;
|
||||
color: $semantic-color-text-primary;
|
||||
border-color: $semantic-color-border-secondary;
|
||||
transform: $semantic-motion-hover-transform-lift-sm;
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $semantic-color-surface-high;
|
||||
transform: scale(0.98);
|
||||
box-shadow: $semantic-shadow-none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
background-color: $semantic-color-surface-container;
|
||||
border-color: $semantic-color-border-focus;
|
||||
box-shadow: $semantic-shadow-button-focus;
|
||||
}
|
||||
|
||||
// States
|
||||
&--disabled {
|
||||
opacity: $semantic-opacity-disabled;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&--loading {
|
||||
cursor: wait;
|
||||
|
||||
.ghost-button-content {
|
||||
opacity: $semantic-opacity-invisible;
|
||||
}
|
||||
}
|
||||
|
||||
&--full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Loader styles
|
||||
.ghost-button-loader {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.ghost-button-spinner {
|
||||
width: $semantic-spacing-4; // 1rem
|
||||
height: $semantic-spacing-4; // 1rem
|
||||
border: $semantic-border-width-2 solid rgba($semantic-color-brand-primary, 0.3);
|
||||
border-top: $semantic-border-width-2 solid $semantic-color-brand-primary;
|
||||
border-radius: $semantic-border-radius-full;
|
||||
animation: spin 1s $semantic-motion-easing-linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// Ripple effect (basic implementation)
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle, rgba($semantic-color-brand-primary, 0.08) 0%, transparent 70%);
|
||||
transform: scale(0);
|
||||
opacity: $semantic-opacity-invisible;
|
||||
pointer-events: none;
|
||||
transition: transform $semantic-motion-duration-normal $semantic-motion-easing-ease, opacity $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
&:active:not(:disabled)::after {
|
||||
transform: scale(1);
|
||||
opacity: $semantic-opacity-opaque;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
export type GhostButtonSize = 'small' | 'medium' | 'large';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-ghost-button',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<button
|
||||
[class]="buttonClasses"
|
||||
[disabled]="disabled || loading"
|
||||
[type]="type"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
@if (loading) {
|
||||
<div class="ghost-button-loader">
|
||||
<div class="ghost-button-spinner"></div>
|
||||
</div>
|
||||
} @else {
|
||||
<ng-content></ng-content>
|
||||
}
|
||||
</button>
|
||||
`,
|
||||
styleUrl: './ghost-button.component.scss'
|
||||
})
|
||||
export class GhostButtonComponent {
|
||||
@Input() size: GhostButtonSize = 'medium';
|
||||
@Input() disabled: boolean = false;
|
||||
@Input() loading: boolean = false;
|
||||
@Input() type: 'button' | 'submit' | 'reset' = 'button';
|
||||
@Input() fullWidth: boolean = false;
|
||||
@Input() class: string = '';
|
||||
|
||||
@Output() clicked = new EventEmitter<Event>();
|
||||
|
||||
get buttonClasses(): string {
|
||||
return [
|
||||
'ui-ghost-button',
|
||||
`ui-ghost-button--${this.size}`,
|
||||
this.disabled ? 'ui-ghost-button--disabled' : '',
|
||||
this.loading ? 'ui-ghost-button--loading' : '',
|
||||
this.fullWidth ? 'ui-ghost-button--full-width' : '',
|
||||
this.class
|
||||
].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
handleClick(event: Event): void {
|
||||
if (!this.disabled && !this.loading) {
|
||||
this.clicked.emit(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './simple-button.component';
|
||||
export * from './button.component';
|
||||
export * from './text-button.component';
|
||||
export * from './ghost-button.component';
|
||||
export * from './fab.component';
|
||||
@@ -1 +1,5 @@
|
||||
export * from './button.component';
|
||||
export * from './text-button.component';
|
||||
export * from './ghost-button.component';
|
||||
export * from './fab.component';
|
||||
export * from './simple-button.component';
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
@use "../../../../../shared-ui/src/styles/semantic/index" as *;
|
||||
|
||||
// Tokens available globally via main application styles
|
||||
|
||||
.ui-simple-button {
|
||||
// Reset and base styles
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
// Typography
|
||||
font-family: $semantic-typography-font-family-sans;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
line-height: $semantic-typography-line-height-normal;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
|
||||
// Default sizing
|
||||
height: $semantic-spacing-10; // 2.5rem
|
||||
padding: 0 $semantic-spacing-4; // 1rem
|
||||
min-width: $semantic-spacing-20; // 5rem
|
||||
|
||||
// Interaction states
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
// Transitions
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
|
||||
|
||||
// Primary variant
|
||||
&--primary {
|
||||
background-color: $semantic-color-interactive-primary;
|
||||
color: $semantic-color-on-brand-primary;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $semantic-color-brand-primary;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
box-shadow: $semantic-shadow-button-active;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-brand-primary;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary variant
|
||||
&--secondary {
|
||||
background-color: $semantic-color-container-primary;
|
||||
color: $semantic-color-on-container-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $semantic-color-container-primary;
|
||||
border-color: $semantic-color-interactive-primary;
|
||||
transform: translateY(-1px);
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $semantic-color-container-primary;
|
||||
transform: scale(0.98);
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-brand-primary;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled state
|
||||
&:disabled {
|
||||
opacity: $semantic-opacity-disabled;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-simple-button',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<button
|
||||
[class]="buttonClasses"
|
||||
[disabled]="disabled"
|
||||
[type]="type"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
</button>
|
||||
`,
|
||||
styleUrl: './simple-button.component.scss'
|
||||
})
|
||||
export class SimpleButtonComponent {
|
||||
@Input() variant: 'primary' | 'secondary' = 'primary';
|
||||
@Input() disabled: boolean = false;
|
||||
@Input() type: 'button' | 'submit' | 'reset' = 'button';
|
||||
|
||||
@Output() clicked = new EventEmitter<Event>();
|
||||
|
||||
get buttonClasses(): string {
|
||||
return [
|
||||
'ui-simple-button',
|
||||
`ui-simple-button--${this.variant}`,
|
||||
this.disabled ? 'ui-simple-button--disabled' : ''
|
||||
].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
handleClick(event: Event): void {
|
||||
if (!this.disabled) {
|
||||
this.clicked.emit(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
@use "../../../../../shared-ui/src/styles/semantic/index" as *;
|
||||
.ui-text-button {
|
||||
// Reset and base styles
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// Typography
|
||||
font-family: $semantic-typography-font-family-sans;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
text-align: center;
|
||||
text-transform: none;
|
||||
letter-spacing: $semantic-typography-letter-spacing-wide;
|
||||
white-space: nowrap;
|
||||
color: $semantic-color-interactive-primary;
|
||||
|
||||
// Interaction states
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
box-sizing: border-box;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
|
||||
// Transitions
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease-out;
|
||||
|
||||
// Size variants
|
||||
&--small {
|
||||
height: $semantic-spacing-8; // 2rem
|
||||
padding: 0 $semantic-spacing-1; // 0.25rem
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
line-height: $semantic-typography-line-height-normal;
|
||||
min-width: $semantic-spacing-12; // 3rem
|
||||
}
|
||||
|
||||
&--medium {
|
||||
height: $semantic-spacing-10; // 2.5rem
|
||||
padding: 0 $semantic-spacing-2; // 0.5rem
|
||||
font-size: $semantic-typography-font-size-md;
|
||||
line-height: $semantic-typography-line-height-normal;
|
||||
min-width: $semantic-spacing-16; // 4rem
|
||||
}
|
||||
|
||||
&--large {
|
||||
height: $semantic-spacing-12; // 3rem
|
||||
padding: 0 $semantic-spacing-3; // 0.75rem
|
||||
font-size: $semantic-typography-font-size-lg;
|
||||
line-height: $semantic-typography-line-height-normal;
|
||||
min-width: $semantic-spacing-20; // 5rem
|
||||
}
|
||||
|
||||
// Hover states
|
||||
&:hover:not(:disabled) {
|
||||
background-color: rgba($semantic-color-brand-primary, 0.08);
|
||||
color: $semantic-color-brand-primary;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: rgba($semantic-color-brand-primary, 0.12);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
background-color: rgba($semantic-color-brand-primary, 0.08);
|
||||
box-shadow: $semantic-shadow-button-focus;
|
||||
}
|
||||
|
||||
// States
|
||||
&--disabled {
|
||||
opacity: $semantic-opacity-disabled;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&--loading {
|
||||
cursor: wait;
|
||||
|
||||
.text-button-content {
|
||||
opacity: $semantic-opacity-invisible;
|
||||
}
|
||||
}
|
||||
|
||||
// Loader styles
|
||||
.text-button-loader {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.text-button-spinner {
|
||||
width: $semantic-spacing-3-5; // 0.875rem
|
||||
height: $semantic-spacing-3-5; // 0.875rem
|
||||
border: $semantic-border-width-2 solid rgba($semantic-color-brand-primary, 0.3); // closest to 1.5px
|
||||
border-top: $semantic-border-width-2 solid $semantic-color-brand-primary;
|
||||
border-radius: $semantic-border-radius-full;
|
||||
animation: spin 1s $semantic-motion-easing-linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// Ripple effect (basic implementation)
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle, rgba($semantic-color-brand-primary, 0.12) 0%, transparent 70%);
|
||||
transform: scale(0);
|
||||
opacity: $semantic-opacity-invisible;
|
||||
pointer-events: none;
|
||||
transition: transform $semantic-motion-duration-normal $semantic-motion-easing-ease, opacity $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
&:active:not(:disabled)::after {
|
||||
transform: scale(1);
|
||||
opacity: $semantic-opacity-opaque;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
export type TextButtonSize = 'small' | 'medium' | 'large';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-text-button',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<button
|
||||
[class]="buttonClasses"
|
||||
[disabled]="disabled || loading"
|
||||
[type]="type"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
@if (loading) {
|
||||
<div class="text-button-loader">
|
||||
<div class="text-button-spinner"></div>
|
||||
</div>
|
||||
} @else {
|
||||
<ng-content></ng-content>
|
||||
}
|
||||
</button>
|
||||
`,
|
||||
styleUrl: './text-button.component.scss'
|
||||
})
|
||||
export class TextButtonComponent {
|
||||
@Input() size: TextButtonSize = 'medium';
|
||||
@Input() disabled: boolean = false;
|
||||
@Input() loading: boolean = false;
|
||||
@Input() type: 'button' | 'submit' | 'reset' = 'button';
|
||||
@Input() class: string = '';
|
||||
|
||||
@Output() clicked = new EventEmitter<Event>();
|
||||
|
||||
get buttonClasses(): string {
|
||||
return [
|
||||
'ui-text-button',
|
||||
`ui-text-button--${this.size}`,
|
||||
this.disabled ? 'ui-text-button--disabled' : '',
|
||||
this.loading ? 'ui-text-button--loading' : '',
|
||||
this.class
|
||||
].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
handleClick(event: Event): void {
|
||||
if (!this.disabled && !this.loading) {
|
||||
this.clicked.emit(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
|
||||
.skyui-avatar {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background-color: $semantic-color-surface-secondary;
|
||||
color: $semantic-color-text-secondary;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
// Size variants based on semantic sizing tokens
|
||||
&--size-xs {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
.skyui-avatar__initials {
|
||||
font-size: calc(24px * 0.4);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.skyui-avatar__icon {
|
||||
font-size: calc(24px * 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
&--size-sm {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
.skyui-avatar__initials {
|
||||
font-size: calc(32px * 0.4);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.skyui-avatar__icon {
|
||||
font-size: calc(32px * 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
&--size-md {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
.skyui-avatar__initials {
|
||||
font-size: calc(40px * 0.4);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.skyui-avatar__icon {
|
||||
font-size: calc(40px * 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
&--size-lg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
.skyui-avatar__initials {
|
||||
font-size: calc(48px * 0.4);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.skyui-avatar__icon {
|
||||
font-size: calc(48px * 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
&--size-xl {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
|
||||
.skyui-avatar__initials {
|
||||
font-size: calc(64px * 0.35);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.skyui-avatar__icon {
|
||||
font-size: calc(64px * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&--size-xxl {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
|
||||
.skyui-avatar__initials {
|
||||
font-size: calc(80px * 0.35);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.skyui-avatar__icon {
|
||||
font-size: calc(80px * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// Loading state
|
||||
&--loading {
|
||||
background-color: $semantic-color-surface-disabled;
|
||||
|
||||
.skyui-avatar__loading {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
border: 2px solid $semantic-color-border-subtle;
|
||||
border-top: 2px solid $semantic-color-brand-primary;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
// Avatar image
|
||||
&__image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Fallback container
|
||||
&__fallback {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: $semantic-color-container-primary;
|
||||
color: $semantic-color-on-container-primary;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Initials text
|
||||
&__initials {
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
// Icon
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// Status indicator
|
||||
&__status {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 25%;
|
||||
height: 25%;
|
||||
min-width: 8px;
|
||||
min-height: 8px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid $semantic-color-surface-primary;
|
||||
box-sizing: border-box;
|
||||
|
||||
&--online {
|
||||
background-color: $semantic-color-success;
|
||||
}
|
||||
|
||||
&--offline {
|
||||
background-color: $semantic-color-text-tertiary;
|
||||
}
|
||||
|
||||
&--away {
|
||||
background-color: $semantic-color-warning;
|
||||
}
|
||||
|
||||
&--busy {
|
||||
background-color: $semantic-color-danger;
|
||||
}
|
||||
}
|
||||
|
||||
// Badge indicator positioning
|
||||
&__badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
// Animation for loading spinner
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Focus and hover states for interactive avatars
|
||||
.skyui-avatar {
|
||||
&[role="button"]:focus-visible,
|
||||
&[tabindex]:focus-visible {
|
||||
outline: 2px solid $semantic-color-brand-primary;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&[role="button"]:hover,
|
||||
&[tabindex]:hover {
|
||||
transform: scale(1.05);
|
||||
transition: transform $semantic-duration-fast $semantic-easing-standard;
|
||||
}
|
||||
}
|
||||
|
||||
// High contrast mode support
|
||||
@media (prefers-contrast: high) {
|
||||
.skyui-avatar {
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
|
||||
&__status {
|
||||
border-width: $semantic-border-width-3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reduced motion support
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.skyui-avatar {
|
||||
&[role="button"]:hover,
|
||||
&[tabindex]:hover {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { faUser } from '@fortawesome/free-solid-svg-icons';
|
||||
import { BadgeComponent } from '../badge/badge.component';
|
||||
|
||||
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-avatar',
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, FontAwesomeModule, BadgeComponent],
|
||||
template: `
|
||||
<div class="skyui-avatar"
|
||||
[class]="'skyui-avatar--size-' + size"
|
||||
[class.skyui-avatar--loading]="loading"
|
||||
[attr.aria-label]="ariaLabel || (name ? name + ' avatar' : 'User avatar')">
|
||||
|
||||
@if (loading) {
|
||||
<div class="skyui-avatar__loading">
|
||||
<div class="loading-spinner" aria-hidden="true"></div>
|
||||
</div>
|
||||
} @else {
|
||||
@if (imageUrl && !imageError) {
|
||||
<img
|
||||
[src]="imageUrl"
|
||||
[alt]="altText || (name ? name + ' avatar' : 'User avatar')"
|
||||
class="skyui-avatar__image"
|
||||
(error)="onImageError()"
|
||||
(load)="onImageLoad()">
|
||||
} @else {
|
||||
<div class="skyui-avatar__fallback">
|
||||
@if (computedInitials) {
|
||||
<span class="skyui-avatar__initials" aria-hidden="true">{{ computedInitials }}</span>
|
||||
} @else {
|
||||
<fa-icon
|
||||
[icon]="faUser"
|
||||
class="skyui-avatar__icon"
|
||||
aria-hidden="true">
|
||||
</fa-icon>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (status) {
|
||||
<div class="skyui-avatar__status"
|
||||
[class]="'skyui-avatar__status--' + status"
|
||||
[attr.aria-label]="statusLabel || status + ' status'">
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (badge !== undefined && badge !== null) {
|
||||
<ui-badge
|
||||
class="skyui-avatar__badge"
|
||||
variant="danger"
|
||||
size="xs"
|
||||
[ariaLabel]="badgeLabel || 'Badge: ' + badge">
|
||||
{{ badge }}
|
||||
</ui-badge>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './avatar.component.scss'
|
||||
})
|
||||
export class AvatarComponent {
|
||||
@Input() imageUrl?: string;
|
||||
@Input() name?: string;
|
||||
@Input() initials?: string;
|
||||
@Input() size: AvatarSize = 'md';
|
||||
@Input() altText?: string;
|
||||
@Input() ariaLabel?: string;
|
||||
@Input() loading: boolean = false;
|
||||
@Input() status?: 'online' | 'offline' | 'away' | 'busy';
|
||||
@Input() statusLabel?: string;
|
||||
@Input() badge?: string | number;
|
||||
@Input() badgeLabel?: string;
|
||||
|
||||
faUser = faUser;
|
||||
imageError = false;
|
||||
|
||||
onImageError(): void {
|
||||
this.imageError = true;
|
||||
}
|
||||
|
||||
onImageLoad(): void {
|
||||
this.imageError = false;
|
||||
}
|
||||
|
||||
// Auto-generate initials from name if not provided
|
||||
get computedInitials(): string {
|
||||
if (this.initials) {
|
||||
return this.initials.slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
if (this.name) {
|
||||
const nameParts = this.name.trim().split(/\s+/);
|
||||
if (nameParts.length >= 2) {
|
||||
return (nameParts[0][0] + nameParts[nameParts.length - 1][0]).toUpperCase();
|
||||
} else if (nameParts[0]) {
|
||||
return nameParts[0].slice(0, 2).toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './avatar.component';
|
||||
@@ -0,0 +1,176 @@
|
||||
@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
|
||||
/**
|
||||
* ==========================================================================
|
||||
* BADGE COMPONENT STYLES
|
||||
* ==========================================================================
|
||||
* Material Design 3 inspired badge component with design token integration.
|
||||
* Supports multiple variants, sizes, shapes, and dot mode for notifications.
|
||||
* ==========================================================================
|
||||
*/
|
||||
|
||||
|
||||
// Tokens available globally via main application styles
|
||||
|
||||
.ui-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
transition: all $semantic-duration-short $semantic-easing-standard;
|
||||
background-color: aqua;
|
||||
|
||||
// Default size (md)
|
||||
min-width: $semantic-sizing-icon-navigation;
|
||||
height: $semantic-sizing-icon-navigation;
|
||||
padding: 0 $semantic-spacing-component-xs;
|
||||
font-size: $semantic-typography-font-size-xs;
|
||||
border-radius: $semantic-border-radius-full;
|
||||
|
||||
// Default variant styling
|
||||
background-color: $semantic-color-surface-secondary;
|
||||
color: $semantic-color-text-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
// Size variants
|
||||
&[data-size="xs"] {
|
||||
min-width: $semantic-typography-icon-small-size;
|
||||
height: $semantic-typography-icon-small-size;
|
||||
padding: 0 calc($semantic-spacing-component-xs / 2);
|
||||
font-size: $semantic-typography-font-size-xs;
|
||||
}
|
||||
|
||||
&[data-size="sm"] {
|
||||
min-width: $semantic-sizing-icon-button;
|
||||
height: $semantic-sizing-icon-button;
|
||||
padding: 0 $semantic-spacing-component-xs;
|
||||
font-size: $semantic-typography-font-size-xs;
|
||||
}
|
||||
|
||||
&[data-size="md"] {
|
||||
min-width: $semantic-sizing-icon-navigation;
|
||||
height: $semantic-sizing-icon-navigation;
|
||||
padding: 0 $semantic-spacing-component-xs;
|
||||
font-size: $semantic-typography-font-size-xs;
|
||||
}
|
||||
|
||||
&[data-size="lg"] {
|
||||
min-width: $semantic-sizing-icon-header;
|
||||
height: $semantic-sizing-icon-header;
|
||||
padding: 0 $semantic-spacing-component-sm;
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
}
|
||||
|
||||
// Shape variants
|
||||
&[data-shape="pill"] {
|
||||
border-radius: $semantic-border-radius-full;
|
||||
}
|
||||
|
||||
&[data-shape="rounded"] {
|
||||
border-radius: $semantic-border-radius-md;
|
||||
}
|
||||
|
||||
&[data-shape="square"] {
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
}
|
||||
|
||||
// Dot mode - small notification dot
|
||||
&[data-dot="true"] {
|
||||
min-width: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
border: 2px solid $semantic-color-surface-primary;
|
||||
|
||||
&[data-size="xs"] {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&[data-size="sm"] {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&[data-size="md"] {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
&[data-size="lg"] {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// Color variants
|
||||
&[data-variant="default"] {
|
||||
background-color: $semantic-color-surface-secondary;
|
||||
color: $semantic-color-text-primary;
|
||||
border-color: $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
&[data-variant="primary"] {
|
||||
background-color: $semantic-color-brand-primary;
|
||||
color: $semantic-color-text-inverse;
|
||||
border-color: $semantic-color-brand-primary;
|
||||
}
|
||||
|
||||
&[data-variant="secondary"] {
|
||||
background-color: $semantic-color-brand-secondary;
|
||||
color: $semantic-color-text-inverse;
|
||||
border-color: $semantic-color-brand-secondary;
|
||||
}
|
||||
|
||||
&[data-variant="success"] {
|
||||
background-color: $semantic-color-success;
|
||||
color: $semantic-color-text-inverse;
|
||||
border-color: $semantic-color-success;
|
||||
}
|
||||
|
||||
&[data-variant="warning"] {
|
||||
background-color: $semantic-color-warning;
|
||||
color: $semantic-color-text-inverse;
|
||||
border-color: $semantic-color-warning;
|
||||
}
|
||||
|
||||
&[data-variant="danger"] {
|
||||
background-color: $semantic-color-danger;
|
||||
color: $semantic-color-text-inverse;
|
||||
border-color: $semantic-color-danger;
|
||||
}
|
||||
|
||||
&[data-variant="info"] {
|
||||
background-color: $semantic-color-info;
|
||||
color: $semantic-color-text-inverse;
|
||||
border-color: $semantic-color-info;
|
||||
}
|
||||
|
||||
// Hover states for interactive badges
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
|
||||
// Focus states for accessibility
|
||||
&:focus {
|
||||
outline: 2px solid $semantic-color-focus-ring;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: calc($semantic-breakpoint-sm - 1px)) {
|
||||
&[data-size="lg"] {
|
||||
min-width: $semantic-sizing-icon-navigation;
|
||||
height: $semantic-sizing-icon-navigation;
|
||||
padding: 0 $semantic-spacing-component-xs;
|
||||
font-size: $semantic-typography-font-size-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
export type BadgeVariant = 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info';
|
||||
export type BadgeSize = 'xs' | 'sm' | 'md' | 'lg';
|
||||
export type BadgeShape = 'pill' | 'rounded' | 'square';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-badge',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<span
|
||||
class="ui-badge"
|
||||
[attr.data-variant]="variant"
|
||||
[attr.data-size]="size"
|
||||
[attr.data-shape]="shape"
|
||||
[attr.data-dot]="isDot"
|
||||
[attr.aria-label]="ariaLabel"
|
||||
[title]="title"
|
||||
>
|
||||
@if (!isDot) {
|
||||
<ng-content></ng-content>
|
||||
}
|
||||
</span>
|
||||
`,
|
||||
styleUrls: ['./badge.component.scss']
|
||||
})
|
||||
export class BadgeComponent {
|
||||
@Input() variant: BadgeVariant = 'default';
|
||||
@Input() size: BadgeSize = 'md';
|
||||
@Input() shape: BadgeShape = 'pill';
|
||||
@Input() isDot: boolean = false;
|
||||
@Input() ariaLabel?: string;
|
||||
@Input() title?: string;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './badge.component';
|
||||
@@ -0,0 +1,430 @@
|
||||
@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
|
||||
|
||||
// Tokens available globally via main application styles
|
||||
|
||||
.ui-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-right: $semantic-spacing-grid-gap-xs;
|
||||
margin-bottom: $semantic-spacing-grid-gap-xs;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// Base styles
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
transition: all $semantic-duration-fast $semantic-easing-standard;
|
||||
|
||||
// Size variants - following 8px grid increments
|
||||
&--sm {
|
||||
min-height: $semantic-sizing-card-height-sm;
|
||||
|
||||
.card-content-layer {
|
||||
padding: $semantic-spacing-component-padding-sm;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding-top: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
&--md {
|
||||
min-height: $semantic-sizing-card-height-md;
|
||||
|
||||
.card-content-layer {
|
||||
padding: $semantic-spacing-component-padding-lg;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding-top: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
&--lg {
|
||||
min-height: $semantic-sizing-card-height-lg;
|
||||
|
||||
.card-content-layer {
|
||||
padding: $semantic-spacing-component-padding-xl;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding-top: $semantic-spacing-component-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// Elevation variants - Material Design shadow levels
|
||||
&--elevation-none {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&--elevation-sm {
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
|
||||
&--elevation-md {
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
}
|
||||
|
||||
&--elevation-lg {
|
||||
box-shadow: $semantic-shadow-elevation-3;
|
||||
}
|
||||
|
||||
&--elevation-xl {
|
||||
box-shadow: $semantic-shadow-elevation-4;
|
||||
}
|
||||
|
||||
// Radius variants - consistent scale
|
||||
&--radius-none {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&--radius-sm {
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
}
|
||||
|
||||
&--radius-md {
|
||||
border-radius: $semantic-border-radius-md;
|
||||
}
|
||||
|
||||
&--radius-lg {
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
}
|
||||
|
||||
&--radius-full {
|
||||
border-radius: $semantic-border-radius-full;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
// Variant styles - Material Design 3 inspired colors
|
||||
&--elevated {
|
||||
background-color: $semantic-color-surface-primary;
|
||||
border: none;
|
||||
|
||||
&:hover:not(.ui-card--disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: $semantic-shadow-card-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&--filled {
|
||||
background-color: $semantic-color-container-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
|
||||
&:hover:not(.ui-card--disabled) {
|
||||
background-color: $semantic-color-surface-interactive;
|
||||
}
|
||||
}
|
||||
|
||||
&--outlined {
|
||||
background-color: transparent;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
|
||||
&:hover:not(.ui-card--disabled) {
|
||||
border-color: $semantic-color-border-focus;
|
||||
background-color: $semantic-color-surface-elevated;
|
||||
}
|
||||
}
|
||||
|
||||
// Clickable state
|
||||
&--clickable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:active:not(.ui-card--disabled) {
|
||||
transform: scale(0.98); // Subtle press effect
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-border-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled state
|
||||
&--disabled {
|
||||
opacity: 0.38; // Material Design disabled opacity
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Glass effect using CSS variables and semantic approach
|
||||
&--glass {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
backdrop-filter: blur(var(--glass-blur-md, 8px));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur-md, 8px));
|
||||
border: 1px solid var(--glass-border-color, rgba(255, 255, 255, 0.2));
|
||||
transition: all $semantic-duration-short $semantic-easing-standard;
|
||||
|
||||
.card-content-layer {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
// Glass variant styles using CSS variables - fixed syntax
|
||||
&.ui-card--glass-translucent {
|
||||
background: rgba(var(--glass-background-base, 255, 255, 255), var(--glass-opacity-translucent, 0.1));
|
||||
backdrop-filter: blur(var(--glass-blur-sm, 4px));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur-sm, 4px));
|
||||
}
|
||||
|
||||
&.ui-card--glass-light {
|
||||
background: rgba(var(--glass-background-base, 255, 255, 255), var(--glass-opacity-light, 0.3));
|
||||
backdrop-filter: blur(var(--glass-blur-md, 8px));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur-md, 8px));
|
||||
}
|
||||
|
||||
&.ui-card--glass-medium {
|
||||
background: rgba(var(--glass-background-base, 255, 255, 255), var(--glass-opacity-medium, 0.5));
|
||||
backdrop-filter: blur(var(--glass-blur-md, 8px));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur-md, 8px));
|
||||
}
|
||||
|
||||
&.ui-card--glass-heavy {
|
||||
background: rgba(var(--glass-background-base, 255, 255, 255), var(--glass-opacity-heavy, 0.7));
|
||||
backdrop-filter: blur(var(--glass-blur-lg, 16px));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur-lg, 16px));
|
||||
}
|
||||
|
||||
&.ui-card--glass-frosted {
|
||||
background: rgba(var(--glass-background-base, 255, 255, 255), var(--glass-opacity-frosted, 0.85));
|
||||
backdrop-filter: blur(var(--glass-blur-xl, 24px));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur-xl, 24px));
|
||||
}
|
||||
|
||||
// Interactive glass states
|
||||
&.ui-card--clickable:hover:not(.ui-card--disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: $semantic-shadow-elevation-3;
|
||||
}
|
||||
|
||||
&.ui-card--clickable:active:not(.ui-card--disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
// Disabled state for glass cards
|
||||
&.ui-card--disabled {
|
||||
backdrop-filter: blur(var(--glass-blur-xs, 2px));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur-xs, 2px));
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
// With background layer
|
||||
&--with-background {
|
||||
.card-background-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
box-sizing: border-box;
|
||||
transform: translateZ(0);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.card-content-layer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: rgba(255, 255, 255, 0.9); // Semi-transparent overlay
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: $semantic-color-text-inverse;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Card content structure
|
||||
.card-content-layer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
flex: 1;
|
||||
|
||||
// Typography reset
|
||||
> *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
> *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
border-top: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
// Glass overlay for enhanced glass effect
|
||||
.card-glass-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(145deg,
|
||||
rgba(255, 255, 255, 0.1) 0%,
|
||||
rgba(255, 255, 255, 0.05) 50%,
|
||||
rgba(255, 255, 255, 0.1) 100%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
// Ripple effect for clickable cards
|
||||
.card-ripple-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(120, 53, 255, 0.2); // Brand color ripple
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
opacity: 0;
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.ui-card--clickable:active & {
|
||||
&::after {
|
||||
transform: translate(-50%, -50%) scale(2);
|
||||
opacity: 1;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hover effects based on elevation
|
||||
.ui-card:hover:not(.ui-card--disabled) {
|
||||
&.ui-card--elevation-sm {
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
}
|
||||
|
||||
&.ui-card--elevation-md {
|
||||
box-shadow: $semantic-shadow-elevation-3;
|
||||
}
|
||||
|
||||
&.ui-card--elevation-lg {
|
||||
box-shadow: $semantic-shadow-elevation-4;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.ui-card {
|
||||
&--elevated {
|
||||
background-color: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-inverse;
|
||||
}
|
||||
|
||||
&--filled {
|
||||
background-color: $semantic-color-surface-secondary;
|
||||
border-color: $semantic-color-border-primary;
|
||||
color: $semantic-color-text-inverse;
|
||||
}
|
||||
|
||||
&--outlined {
|
||||
background-color: transparent;
|
||||
border-color: $semantic-color-border-primary;
|
||||
color: $semantic-color-text-inverse;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
border-color: $semantic-color-border-primary;
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: $semantic-color-text-inverse;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
border-color: $semantic-color-border-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adaptations
|
||||
@media (max-width: $semantic-sizing-breakpoint-tablet) {
|
||||
.ui-card {
|
||||
margin-right: 0;
|
||||
|
||||
&--lg {
|
||||
.card-content-layer {
|
||||
padding: $semantic-spacing-component-padding-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Accessibility
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ui-card {
|
||||
transition: none;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.card-ripple-overlay::after {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
export type CardVariant = 'elevated' | 'filled' | 'outlined';
|
||||
export type CardSize = 'sm' | 'md' | 'lg';
|
||||
export type CardElevation = 'none' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
export type CardRadius = 'none' | 'sm' | 'md' | 'lg' | 'full';
|
||||
export type GlassVariant = 'translucent' | 'light' | 'medium' | 'heavy' | 'frosted';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-card',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div
|
||||
[class]="cardClasses"
|
||||
[style.backgroundImage]="backgroundImage"
|
||||
[attr.tabindex]="clickable && !disabled ? '0' : null"
|
||||
[attr.role]="clickable ? 'button' : null"
|
||||
[attr.aria-disabled]="disabled"
|
||||
(click)="handleClick($event)"
|
||||
(keydown.enter)="handleClick($event)"
|
||||
(keydown.space)="handleClick($event)"
|
||||
>
|
||||
@if (hasBackgroundLayer) {
|
||||
<div class="card-background-layer">
|
||||
<ng-content select="[slot=background]"></ng-content>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card-content-layer">
|
||||
@if (hasHeader) {
|
||||
<div class="card-header">
|
||||
<ng-content select="[slot=header]"></ng-content>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card-body">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
|
||||
@if (hasFooter) {
|
||||
<div class="card-footer">
|
||||
<ng-content select="[slot=footer]"></ng-content>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (glass) {
|
||||
<div class="card-glass-overlay"></div>
|
||||
}
|
||||
|
||||
@if (clickable && !disabled) {
|
||||
<div class="card-ripple-overlay"></div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './card.component.scss'
|
||||
})
|
||||
export class CardComponent {
|
||||
@Input() variant: CardVariant = 'elevated';
|
||||
@Input() size: CardSize = 'md';
|
||||
@Input() elevation: CardElevation = 'sm';
|
||||
@Input() radius: CardRadius = 'md';
|
||||
@Input() disabled: boolean = false;
|
||||
@Input() clickable: boolean = false;
|
||||
@Input() glass: boolean = false;
|
||||
@Input() glassVariant: GlassVariant = 'medium';
|
||||
@Input() backgroundImage?: string;
|
||||
@Input() hasBackgroundLayer: boolean = false;
|
||||
@Input() hasHeader: boolean = false;
|
||||
@Input() hasFooter: boolean = false;
|
||||
@Input() class: string = '';
|
||||
|
||||
@Output() cardClick = new EventEmitter<Event>();
|
||||
|
||||
get cardClasses(): string {
|
||||
return [
|
||||
'ui-card',
|
||||
`ui-card--${this.variant}`,
|
||||
`ui-card--${this.size}`,
|
||||
`ui-card--elevation-${this.elevation}`,
|
||||
`ui-card--radius-${this.radius}`,
|
||||
this.disabled ? 'ui-card--disabled' : '',
|
||||
this.clickable ? 'ui-card--clickable' : '',
|
||||
this.glass ? 'ui-card--glass' : '',
|
||||
this.glass ? `ui-card--glass-${this.glassVariant}` : '',
|
||||
this.hasBackgroundLayer ? 'ui-card--with-background' : '',
|
||||
this.class
|
||||
].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
handleClick(event: Event): void {
|
||||
if (this.clickable && !this.disabled) {
|
||||
this.cardClick.emit(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './card.component';
|
||||
@@ -0,0 +1,181 @@
|
||||
@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
|
||||
|
||||
@mixin font-properties($font-map) {
|
||||
@if type-of($font-map) == 'map' {
|
||||
font-family: map-get($font-map, 'font-family');
|
||||
font-size: map-get($font-map, 'font-size');
|
||||
line-height: map-get($font-map, 'line-height');
|
||||
font-weight: map-get($font-map, 'font-weight');
|
||||
letter-spacing: map-get($font-map, 'letter-spacing');
|
||||
} @else {
|
||||
font: $font-map;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background: $semantic-color-surface-container;
|
||||
|
||||
.carousel-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px; // Default height
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.carousel-track {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform $semantic-duration-medium $semantic-easing-standard;
|
||||
}
|
||||
|
||||
.carousel-slide {
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.multiple-items {
|
||||
flex: 0 0 calc(100% / var(--items-per-view, 1));
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.slide-content {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
text-align: center;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
h3 {
|
||||
@include font-properties($semantic-typography-heading-h3);
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
p {
|
||||
@include font-properties($semantic-typography-body-medium);
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-controls {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 2;
|
||||
|
||||
button {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: $semantic-shadow-elevation-3;
|
||||
transition: all $semantic-motion-duration-fast $semantic-easing-standard;
|
||||
|
||||
&:hover {
|
||||
background: white;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $semantic-color-primary;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&.prev {
|
||||
left: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&.next {
|
||||
right: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-indicators {
|
||||
position: absolute;
|
||||
bottom: $semantic-spacing-component-md;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
z-index: 2;
|
||||
|
||||
button {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
cursor: pointer;
|
||||
transition: all $semantic-duration-medium $semantic-easing-standard;
|
||||
|
||||
&.active {
|
||||
background: $semantic-color-primary;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $semantic-color-primary;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.carousel-container {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.carousel-controls button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-slide .slide-content {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-play animation
|
||||
.carousel.auto-play .carousel-track {
|
||||
transition: transform $semantic-duration-medium $semantic-easing-standard;
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
export type CarouselSize = 'sm' | 'md' | 'lg';
|
||||
export type CarouselVariant = 'card' | 'image' | 'content';
|
||||
export type CarouselIndicatorStyle = 'dots' | 'bars' | 'thumbnails' | 'none';
|
||||
export type CarouselTransition = 'slide' | 'fade' | 'scale';
|
||||
|
||||
export interface CarouselItem {
|
||||
id: string | number;
|
||||
imageUrl?: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
content?: string;
|
||||
thumbnail?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-carousel',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
class="ui-carousel"
|
||||
[class.ui-carousel--{{size}}]="size"
|
||||
[class.ui-carousel--{{variant}}]="variant"
|
||||
[class.ui-carousel--{{transition}}]="transition"
|
||||
[class.ui-carousel--disabled]="disabled"
|
||||
[class.ui-carousel--autoplay]="autoplay"
|
||||
[attr.aria-roledescription]="'carousel'"
|
||||
[attr.aria-label]="ariaLabel"
|
||||
role="region">
|
||||
|
||||
<!-- Navigation buttons -->
|
||||
@if (showNavigation && items.length > 1) {
|
||||
<button
|
||||
class="ui-carousel__nav ui-carousel__nav--prev"
|
||||
[disabled]="disabled || (currentIndex() === 0 && !loop)"
|
||||
(click)="goToPrevious()"
|
||||
[attr.aria-label]="'Previous slide'"
|
||||
type="button">
|
||||
<svg class="ui-carousel__nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="15,18 9,12 15,6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="ui-carousel__nav ui-carousel__nav--next"
|
||||
[disabled]="disabled || (currentIndex() === items.length - 1 && !loop)"
|
||||
(click)="goToNext()"
|
||||
[attr.aria-label]="'Next slide'"
|
||||
type="button">
|
||||
<svg class="ui-carousel__nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="9,18 15,12 9,6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- Carousel container -->
|
||||
<div class="ui-carousel__container">
|
||||
<div
|
||||
class="ui-carousel__track"
|
||||
[style.transform]="'translateX(-' + (currentIndex() * 100) + '%)'">
|
||||
@for (item of items; track item.id; let i = $index) {
|
||||
<div
|
||||
class="ui-carousel__slide"
|
||||
[class.ui-carousel__slide--active]="i === currentIndex()"
|
||||
[attr.aria-hidden]="i === currentIndex() ? 'false' : 'true'"
|
||||
[attr.aria-label]="'Slide ' + (i + 1) + ' of ' + items.length">
|
||||
|
||||
<!-- Content based on variant -->
|
||||
@switch (variant) {
|
||||
@case ('image') {
|
||||
@if (item.imageUrl) {
|
||||
<div class="ui-carousel__image-container">
|
||||
<img
|
||||
[src]="item.imageUrl"
|
||||
[alt]="item.title || 'Carousel image'"
|
||||
class="ui-carousel__image"
|
||||
loading="lazy">
|
||||
@if (item.title || item.subtitle) {
|
||||
<div class="ui-carousel__image-overlay">
|
||||
@if (item.title) {
|
||||
<h3 class="ui-carousel__title">{{ item.title }}</h3>
|
||||
}
|
||||
@if (item.subtitle) {
|
||||
<p class="ui-carousel__subtitle">{{ item.subtitle }}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@case ('card') {
|
||||
<div class="ui-carousel__card">
|
||||
@if (item.imageUrl) {
|
||||
<img
|
||||
[src]="item.imageUrl"
|
||||
[alt]="item.title || 'Card image'"
|
||||
class="ui-carousel__card-image"
|
||||
loading="lazy">
|
||||
}
|
||||
<div class="ui-carousel__card-content">
|
||||
@if (item.title) {
|
||||
<h3 class="ui-carousel__title">{{ item.title }}</h3>
|
||||
}
|
||||
@if (item.subtitle) {
|
||||
<p class="ui-carousel__subtitle">{{ item.subtitle }}</p>
|
||||
}
|
||||
@if (item.content) {
|
||||
<p class="ui-carousel__content">{{ item.content }}</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@case ('content') {
|
||||
<div class="ui-carousel__content-slide">
|
||||
@if (item.title) {
|
||||
<h3 class="ui-carousel__title">{{ item.title }}</h3>
|
||||
}
|
||||
@if (item.subtitle) {
|
||||
<p class="ui-carousel__subtitle">{{ item.subtitle }}</p>
|
||||
}
|
||||
@if (item.content) {
|
||||
<div class="ui-carousel__content" [innerHTML]="item.content"></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Indicators -->
|
||||
@if (showIndicators && indicatorStyle !== 'none' && items.length > 1) {
|
||||
<div class="ui-carousel__indicators" [class.ui-carousel__indicators--{{indicatorStyle}}]="indicatorStyle">
|
||||
@for (item of items; track item.id; let i = $index) {
|
||||
<button
|
||||
class="ui-carousel__indicator"
|
||||
[class.ui-carousel__indicator--active]="i === currentIndex()"
|
||||
[disabled]="disabled"
|
||||
(click)="goToSlide(i)"
|
||||
[attr.aria-label]="'Go to slide ' + (i + 1)"
|
||||
type="button">
|
||||
@switch (indicatorStyle) {
|
||||
@case ('thumbnails') {
|
||||
@if (item.thumbnail || item.imageUrl) {
|
||||
<img
|
||||
[src]="item.thumbnail || item.imageUrl"
|
||||
[alt]="item.title || 'Slide thumbnail'"
|
||||
class="ui-carousel__indicator-thumbnail">
|
||||
}
|
||||
}
|
||||
@case ('dots') {
|
||||
<span class="ui-carousel__indicator-dot"></span>
|
||||
}
|
||||
@case ('bars') {
|
||||
<span class="ui-carousel__indicator-bar"></span>
|
||||
}
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './carousel.component.scss'
|
||||
})
|
||||
export class CarouselComponent {
|
||||
@Input() items: CarouselItem[] = [];
|
||||
@Input() size: CarouselSize = 'md';
|
||||
@Input() variant: CarouselVariant = 'image';
|
||||
@Input() transition: CarouselTransition = 'slide';
|
||||
@Input() indicatorStyle: CarouselIndicatorStyle = 'dots';
|
||||
@Input() disabled = false;
|
||||
@Input() autoplay = false;
|
||||
@Input() autoplayInterval = 3000;
|
||||
@Input() loop = true;
|
||||
@Input() showNavigation = true;
|
||||
@Input() showIndicators = true;
|
||||
@Input() ariaLabel = 'Image carousel';
|
||||
@Input() initialIndex = 0;
|
||||
|
||||
@Output() slideChange = new EventEmitter<number>();
|
||||
@Output() itemClick = new EventEmitter<{item: CarouselItem, index: number}>();
|
||||
|
||||
private _currentIndex = signal(this.initialIndex);
|
||||
private _autoplayTimer?: number;
|
||||
|
||||
currentIndex = this._currentIndex.asReadonly();
|
||||
|
||||
ngOnInit(): void {
|
||||
this._currentIndex.set(this.initialIndex);
|
||||
if (this.autoplay && !this.disabled) {
|
||||
this.startAutoplay();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stopAutoplay();
|
||||
}
|
||||
|
||||
goToSlide(index: number): void {
|
||||
if (this.disabled || index < 0 || index >= this.items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._currentIndex.set(index);
|
||||
this.slideChange.emit(index);
|
||||
this.resetAutoplay();
|
||||
}
|
||||
|
||||
goToNext(): void {
|
||||
const nextIndex = this.loop && this.currentIndex() === this.items.length - 1
|
||||
? 0
|
||||
: Math.min(this.currentIndex() + 1, this.items.length - 1);
|
||||
this.goToSlide(nextIndex);
|
||||
}
|
||||
|
||||
goToPrevious(): void {
|
||||
const prevIndex = this.loop && this.currentIndex() === 0
|
||||
? this.items.length - 1
|
||||
: Math.max(this.currentIndex() - 1, 0);
|
||||
this.goToSlide(prevIndex);
|
||||
}
|
||||
|
||||
private startAutoplay(): void {
|
||||
this.stopAutoplay();
|
||||
this._autoplayTimer = window.setInterval(() => {
|
||||
this.goToNext();
|
||||
}, this.autoplayInterval);
|
||||
}
|
||||
|
||||
private stopAutoplay(): void {
|
||||
if (this._autoplayTimer) {
|
||||
clearInterval(this._autoplayTimer);
|
||||
this._autoplayTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private resetAutoplay(): void {
|
||||
if (this.autoplay && !this.disabled) {
|
||||
this.stopAutoplay();
|
||||
this.startAutoplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user