Compare commits
10 Commits
c803831f60
...
5346d6d0c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5346d6d0c9 | ||
|
|
876eb301a0 | ||
|
|
2f56ee01b3 | ||
|
|
2bbbf1b9f1 | ||
|
|
e9f975eb02 | ||
|
|
1aaef36763 | ||
|
|
bf67b7f955 | ||
|
|
6b8352a8a0 | ||
|
|
6f0ab0cf5f | ||
|
|
5983722793 |
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
|
||||
82
CLAUDE.md
82
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
|
||||
- **ui-design-system**: UI design system with comprehensive SCSS styling, design tokens, and reusable components
|
||||
- **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
|
||||
|
||||
@@ -18,7 +19,7 @@ SSuite is an Angular workspace containing three libraries designed as a modular
|
||||
ng build
|
||||
|
||||
# Build specific library
|
||||
ng build shared-ui
|
||||
ng build ui-design-system
|
||||
ng build shared-utils
|
||||
ng build ui-essentials
|
||||
|
||||
@@ -32,37 +33,60 @@ ng build --watch --configuration development
|
||||
ng test
|
||||
|
||||
# Run tests for specific library
|
||||
ng test shared-ui
|
||||
ng test ui-design-system
|
||||
ng test shared-utils
|
||||
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/
|
||||
├── ui-design-system/ - 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
|
||||
Libraries are mapped in `tsconfig.json` paths:
|
||||
```json
|
||||
"paths": {
|
||||
"shared-ui": ["./dist/shared-ui"],
|
||||
"ui-design-system": ["./dist/ui-design-system"],
|
||||
"shared-utils": ["./dist/shared-utils"],
|
||||
"ui-essentials": ["./dist/ui-essentials"]
|
||||
}
|
||||
```
|
||||
|
||||
### Design System (shared-ui)
|
||||
### 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 (ui-design-system)
|
||||
Contains a comprehensive SCSS architecture:
|
||||
- **Base tokens**: Colors, typography, spacing, motion, shadows
|
||||
- **Semantic tokens**: High-level design tokens built on base tokens
|
||||
@@ -75,30 +99,42 @@ Contains a comprehensive SCSS architecture:
|
||||
#### Style Import Options:
|
||||
```scss
|
||||
// Complete design system (includes semantic + base tokens + fonts)
|
||||
@use 'shared-ui/src/styles' as ui;
|
||||
@use 'ui-design-system/src/styles' as ui;
|
||||
|
||||
// Tokens only (semantic + base tokens)
|
||||
@use 'shared-ui/src/styles/tokens' as tokens;
|
||||
@use 'ui-design-system/src/styles/tokens' as tokens;
|
||||
|
||||
// Semantic tokens only (includes base tokens)
|
||||
@use 'shared-ui/src/styles/semantic' as semantic;
|
||||
@use 'ui-design-system/src/styles/semantic' as semantic;
|
||||
|
||||
// Base tokens only
|
||||
@use 'shared-ui/src/styles/base' as base;
|
||||
@use 'ui-design-system/src/styles/base' as base;
|
||||
|
||||
// Components and utilities (import separately to avoid circular dependencies)
|
||||
@use 'shared-ui/src/styles/commons' as commons;
|
||||
@use 'ui-design-system/src/styles/commons' as commons;
|
||||
```
|
||||
|
||||
Entry points:
|
||||
- Main: `projects/shared-ui/src/styles/index.scss`
|
||||
- Tokens: `projects/shared-ui/src/styles/tokens.scss`
|
||||
- Semantic: `projects/shared-ui/src/styles/semantic/index.scss`
|
||||
- Main: `projects/ui-design-system/src/styles/index.scss`
|
||||
- Tokens: `projects/ui-design-system/src/styles/tokens.scss`
|
||||
- Semantic: `projects/ui-design-system/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`
|
||||
884
COMPONENT_CREATION_TEMPLATE.md
Normal file
884
COMPONENT_CREATION_TEMPLATE.md
Normal file
@@ -0,0 +1,884 @@
|
||||
# Component Creation Template
|
||||
|
||||
## ⚠️ CRITICAL RESTRICTIONS ⚠️
|
||||
|
||||
### DO NOT MODIFY EXISTING COMPONENTS
|
||||
- **NEVER** alter Angular or SCSS code of already built components
|
||||
- **NEVER** change existing component files unless explicitly instructed
|
||||
- **ONLY** create NEW components as specified in the task
|
||||
- **ONLY** update integration files (routes, menu, public API) to add references to new components
|
||||
- If asked to modify an existing component, REQUEST CLARIFICATION before proceeding
|
||||
- Existing components are PRODUCTION CODE and must remain untouched
|
||||
|
||||
### Protected Files and Directories
|
||||
The following should NEVER be modified without explicit permission:
|
||||
- Any existing component files in `@projects/ui-essentials/src/lib/components/`
|
||||
- Any existing demo files in `@projects/demo-ui-essentials/src/app/demos/`
|
||||
- Only ADD new entries to:
|
||||
- `demos.routes.ts` (add new cases, don't modify existing)
|
||||
- `dashboard.component.ts` (add new menu items, don't modify existing)
|
||||
- `public-api.ts` (add new exports, don't modify existing)
|
||||
|
||||
---
|
||||
|
||||
## Component Creation Task Template
|
||||
|
||||
### 1. Component Selection
|
||||
|
||||
- Alert/Notification
|
||||
|
||||
|
||||
### 2. Pre-Development Checklist
|
||||
- [ ] Check if similar component exists in codebase
|
||||
- [ ] Review existing components in same category for patterns
|
||||
- [ ] Identify required design tokens from semantic layer
|
||||
- [ ] Plan component API (inputs, outputs, content projection)
|
||||
- [ ] **CONFIRM: This is a NEW component, not a modification of an existing one**
|
||||
|
||||
### 3. Implementation Steps
|
||||
|
||||
#### Step 1: Create Component Structure
|
||||
**Location:** `@projects/ui-essentials/src/lib/components/[category]/[component-name]/`
|
||||
|
||||
Create these NEW files (do not overwrite existing):
|
||||
- `[component-name].component.ts`
|
||||
- `[component-name].component.scss`
|
||||
- `index.ts` (barrel export)
|
||||
|
||||
#### Step 2: SCSS Implementation
|
||||
Follow the **Design Token Guidelines** below to create component styles.
|
||||
|
||||
#### Step 3: Angular Component Implementation
|
||||
Follow the **Angular Component Guidelines** below.
|
||||
|
||||
#### Step 4: Create Demo Component
|
||||
**Location:** `@projects/demo-ui-essentials/src/app/demos/[component-name]-demo/`
|
||||
- Create NEW demo component showing all variants, sizes, and states
|
||||
- Include interactive examples and code snippets
|
||||
- Follow existing demo patterns
|
||||
|
||||
#### Step 5: Integration Updates (ADDITIVE ONLY)
|
||||
|
||||
##### A. Update Routes (ADD ONLY - DO NOT MODIFY EXISTING)
|
||||
**File:** `@projects/demo-ui-essentials/src/app/demos/demos.routes.ts`
|
||||
```typescript
|
||||
// Add import - DO NOT MODIFY EXISTING IMPORTS
|
||||
import { [ComponentName]DemoComponent } from './[component-name]-demo/[component-name]-demo.component';
|
||||
|
||||
// Add to template switch case - DO NOT MODIFY EXISTING CASES
|
||||
@case ("[component-name]") {
|
||||
<ui-[component-name]-demo></ui-[component-name]-demo>
|
||||
}
|
||||
|
||||
// Add to imports array - DO NOT REMOVE OR MODIFY EXISTING ENTRIES
|
||||
imports: [...existing, [ComponentName]DemoComponent]
|
||||
```
|
||||
|
||||
##### B. Update Dashboard Menu (ADD ONLY - DO NOT MODIFY EXISTING)
|
||||
**File:** `@projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts`
|
||||
```typescript
|
||||
// Import appropriate FontAwesome icon - DO NOT MODIFY EXISTING IMPORTS
|
||||
import { faIconName } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
// Add in ngOnInit() - DO NOT MODIFY EXISTING MENU ITEMS
|
||||
this.addMenuItem("[component-id]", "[Component Label]", this.faIconName);
|
||||
```
|
||||
|
||||
##### C. Update Public API (ADD ONLY - DO NOT MODIFY EXISTING)
|
||||
**File:** `@projects/ui-essentials/src/public-api.ts`
|
||||
```typescript
|
||||
// Add export in appropriate section - DO NOT MODIFY EXISTING EXPORTS
|
||||
export * from './lib/components/[category]/[component-name]';
|
||||
```
|
||||
|
||||
### ⚠️ INTEGRATION WARNING ⚠️
|
||||
When updating integration files:
|
||||
- **ONLY ADD** new entries
|
||||
- **NEVER DELETE** existing entries
|
||||
- **NEVER MODIFY** existing entries
|
||||
- **PRESERVE** all existing formatting and structure
|
||||
- **TEST** that existing components still work after changes
|
||||
|
||||
---
|
||||
|
||||
## Design Token Guidelines ⚠️ CRITICAL: ONLY USE EXISTING TOKENS ⚠️
|
||||
|
||||
### MANDATORY: Import and Use Semantic Tokens
|
||||
```scss
|
||||
@use '@projects/shared-ui/src/styles/semantic' as *;
|
||||
```
|
||||
|
||||
### 🚨 TOKEN VALIDATION RULES 🚨
|
||||
- ❌ **NEVER** invent or guess token names
|
||||
- ❌ **NEVER** use tokens not listed below
|
||||
- ❌ **NEVER** hardcode values (px, rem, #hex colors, etc.)
|
||||
- ✅ **ALWAYS** use semantic tokens for ALL values
|
||||
- ✅ **ALWAYS** check this reference before using any token
|
||||
- ✅ **ASK FOR GUIDANCE** if needed token doesn't exist
|
||||
|
||||
### Available Token Categories (VERIFIED ACTUAL TOKENS):
|
||||
|
||||
## 1. **COLORS** (Single Values - Use Directly)
|
||||
```scss
|
||||
// Text Colors
|
||||
$semantic-color-text-primary // Primary text
|
||||
$semantic-color-text-secondary // Secondary text
|
||||
$semantic-color-text-tertiary // Tertiary text
|
||||
$semantic-color-text-disabled // Disabled text
|
||||
$semantic-color-text-inverse // Inverse text
|
||||
|
||||
// Surface Colors
|
||||
$semantic-color-surface-primary // Primary surface
|
||||
$semantic-color-surface-secondary // Secondary surface
|
||||
$semantic-color-surface-elevated // Elevated surface
|
||||
$semantic-color-surface // Base surface
|
||||
$semantic-color-surface-container // Container surface
|
||||
|
||||
// Border Colors
|
||||
$semantic-color-border-primary // Primary border
|
||||
$semantic-color-border-secondary // Secondary border
|
||||
$semantic-color-border-subtle // Subtle border
|
||||
$semantic-color-border-focus // Focus border
|
||||
$semantic-color-border-error // Error border
|
||||
|
||||
// Brand Colors
|
||||
$semantic-color-primary // Primary brand
|
||||
$semantic-color-secondary // Secondary brand
|
||||
$semantic-color-on-primary // Text on primary
|
||||
$semantic-color-on-secondary // Text on secondary
|
||||
|
||||
// Feedback Colors
|
||||
$semantic-color-success // Success state
|
||||
$semantic-color-warning // Warning state
|
||||
$semantic-color-danger // Danger/error state
|
||||
$semantic-color-info // Info state
|
||||
$semantic-color-on-success // Text on success
|
||||
$semantic-color-on-warning // Text on warning
|
||||
$semantic-color-on-danger // Text on danger
|
||||
$semantic-color-on-info // Text on info
|
||||
|
||||
// Focus and Interactive
|
||||
$semantic-color-focus // Focus indicator
|
||||
$semantic-color-interactive-primary // Interactive primary
|
||||
$semantic-color-backdrop // Backdrop/overlay
|
||||
```
|
||||
|
||||
## 2. **TYPOGRAPHY** (Maps and Single Values)
|
||||
|
||||
### Typography Maps (Use with map-get())
|
||||
```scss
|
||||
// Headings (Maps - Extract individual properties)
|
||||
$semantic-typography-heading-h1 // h1 heading map
|
||||
$semantic-typography-heading-h2 // h2 heading map
|
||||
$semantic-typography-heading-h3 // h3 heading map
|
||||
$semantic-typography-heading-h4 // h4 heading map
|
||||
$semantic-typography-heading-h5 // h5 heading map
|
||||
|
||||
// Body Text (Maps)
|
||||
$semantic-typography-body-large // Large body text map
|
||||
$semantic-typography-body-medium // Medium body text map
|
||||
$semantic-typography-body-small // Small body text map
|
||||
|
||||
// UI Components (Maps)
|
||||
$semantic-typography-button-large // Large button text map
|
||||
$semantic-typography-button-medium // Medium button text map
|
||||
$semantic-typography-button-small // Small button text map
|
||||
$semantic-typography-label // Form label map
|
||||
$semantic-typography-input // Input text map
|
||||
$semantic-typography-caption // Caption text map
|
||||
|
||||
// Usage Example for Maps:
|
||||
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);
|
||||
```
|
||||
|
||||
### Typography Single Values (Use Directly)
|
||||
```scss
|
||||
// Font Weights
|
||||
$semantic-typography-font-weight-normal
|
||||
$semantic-typography-font-weight-medium
|
||||
$semantic-typography-font-weight-semibold
|
||||
$semantic-typography-font-weight-bold
|
||||
|
||||
// Font Sizes
|
||||
$semantic-typography-font-size-xs
|
||||
$semantic-typography-font-size-sm
|
||||
$semantic-typography-font-size-md
|
||||
$semantic-typography-font-size-lg
|
||||
$semantic-typography-font-size-xl
|
||||
$semantic-typography-font-size-2xl
|
||||
$semantic-typography-font-size-3xl
|
||||
|
||||
// Line Heights
|
||||
$semantic-typography-line-height-tight
|
||||
$semantic-typography-line-height-normal
|
||||
$semantic-typography-line-height-relaxed
|
||||
|
||||
// Font Families
|
||||
$semantic-typography-font-family-sans
|
||||
$semantic-typography-font-family-serif
|
||||
$semantic-typography-font-family-mono
|
||||
```
|
||||
|
||||
## 3. **SPACING** (Single Values - Use Directly)
|
||||
```scss
|
||||
// Component Spacing
|
||||
$semantic-spacing-component-xs // Extra small component spacing
|
||||
$semantic-spacing-component-sm // Small component spacing
|
||||
$semantic-spacing-component-md // Medium component spacing
|
||||
$semantic-spacing-component-lg // Large component spacing
|
||||
$semantic-spacing-component-xl // Extra large component spacing
|
||||
|
||||
// Layout Spacing
|
||||
$semantic-spacing-layout-section-xs // Extra small section spacing
|
||||
$semantic-spacing-layout-section-sm // Small section spacing
|
||||
$semantic-spacing-layout-section-md // Medium section spacing
|
||||
$semantic-spacing-layout-section-lg // Large section spacing
|
||||
$semantic-spacing-layout-section-xl // Extra large section spacing
|
||||
|
||||
// Content Spacing
|
||||
$semantic-spacing-content-paragraph // Paragraph spacing
|
||||
$semantic-spacing-content-heading // Heading spacing
|
||||
$semantic-spacing-content-list-item // List item spacing
|
||||
$semantic-spacing-content-line-tight // Tight line spacing
|
||||
|
||||
// Interactive Spacing
|
||||
$semantic-spacing-interactive-button-padding-x // Button horizontal padding
|
||||
$semantic-spacing-interactive-button-padding-y // Button vertical padding
|
||||
$semantic-spacing-interactive-input-padding-x // Input horizontal padding
|
||||
$semantic-spacing-interactive-input-padding-y // Input vertical padding
|
||||
|
||||
// Grid and Stack Spacing
|
||||
$semantic-spacing-grid-gap-sm // Small grid gap
|
||||
$semantic-spacing-grid-gap-md // Medium grid gap
|
||||
$semantic-spacing-grid-gap-lg // Large grid gap
|
||||
$semantic-spacing-stack-sm // Small stack spacing
|
||||
$semantic-spacing-stack-md // Medium stack spacing
|
||||
$semantic-spacing-stack-lg // Large stack spacing
|
||||
|
||||
// Form Spacing
|
||||
$semantic-spacing-form-field-gap // Form field gap
|
||||
$semantic-spacing-form-group-gap // Form group gap
|
||||
```
|
||||
|
||||
## 4. **BORDERS** (Single Values - Use Directly)
|
||||
```scss
|
||||
// Border Widths
|
||||
$semantic-border-width-1 // 1px border
|
||||
$semantic-border-width-2 // 2px border
|
||||
$semantic-border-card-width // Standard card border width
|
||||
|
||||
// Border Radius
|
||||
$semantic-border-radius-sm // Small radius
|
||||
$semantic-border-radius-md // Medium radius
|
||||
$semantic-border-radius-lg // Large radius
|
||||
$semantic-border-radius-xl // Extra large radius
|
||||
$semantic-border-radius-full // Full/circle radius
|
||||
|
||||
// Component Borders
|
||||
$semantic-border-button-radius // Button border radius
|
||||
$semantic-border-input-radius // Input border radius
|
||||
$semantic-border-card-radius // Card border radius
|
||||
```
|
||||
|
||||
## 5. **SHADOWS** (Single Values - Use Directly)
|
||||
```scss
|
||||
// Elevation Shadows
|
||||
$semantic-shadow-elevation-0 // No shadow
|
||||
$semantic-shadow-elevation-1 // Level 1 elevation
|
||||
$semantic-shadow-elevation-2 // Level 2 elevation
|
||||
$semantic-shadow-elevation-3 // Level 3 elevation
|
||||
$semantic-shadow-elevation-4 // Level 4 elevation
|
||||
$semantic-shadow-elevation-5 // Level 5 elevation
|
||||
|
||||
// Component Shadows
|
||||
$semantic-shadow-card-rest // Card default shadow
|
||||
$semantic-shadow-card-hover // Card hover shadow
|
||||
$semantic-shadow-button-rest // Button default shadow
|
||||
$semantic-shadow-button-hover // Button hover shadow
|
||||
$semantic-shadow-dropdown // Dropdown shadow
|
||||
$semantic-shadow-modal // Modal shadow
|
||||
```
|
||||
|
||||
## 6. **MOTION** (Single Values and Maps)
|
||||
|
||||
### Motion Single Values (Use Directly)
|
||||
```scss
|
||||
// Duration
|
||||
$semantic-motion-duration-fast // Fast duration
|
||||
$semantic-motion-duration-normal // Normal duration
|
||||
$semantic-motion-duration-slow // Slow duration
|
||||
|
||||
// Easing
|
||||
$semantic-motion-easing-ease // Standard easing
|
||||
$semantic-motion-easing-ease-in-out // Ease in-out
|
||||
$semantic-motion-easing-spring // Spring easing
|
||||
```
|
||||
|
||||
## 7. **SIZING** (Single Values - Use Directly)
|
||||
```scss
|
||||
// Button Heights
|
||||
$semantic-sizing-button-height-sm // Small button height
|
||||
$semantic-sizing-button-height-md // Medium button height
|
||||
$semantic-sizing-button-height-lg // Large button height
|
||||
|
||||
// Input Heights
|
||||
$semantic-sizing-input-height-sm // Small input height
|
||||
$semantic-sizing-input-height-md // Medium input height
|
||||
$semantic-sizing-input-height-lg // Large input height
|
||||
|
||||
// Icon Sizes
|
||||
$semantic-sizing-icon-inline // Inline icon size
|
||||
$semantic-sizing-icon-button // Button icon size
|
||||
$semantic-sizing-icon-navigation // Navigation icon size
|
||||
|
||||
// Touch Targets
|
||||
$semantic-sizing-touch-minimum // Minimum touch target
|
||||
$semantic-sizing-touch-target // Standard touch target
|
||||
```
|
||||
|
||||
## 8. **Z-INDEX** (Single Values - Use Directly)
|
||||
```scss
|
||||
$semantic-z-index-dropdown // Dropdown layer
|
||||
$semantic-z-index-modal // Modal layer
|
||||
$semantic-z-index-tooltip // Tooltip layer
|
||||
$semantic-z-index-overlay // Overlay layer
|
||||
```
|
||||
|
||||
## 9. **OPACITY** (Single Values - Use Directly)
|
||||
```scss
|
||||
$semantic-opacity-disabled // Disabled opacity
|
||||
$semantic-opacity-hover // Hover opacity
|
||||
$semantic-opacity-backdrop // Backdrop opacity
|
||||
$semantic-opacity-subtle // Subtle opacity
|
||||
```
|
||||
|
||||
### Strict Rules:
|
||||
- ❌ **NEVER** hardcode values (px, rem, #hex colors, etc.)
|
||||
- ❌ **NEVER** use base tokens directly (from `/base/` directory)
|
||||
- ❌ **NEVER** use magic numbers
|
||||
- ✅ **ALWAYS** use semantic tokens for ALL values
|
||||
- ✅ **ALWAYS** follow BEM naming convention for classes
|
||||
- ✅ **ALWAYS** use SCSS nesting appropriately (max 3 levels)
|
||||
- ✅ **ALWAYS** include responsive breakpoints using `$semantic-breakpoint-*` tokens
|
||||
- ✅ **ALWAYS** maintain consistency with existing components
|
||||
|
||||
### Component SCSS Template (CORRECTED)
|
||||
```scss
|
||||
@use '@projects/shared-ui/src/styles/semantic' as *;
|
||||
|
||||
.ui-[component-name] {
|
||||
// Core Structure
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
// Layout & Spacing - ONLY USE ACTUAL TOKENS
|
||||
padding: $semantic-spacing-component-md;
|
||||
margin: $semantic-spacing-layout-section-sm;
|
||||
|
||||
// Visual Design - ONLY USE ACTUAL TOKENS
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-card-width solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
|
||||
// Typography - USE MAP-GET FOR MAPS, SINGLE VALUES DIRECTLY
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
// Transitions - ONLY USE ACTUAL TOKENS
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
// Sizing Variants - ONLY USE ACTUAL TOKENS
|
||||
&--sm {
|
||||
min-height: $semantic-sizing-button-height-sm;
|
||||
padding: $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);
|
||||
}
|
||||
|
||||
&--md {
|
||||
min-height: $semantic-sizing-button-height-md;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
// Typography already set above for medium
|
||||
}
|
||||
|
||||
&--lg {
|
||||
min-height: $semantic-sizing-button-height-lg;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
}
|
||||
|
||||
// Color Variants - ONLY USE ACTUAL TOKENS
|
||||
&--primary {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
border-color: $semantic-color-secondary;
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: $semantic-color-success;
|
||||
color: $semantic-color-on-success;
|
||||
border-color: $semantic-color-success;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: $semantic-color-danger;
|
||||
color: $semantic-color-on-danger;
|
||||
border-color: $semantic-color-danger;
|
||||
}
|
||||
|
||||
// State Variants - ONLY USE ACTUAL TOKENS
|
||||
&--disabled {
|
||||
opacity: $semantic-opacity-disabled;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// BEM Element - ONLY USE ACTUAL TOKENS
|
||||
&__element {
|
||||
padding: $semantic-spacing-content-line-tight;
|
||||
color: $semantic-color-text-secondary;
|
||||
|
||||
// Element modifier
|
||||
&--active {
|
||||
color: $semantic-color-text-primary;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
// Interactive States - ONLY USE ACTUAL TOKENS
|
||||
&:not(.ui-[component-name]--disabled) {
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-card-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: $semantic-shadow-card-rest;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design - USE ACTUAL BREAKPOINT TOKENS
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
padding: $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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 🚨 CRITICAL TOKEN VALIDATION 🚨
|
||||
|
||||
#### ❌ COMMON MISTAKES TO AVOID:
|
||||
```scss
|
||||
// WRONG - These tokens DON'T EXIST:
|
||||
font-size: $semantic-typography-heading-h3; // This is a MAP, not a single value!
|
||||
padding: $semantic-spacing-layout-xs; // DOESN'T EXIST
|
||||
box-shadow: $semantic-shadow-card-featured; // DOESN'T EXIST
|
||||
border-radius: $semantic-border-radius-xs; // DOESN'T EXIST
|
||||
transition: $semantic-motion-duration-medium; // DOESN'T EXIST
|
||||
background: $semantic-color-background-primary; // DOESN'T EXIST
|
||||
|
||||
// WRONG - Hardcoded values:
|
||||
padding: 16px; // Use $semantic-spacing-component-md
|
||||
color: #333333; // Use $semantic-color-text-primary
|
||||
border-radius: 8px; // Use $semantic-border-radius-md
|
||||
```
|
||||
|
||||
#### ✅ CORRECT USAGE:
|
||||
```scss
|
||||
// RIGHT - Use map-get for typography maps:
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
|
||||
// RIGHT - Use single value tokens directly:
|
||||
padding: $semantic-spacing-component-md;
|
||||
color: $semantic-color-text-primary;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
```
|
||||
|
||||
### ⚠️ MAP USAGE WARNING ⚠️
|
||||
These tokens are **MAPS** and require `map-get()`:
|
||||
- `$semantic-typography-heading-*` (h1, h2, h3, h4, h5)
|
||||
- `$semantic-typography-body-*` (large, medium, small)
|
||||
- `$semantic-typography-button-*` (large, medium, small)
|
||||
- `$semantic-typography-label`
|
||||
- `$semantic-typography-input`
|
||||
- `$semantic-typography-caption`
|
||||
|
||||
**Always extract individual properties:**
|
||||
```scss
|
||||
// CORRECT MAP USAGE:
|
||||
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);
|
||||
```
|
||||
|
||||
### Expected Deliverables:
|
||||
1. **SCSS file** properly structured with ONLY existing semantic tokens
|
||||
2. **BEM methodology** consistently applied throughout
|
||||
3. **Responsive design** using actual breakpoint tokens
|
||||
4. **Interactive states** (hover, focus, active) defined with real tokens
|
||||
5. **Zero hardcoded values** - everything uses verified design tokens
|
||||
6. **Proper map usage** for typography tokens
|
||||
7. **Comments** explaining major sections if complex
|
||||
|
||||
### Quality Checklist:
|
||||
- [ ] All colors use actual `$semantic-color-*` tokens
|
||||
- [ ] All spacing uses actual `$semantic-spacing-*` tokens
|
||||
- [ ] All typography uses proper map-get() for maps or single tokens
|
||||
- [ ] All shadows use actual `$semantic-shadow-*` tokens
|
||||
- [ ] All borders use actual `$semantic-border-*` tokens
|
||||
- [ ] All motion uses actual `$semantic-motion-*` tokens
|
||||
- [ ] **No hardcoded values anywhere**
|
||||
- [ ] **No invented token names**
|
||||
- [ ] BEM naming convention followed
|
||||
- [ ] Responsive breakpoints implemented with actual tokens
|
||||
- [ ] File saved in correct directory
|
||||
|
||||
---
|
||||
|
||||
## Angular Component Guidelines (Enhanced)
|
||||
|
||||
### Angular 19+ Development Standards:
|
||||
|
||||
#### TEMPLATES:
|
||||
- Use inline templates in @Component decorator
|
||||
- Use new control flow syntax: @if, @for, @switch (NOT *ngIf, *ngFor)
|
||||
- Use @defer for lazy loading where appropriate
|
||||
- Put stylesheets in a separate scss file to the component
|
||||
|
||||
#### STYLING:
|
||||
- Import design tokens: @use '@projects/shared-ui/src/styles/semantic' as *;
|
||||
- **CRITICAL:** Ensure correct semantic tokens are used. Do not invent new ones.
|
||||
- **If a semantic variable is required but doesn't exist, ASK FOR INSTRUCTION**
|
||||
- NO hardcoded values - use only design tokens for colors, spacing, typography, breakpoints
|
||||
- Follow BEM naming convention for class names
|
||||
- Use SCSS nesting judiciously (max 3 levels)
|
||||
- Prefer mixins and functions from the design system
|
||||
|
||||
#### TYPESCRIPT:
|
||||
- Use standalone components (no NgModules)
|
||||
- Prefer signals over observables for state management
|
||||
- Use inject() function over constructor injection
|
||||
- Strong typing everywhere - avoid 'any'
|
||||
- Use readonly where applicable
|
||||
- Implement OnPush change detection by default
|
||||
|
||||
#### STRUCTURE:
|
||||
- Feature-based folder structure
|
||||
- Barrel exports for clean imports
|
||||
- Smart/presentational component pattern
|
||||
- Services should be provided in root unless scoped needed
|
||||
|
||||
### Component Template
|
||||
```typescript
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type ComponentSize = 'sm' | 'md' | 'lg';
|
||||
type ComponentVariant = 'primary' | 'secondary' | 'success' | 'danger';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-[component-name]',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
class="ui-[component-name]"
|
||||
[class.ui-[component-name]--{{size}}]="size"
|
||||
[class.ui-[component-name]--{{variant}}]="variant"
|
||||
[class.ui-[component-name]--disabled]="disabled"
|
||||
[class.ui-[component-name]--loading]="loading"
|
||||
[attr.aria-disabled]="disabled"
|
||||
[attr.aria-busy]="loading"
|
||||
[attr.role]="role"
|
||||
[tabindex]="disabled ? -1 : tabIndex"
|
||||
(click)="handleClick($event)"
|
||||
(keydown)="handleKeydown($event)">
|
||||
|
||||
@if (loading) {
|
||||
<span class="ui-[component-name]__loader" aria-hidden="true"></span>
|
||||
}
|
||||
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './[component-name].component.scss'
|
||||
})
|
||||
export class [ComponentName]Component {
|
||||
@Input() size: ComponentSize = 'md';
|
||||
@Input() variant: ComponentVariant = 'primary';
|
||||
@Input() disabled = false;
|
||||
@Input() loading = false;
|
||||
@Input() role = '[appropriate-aria-role]';
|
||||
@Input() tabIndex = 0;
|
||||
|
||||
@Output() clicked = new EventEmitter<MouseEvent>();
|
||||
|
||||
handleClick(event: MouseEvent): void {
|
||||
if (!this.disabled && !this.loading) {
|
||||
this.clicked.emit(event);
|
||||
}
|
||||
}
|
||||
|
||||
handleKeydown(event: KeyboardEvent): void {
|
||||
// Implement keyboard navigation
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
this.handleClick(event as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Demo Component Template
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { [ComponentName]Component } from '../[component-name].component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-[component-name]-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, [ComponentName]Component],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>[Component Name] Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<ui-[component-name] [size]="size">
|
||||
{{ size }} size
|
||||
</ui-[component-name]>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Color Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (variant of variants; track variant) {
|
||||
<ui-[component-name] [variant]="variant">
|
||||
{{ variant }}
|
||||
</ui-[component-name]>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-row">
|
||||
<ui-[component-name] [disabled]="true">Disabled</ui-[component-name]>
|
||||
<ui-[component-name] [loading]="true">Loading</ui-[component-name]>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive</h3>
|
||||
<ui-[component-name] (clicked)="handleDemoClick($event)">
|
||||
Click me
|
||||
</ui-[component-name]>
|
||||
<p>Clicked: {{ clickCount }} times</p>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './[component-name]-demo.component.scss'
|
||||
})
|
||||
export class [ComponentName]DemoComponent {
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
variants = ['primary', 'secondary', 'success', 'danger'] as const;
|
||||
clickCount = 0;
|
||||
|
||||
handleDemoClick(event: MouseEvent): void {
|
||||
this.clickCount++;
|
||||
console.log('Component clicked', event);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Component Requirements
|
||||
- **Design Tokens**: Use ONLY verified semantic tokens throughout
|
||||
- **Variants**: Include common UI system variants (colors, styles)
|
||||
- **Sizes**: sm, md, lg sizing options
|
||||
- **States**: hover, focus, active, disabled
|
||||
- **Accessibility**: ARIA support, keyboard navigation
|
||||
|
||||
---
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Unit Test Template
|
||||
```typescript
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { [ComponentName]Component } from './[component-name].component';
|
||||
|
||||
describe('[ComponentName]Component', () => {
|
||||
let component: [ComponentName]Component;
|
||||
let fixture: ComponentFixture<[ComponentName]Component>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [[ComponentName]Component]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent([ComponentName]Component);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should apply size classes correctly', () => {
|
||||
component.size = 'lg';
|
||||
fixture.detectChanges();
|
||||
|
||||
const element = fixture.nativeElement.querySelector('.ui-[component-name]');
|
||||
expect(element).toHaveClass('ui-[component-name]--lg');
|
||||
});
|
||||
|
||||
it('should handle disabled state', () => {
|
||||
component.disabled = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
const element = fixture.nativeElement.querySelector('.ui-[component-name]');
|
||||
expect(element).toHaveClass('ui-[component-name]--disabled');
|
||||
expect(element.getAttribute('aria-disabled')).toBe('true');
|
||||
});
|
||||
|
||||
it('should emit click events when not disabled', () => {
|
||||
spyOn(component.clicked, 'emit');
|
||||
const element = fixture.nativeElement.querySelector('.ui-[component-name]');
|
||||
|
||||
element.click();
|
||||
|
||||
expect(component.clicked.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not emit click events when disabled', () => {
|
||||
component.disabled = true;
|
||||
spyOn(component.clicked, 'emit');
|
||||
const element = fixture.nativeElement.querySelector('.ui-[component-name]');
|
||||
|
||||
element.click();
|
||||
|
||||
expect(component.clicked.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should support keyboard navigation', () => {
|
||||
spyOn(component, 'handleClick');
|
||||
const element = fixture.nativeElement.querySelector('.ui-[component-name]');
|
||||
|
||||
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
|
||||
element.dispatchEvent(enterEvent);
|
||||
|
||||
expect(component.handleClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Quality Checklist
|
||||
|
||||
### ⚠️ PRE-FLIGHT CHECK ⚠️
|
||||
- [ ] **CONFIRMED: Creating NEW component, not modifying existing**
|
||||
- [ ] **VERIFIED: No existing component files will be altered**
|
||||
- [ ] **CHECKED: Only adding to integration files, not modifying**
|
||||
|
||||
### Functionality
|
||||
- [ ] All size variants work correctly (sm, md, lg)
|
||||
- [ ] All color variants apply properly
|
||||
- [ ] Disabled state prevents interaction
|
||||
- [ ] Loading state shows indicator
|
||||
- [ ] Events emit correctly
|
||||
- [ ] Content projection works
|
||||
|
||||
### Styling ⚠️ CRITICAL VALIDATION ⚠️
|
||||
- [ ] **Uses ONLY verified existing semantic tokens**
|
||||
- [ ] **NO HARDCODED VALUES** (px, rem, hex colors)
|
||||
- [ ] **NO INVENTED TOKEN NAMES**
|
||||
- [ ] Responsive on all breakpoints
|
||||
- [ ] Follows BEM naming convention
|
||||
- [ ] Proper state transitions
|
||||
- [ ] Map tokens used with map-get()
|
||||
|
||||
### Accessibility
|
||||
- [ ] Proper ARIA attributes
|
||||
- [ ] Keyboard navigable
|
||||
- [ ] Focus indicators visible
|
||||
- [ ] Screen reader friendly
|
||||
- [ ] Meets WCAG 2.1 AA standards
|
||||
- [ ] Color contrast sufficient
|
||||
|
||||
### Code Quality
|
||||
- [ ] TypeScript strictly typed
|
||||
- [ ] OnPush change detection
|
||||
- [ ] Standalone component
|
||||
- [ ] Proper imports/exports
|
||||
- [ ] Demo shows all features
|
||||
- [ ] Unit tests pass
|
||||
|
||||
### Integration
|
||||
- [ ] Added to public API (additive only)
|
||||
- [ ] Demo route configured (additive only)
|
||||
- [ ] Menu item added to dashboard (additive only)
|
||||
- [ ] Build passes without errors
|
||||
- [ ] Follows project conventions
|
||||
- [ ] **Existing components still work**
|
||||
|
||||
---
|
||||
|
||||
## 🚨 FINAL TOKEN VALIDATION 🚨
|
||||
|
||||
### Before Implementation:
|
||||
1. **VERIFY** every token used exists in the reference above
|
||||
2. **ASK FOR HELP** if you need a token that doesn't exist
|
||||
3. **TEST** build process to catch any invalid tokens
|
||||
4. **NEVER GUESS** or invent token names
|
||||
|
||||
### If you need a token that doesn't exist:
|
||||
- **STOP** implementation
|
||||
- **ASK** "This component needs [specific token type] but I don't see one available. Should I [alternative approach] or do you have guidance?"
|
||||
- **WAIT** for direction rather than inventing tokens
|
||||
|
||||
This template provides a comprehensive guide for creating consistent, high-quality NEW components that integrate seamlessly with the existing design system without affecting any existing code, **and most importantly, without using non-existent tokens that would cause build failures**.
|
||||
942
UI_LANDING_PAGES_PLAN.md
Normal file
942
UI_LANDING_PAGES_PLAN.md
Normal file
@@ -0,0 +1,942 @@
|
||||
# UI Landing Pages Library Implementation Plan
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document outlines the implementation strategy for creating a comprehensive `ui-landing-pages` library within the SSuite Angular workspace. The library will provide production-ready components specifically designed for building modern websites and landing pages, integrating seamlessly with existing libraries: `ui-design-system`, `ui-essentials`, `ui-animations`, and `shared-utils`.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Library Structure
|
||||
```
|
||||
projects/ui-landing-pages/
|
||||
├── src/
|
||||
│ ├── lib/
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── heroes/ # Hero section components
|
||||
│ │ │ ├── features/ # Feature showcase components
|
||||
│ │ │ ├── social-proof/ # Testimonials, logos, stats
|
||||
│ │ │ ├── conversion/ # CTAs, pricing, forms
|
||||
│ │ │ ├── navigation/ # Headers, menus, footers
|
||||
│ │ │ ├── content/ # FAQ, team, timeline
|
||||
│ │ │ └── templates/ # Complete page templates
|
||||
│ │ ├── services/ # Landing page utilities
|
||||
│ │ ├── interfaces/ # TypeScript interfaces
|
||||
│ │ └── styles/ # Component-specific styles
|
||||
│ ├── public-api.ts
|
||||
│ └── test.ts
|
||||
├── ng-package.json
|
||||
├── package.json
|
||||
├── tsconfig.lib.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Design System Integration
|
||||
|
||||
#### Typography (Google Fonts)
|
||||
- **Primary Font**: Inter (headings, UI elements)
|
||||
- **Secondary Font**: Source Sans Pro (body text)
|
||||
- **Accent Font**: Roboto Mono (code, technical content)
|
||||
|
||||
#### Design Token Usage
|
||||
```scss
|
||||
// Import design system tokens
|
||||
@use 'ui-design-system/src/styles/tokens' as tokens;
|
||||
|
||||
.hero-section {
|
||||
padding: tokens.$spacing-section-large tokens.$spacing-container;
|
||||
background: tokens.$color-surface-primary;
|
||||
|
||||
.hero-title {
|
||||
font-family: tokens.$font-family-heading;
|
||||
font-size: tokens.$font-size-display-large;
|
||||
color: tokens.$color-text-primary;
|
||||
margin-bottom: tokens.$spacing-content-medium;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Spacing Hierarchy
|
||||
- **Section Padding**: `$spacing-section-large` (96px), `$spacing-section-medium` (64px)
|
||||
- **Content Spacing**: `$spacing-content-large` (48px), `$spacing-content-medium` (32px)
|
||||
- **Element Spacing**: `$spacing-element-large` (24px), `$spacing-element-medium` (16px)
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Foundation & Hero Components
|
||||
|
||||
### Objective
|
||||
Establish library foundation and implement essential hero section components.
|
||||
|
||||
### Timeline: 2-3 weeks
|
||||
|
||||
### Components to Implement
|
||||
|
||||
#### 1.1 HeroSection Component
|
||||
```typescript
|
||||
interface HeroSectionConfig {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
ctaPrimary?: CTAButton;
|
||||
ctaSecondary?: CTAButton;
|
||||
backgroundType?: 'solid' | 'gradient' | 'image' | 'video';
|
||||
alignment?: 'left' | 'center' | 'right';
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Responsive typography scaling
|
||||
- Multiple background options
|
||||
- Flexible CTA button layout
|
||||
- Integration with ui-animations for entrance effects
|
||||
|
||||
#### 1.2 HeroWithImage Component
|
||||
```typescript
|
||||
interface HeroWithImageConfig extends HeroSectionConfig {
|
||||
image: {
|
||||
src: string;
|
||||
alt: string;
|
||||
position: 'left' | 'right' | 'background';
|
||||
};
|
||||
layout: 'split' | 'overlay' | 'background';
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- 50/50 split layouts
|
||||
- Image overlay with opacity controls
|
||||
- Lazy loading integration
|
||||
- Responsive image handling
|
||||
|
||||
#### 1.3 HeroSplitScreen Component
|
||||
```typescript
|
||||
interface HeroSplitScreenConfig {
|
||||
leftContent: HeroContent;
|
||||
rightContent: HeroContent | MediaContent;
|
||||
verticalAlignment?: 'top' | 'center' | 'bottom';
|
||||
reverseOnMobile?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Equal split responsive layout
|
||||
- Content/media combinations
|
||||
- Mobile stacking options
|
||||
- Vertical alignment controls
|
||||
|
||||
### Integration Requirements
|
||||
|
||||
#### UI Essentials Integration
|
||||
```typescript
|
||||
import { ButtonComponent } from 'ui-essentials';
|
||||
import { ContainerComponent } from 'ui-essentials';
|
||||
import { FlexComponent } from 'ui-essentials';
|
||||
|
||||
// Use existing button component for CTAs
|
||||
@Component({
|
||||
template: `
|
||||
<ui-container [maxWidth]="'xl'" [padding]="'large'">
|
||||
<ui-flex [direction]="'column'" [gap]="'large'">
|
||||
<!-- Hero content -->
|
||||
<ui-button
|
||||
[variant]="ctaPrimary.variant"
|
||||
[size]="'large'"
|
||||
(click)="ctaPrimary.action()">
|
||||
{{ ctaPrimary.text }}
|
||||
</ui-button>
|
||||
</ui-flex>
|
||||
</ui-container>
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
#### Design System Integration
|
||||
```scss
|
||||
@use 'ui-design-system/src/styles/tokens' as tokens;
|
||||
@use 'ui-design-system/src/styles/mixins' as mixins;
|
||||
|
||||
.hero-section {
|
||||
@include mixins.section-padding();
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
tokens.$color-primary-500,
|
||||
tokens.$color-primary-700
|
||||
);
|
||||
|
||||
&__title {
|
||||
@include mixins.typography-display-large();
|
||||
color: tokens.$color-text-on-primary;
|
||||
margin-bottom: tokens.$spacing-content-medium;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
@include mixins.typography-body-large();
|
||||
color: tokens.$color-text-on-primary-muted;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Demo Implementation
|
||||
- Create `hero-sections-demo` component
|
||||
- Showcase all hero variants
|
||||
- Include interactive configuration panel
|
||||
- Demonstrate responsive behavior
|
||||
|
||||
### Deliverables
|
||||
1. Library project structure
|
||||
2. Three hero components with full functionality
|
||||
3. Comprehensive demo page
|
||||
4. Unit tests for all components
|
||||
5. Documentation with usage examples
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Feature Sections & Social Proof
|
||||
|
||||
### Objective
|
||||
Build components for showcasing product features and establishing credibility.
|
||||
|
||||
### Timeline: 3-4 weeks
|
||||
|
||||
### Components to Implement
|
||||
|
||||
#### 2.1 FeatureGrid Component
|
||||
```typescript
|
||||
interface FeatureGridConfig {
|
||||
features: FeatureItem[];
|
||||
columns: 2 | 3 | 4;
|
||||
iconStyle: 'outline' | 'filled' | 'duotone';
|
||||
layout: 'card' | 'minimal' | 'centered';
|
||||
}
|
||||
|
||||
interface FeatureItem {
|
||||
icon: string; // FontAwesome icon name
|
||||
title: string;
|
||||
description: string;
|
||||
link?: { text: string; url: string; };
|
||||
}
|
||||
```
|
||||
|
||||
**Integration:**
|
||||
- Use `ui-essentials` Card component as base
|
||||
- Apply `ui-design-system` grid system
|
||||
- Integrate FontAwesome icons
|
||||
- Add hover animations from `ui-animations`
|
||||
|
||||
#### 2.2 FeatureShowcase Component
|
||||
```typescript
|
||||
interface FeatureShowcaseConfig {
|
||||
features: FeatureShowcaseItem[];
|
||||
alternateLayout: boolean;
|
||||
imageAspectRatio: '16:9' | '4:3' | '1:1';
|
||||
}
|
||||
|
||||
interface FeatureShowcaseItem {
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
benefits?: string[];
|
||||
cta?: CTAButton;
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Alternating left/right layouts
|
||||
- Image lazy loading
|
||||
- Smooth scroll animations
|
||||
- Responsive stacking
|
||||
|
||||
#### 2.3 TestimonialCarousel Component
|
||||
```typescript
|
||||
interface TestimonialCarouselConfig {
|
||||
testimonials: Testimonial[];
|
||||
autoplay: boolean;
|
||||
showAvatars: boolean;
|
||||
variant: 'card' | 'quote' | 'minimal';
|
||||
}
|
||||
|
||||
interface Testimonial {
|
||||
quote: string;
|
||||
author: string;
|
||||
title?: string;
|
||||
company?: string;
|
||||
avatar?: string;
|
||||
rating?: number;
|
||||
}
|
||||
```
|
||||
|
||||
**Integration:**
|
||||
- Extend existing carousel functionality
|
||||
- Use `ui-essentials` Avatar component
|
||||
- Apply smooth transitions from `ui-animations`
|
||||
- Implement touch/swipe gestures
|
||||
|
||||
#### 2.4 LogoCloud Component
|
||||
```typescript
|
||||
interface LogoCloudConfig {
|
||||
logos: CompanyLogo[];
|
||||
layout: 'grid' | 'marquee' | 'carousel';
|
||||
grayscale: boolean;
|
||||
hoverEffects: boolean;
|
||||
}
|
||||
|
||||
interface CompanyLogo {
|
||||
name: string;
|
||||
logo: string;
|
||||
url?: string;
|
||||
width?: number;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.5 StatisticsDisplay Component
|
||||
```typescript
|
||||
interface StatisticsDisplayConfig {
|
||||
stats: StatisticItem[];
|
||||
layout: 'horizontal' | 'grid' | 'centered';
|
||||
animateNumbers: boolean;
|
||||
backgroundVariant: 'transparent' | 'subtle' | 'accent';
|
||||
}
|
||||
|
||||
interface StatisticItem {
|
||||
value: string | number;
|
||||
label: string;
|
||||
suffix?: string;
|
||||
prefix?: string;
|
||||
description?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Styling Architecture
|
||||
```scss
|
||||
// Feature components base styles
|
||||
.feature-grid {
|
||||
@include mixins.responsive-grid(
|
||||
$mobile: 1,
|
||||
$tablet: 2,
|
||||
$desktop: var(--columns, 3)
|
||||
);
|
||||
gap: tokens.$spacing-content-large;
|
||||
|
||||
&__item {
|
||||
@include mixins.card-base();
|
||||
padding: tokens.$spacing-content-medium;
|
||||
border-radius: tokens.$border-radius-large;
|
||||
|
||||
&:hover {
|
||||
@include mixins.elevation-hover();
|
||||
transform: translateY(-2px);
|
||||
transition: tokens.$transition-medium;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: tokens.$color-primary-500;
|
||||
margin-bottom: tokens.$spacing-element-medium;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include mixins.typography-heading-small();
|
||||
margin-bottom: tokens.$spacing-element-small;
|
||||
}
|
||||
|
||||
&__description {
|
||||
@include mixins.typography-body-medium();
|
||||
color: tokens.$color-text-secondary;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Demo Implementation
|
||||
- `feature-sections-demo` with grid and showcase variants
|
||||
- `social-proof-demo` with testimonials and logos
|
||||
- Interactive configuration panels
|
||||
- Performance optimization examples
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Conversion & Call-to-Action
|
||||
|
||||
### Objective
|
||||
Implement high-converting components focused on user actions and conversions.
|
||||
|
||||
### Timeline: 3-4 weeks
|
||||
|
||||
### Components to Implement
|
||||
|
||||
#### 3.1 CTASection Component
|
||||
```typescript
|
||||
interface CTASectionConfig {
|
||||
title: string;
|
||||
description?: string;
|
||||
ctaPrimary: CTAButton;
|
||||
ctaSecondary?: CTAButton;
|
||||
backgroundType: 'gradient' | 'pattern' | 'image' | 'solid';
|
||||
urgency?: UrgencyConfig;
|
||||
}
|
||||
|
||||
interface UrgencyConfig {
|
||||
type: 'countdown' | 'limited-offer' | 'social-proof';
|
||||
text: string;
|
||||
endDate?: Date;
|
||||
remaining?: number;
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Multiple background variants
|
||||
- Urgency indicators
|
||||
- A/B testing data attributes
|
||||
- Conversion tracking hooks
|
||||
|
||||
#### 3.2 PricingTable Component
|
||||
```typescript
|
||||
interface PricingTableConfig {
|
||||
plans: PricingPlan[];
|
||||
billingToggle: BillingToggle;
|
||||
featuresComparison: boolean;
|
||||
highlightedPlan?: string;
|
||||
}
|
||||
|
||||
interface PricingPlan {
|
||||
id: string;
|
||||
name: string;
|
||||
price: PriceStructure;
|
||||
features: PricingFeature[];
|
||||
cta: CTAButton;
|
||||
badge?: string;
|
||||
popular?: boolean;
|
||||
}
|
||||
|
||||
interface PriceStructure {
|
||||
monthly: number;
|
||||
yearly: number;
|
||||
currency: string;
|
||||
suffix?: string; // per user, per month, etc.
|
||||
}
|
||||
```
|
||||
|
||||
**Integration:**
|
||||
- Use `ui-essentials` Table component for feature comparison
|
||||
- Implement smooth toggle animations
|
||||
- Apply pricing card hover effects
|
||||
- Include form validation for CTAs
|
||||
|
||||
#### 3.3 NewsletterSignup Component
|
||||
```typescript
|
||||
interface NewsletterSignupConfig {
|
||||
title: string;
|
||||
description?: string;
|
||||
placeholder: string;
|
||||
ctaText: string;
|
||||
privacyText?: string;
|
||||
successMessage: string;
|
||||
variant: 'inline' | 'modal' | 'sidebar' | 'footer';
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Email validation
|
||||
- GDPR compliance options
|
||||
- Success/error states
|
||||
- Integration with email services
|
||||
|
||||
#### 3.4 ContactForm Component
|
||||
```typescript
|
||||
interface ContactFormConfig {
|
||||
fields: FormField[];
|
||||
submitText: string;
|
||||
successMessage: string;
|
||||
layout: 'single-column' | 'two-column' | 'inline';
|
||||
validation: ValidationRules;
|
||||
}
|
||||
|
||||
interface FormField {
|
||||
type: 'text' | 'email' | 'tel' | 'textarea' | 'select' | 'checkbox';
|
||||
name: string;
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
required: boolean;
|
||||
options?: SelectOption[];
|
||||
}
|
||||
```
|
||||
|
||||
**Integration:**
|
||||
- Extend `ui-essentials` form components
|
||||
- Apply `shared-utils` validation
|
||||
- Include accessibility features
|
||||
- Support for custom field types
|
||||
|
||||
### Advanced Styling
|
||||
```scss
|
||||
// CTA Section with gradient backgrounds
|
||||
.cta-section {
|
||||
@include mixins.section-padding();
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&--gradient {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
tokens.$color-primary-500 0%,
|
||||
tokens.$color-primary-700 50%,
|
||||
tokens.$color-accent-500 100%
|
||||
);
|
||||
}
|
||||
|
||||
&--pattern {
|
||||
background-color: tokens.$color-surface-secondary;
|
||||
background-image: url('data:image/svg+xml,...'); // Pattern SVG
|
||||
background-size: 60px 60px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include mixins.typography-display-medium();
|
||||
color: tokens.$color-text-on-primary;
|
||||
margin-bottom: tokens.$spacing-content-medium;
|
||||
}
|
||||
}
|
||||
|
||||
// Pricing table responsive design
|
||||
.pricing-table {
|
||||
@include mixins.responsive-grid(
|
||||
$mobile: 1,
|
||||
$tablet: 2,
|
||||
$desktop: 3
|
||||
);
|
||||
gap: tokens.$spacing-content-medium;
|
||||
|
||||
&__plan {
|
||||
@include mixins.card-elevated();
|
||||
position: relative;
|
||||
padding: tokens.$spacing-content-large;
|
||||
border-radius: tokens.$border-radius-large;
|
||||
|
||||
&--popular {
|
||||
border: 2px solid tokens.$color-primary-500;
|
||||
transform: scale(1.05);
|
||||
|
||||
&::before {
|
||||
content: 'Most Popular';
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: tokens.$color-primary-500;
|
||||
color: tokens.$color-text-on-primary;
|
||||
padding: tokens.$spacing-element-small tokens.$spacing-element-medium;
|
||||
border-radius: tokens.$border-radius-medium;
|
||||
font-size: tokens.$font-size-small;
|
||||
font-weight: tokens.$font-weight-semibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Navigation & Footer
|
||||
|
||||
### Objective
|
||||
Create comprehensive navigation and footer systems for complete website layouts.
|
||||
|
||||
### Timeline: 2-3 weeks
|
||||
|
||||
### Components to Implement
|
||||
|
||||
#### 4.1 LandingHeader Component
|
||||
```typescript
|
||||
interface LandingHeaderConfig {
|
||||
logo: LogoConfig;
|
||||
navigation: NavigationItem[];
|
||||
cta?: CTAButton;
|
||||
variant: 'transparent' | 'solid' | 'blur';
|
||||
sticky: boolean;
|
||||
mobileBreakpoint: number;
|
||||
}
|
||||
|
||||
interface LogoConfig {
|
||||
src?: string;
|
||||
text?: string;
|
||||
href: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
interface NavigationItem {
|
||||
label: string;
|
||||
href?: string;
|
||||
children?: NavigationItem[];
|
||||
megaMenu?: MegaMenuConfig;
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Smooth scroll spy
|
||||
- Mobile hamburger menu
|
||||
- Transparent/solid transitions
|
||||
- Mega menu support
|
||||
|
||||
#### 4.2 MegaMenu Component
|
||||
```typescript
|
||||
interface MegaMenuConfig {
|
||||
columns: MegaMenuColumn[];
|
||||
width: 'container' | 'full' | 'auto';
|
||||
showOnHover: boolean;
|
||||
}
|
||||
|
||||
interface MegaMenuColumn {
|
||||
title?: string;
|
||||
items: NavigationItem[];
|
||||
featured?: FeaturedContent;
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.3 FooterSection Component
|
||||
```typescript
|
||||
interface FooterSectionConfig {
|
||||
columns: FooterColumn[];
|
||||
newsletter?: NewsletterSignupConfig;
|
||||
socialLinks?: SocialLink[];
|
||||
copyright: string;
|
||||
logo?: LogoConfig;
|
||||
variant: 'simple' | 'comprehensive' | 'minimal';
|
||||
}
|
||||
|
||||
interface FooterColumn {
|
||||
title: string;
|
||||
links: FooterLink[];
|
||||
}
|
||||
|
||||
interface FooterLink {
|
||||
text: string;
|
||||
href: string;
|
||||
external?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.4 StickyNavbar Component
|
||||
```typescript
|
||||
interface StickyNavbarConfig extends LandingHeaderConfig {
|
||||
hideOnScroll: boolean;
|
||||
showAfter: number; // pixels
|
||||
shrinkOnScroll: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Navigation Styling
|
||||
```scss
|
||||
.landing-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
transition: tokens.$transition-medium;
|
||||
|
||||
&--transparent {
|
||||
background: transparent;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
&--solid {
|
||||
background: tokens.$color-surface-primary;
|
||||
box-shadow: tokens.$shadow-small;
|
||||
}
|
||||
|
||||
&__container {
|
||||
@include mixins.container-max-width();
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: tokens.$spacing-element-medium tokens.$spacing-container;
|
||||
}
|
||||
|
||||
&__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: tokens.$spacing-element-large;
|
||||
|
||||
@media (max-width: tokens.$breakpoint-tablet) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__nav-item {
|
||||
@include mixins.typography-body-medium();
|
||||
color: tokens.$color-text-primary;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
padding: tokens.$spacing-element-small 0;
|
||||
|
||||
&:hover {
|
||||
color: tokens.$color-primary-500;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: tokens.$color-primary-500;
|
||||
transition: width tokens.$transition-fast;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Content Sections & Templates
|
||||
|
||||
### Objective
|
||||
Complete the library with supplementary content components and ready-to-use page templates.
|
||||
|
||||
### Timeline: 4-5 weeks
|
||||
|
||||
### Components to Implement
|
||||
|
||||
#### 5.1 Content Components
|
||||
|
||||
##### FAQSection Component
|
||||
```typescript
|
||||
interface FAQSectionConfig {
|
||||
title: string;
|
||||
description?: string;
|
||||
faqs: FAQItem[];
|
||||
searchable: boolean;
|
||||
categories?: string[];
|
||||
}
|
||||
|
||||
interface FAQItem {
|
||||
id: string;
|
||||
question: string;
|
||||
answer: string;
|
||||
category?: string;
|
||||
}
|
||||
```
|
||||
|
||||
##### TeamGrid Component
|
||||
```typescript
|
||||
interface TeamGridConfig {
|
||||
title: string;
|
||||
members: TeamMember[];
|
||||
columns: 2 | 3 | 4;
|
||||
showSocial: boolean;
|
||||
}
|
||||
|
||||
interface TeamMember {
|
||||
name: string;
|
||||
role: string;
|
||||
bio?: string;
|
||||
avatar: string;
|
||||
social?: SocialLinks;
|
||||
}
|
||||
```
|
||||
|
||||
##### TimelineSection Component
|
||||
```typescript
|
||||
interface TimelineSectionConfig {
|
||||
title: string;
|
||||
events: TimelineEvent[];
|
||||
orientation: 'vertical' | 'horizontal';
|
||||
variant: 'minimal' | 'detailed' | 'milestone';
|
||||
}
|
||||
|
||||
interface TimelineEvent {
|
||||
date: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image?: string;
|
||||
milestone?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.2 Page Templates
|
||||
|
||||
##### SaaSLandingPage Template
|
||||
```typescript
|
||||
interface SaaSLandingPageConfig {
|
||||
hero: HeroSectionConfig;
|
||||
features: FeatureGridConfig;
|
||||
socialProof: TestimonialCarouselConfig;
|
||||
pricing: PricingTableConfig;
|
||||
cta: CTASectionConfig;
|
||||
footer: FooterSectionConfig;
|
||||
}
|
||||
```
|
||||
|
||||
**Template Structure:**
|
||||
1. Header with transparent navigation
|
||||
2. Hero section with product demo
|
||||
3. Feature showcase (3-column grid)
|
||||
4. Social proof (testimonials + logo cloud)
|
||||
5. Pricing section
|
||||
6. FAQ section
|
||||
7. Final CTA section
|
||||
8. Comprehensive footer
|
||||
|
||||
##### ProductLandingPage Template
|
||||
```typescript
|
||||
interface ProductLandingPageConfig {
|
||||
hero: HeroWithImageConfig;
|
||||
features: FeatureShowcaseConfig;
|
||||
gallery: ProductGallery;
|
||||
reviews: ReviewSection;
|
||||
purchase: PurchaseSection;
|
||||
}
|
||||
```
|
||||
|
||||
##### AgencyLandingPage Template
|
||||
```typescript
|
||||
interface AgencyLandingPageConfig {
|
||||
hero: HeroSectionConfig;
|
||||
services: FeatureGridConfig;
|
||||
portfolio: PortfolioSection;
|
||||
team: TeamGridConfig;
|
||||
testimonials: TestimonialCarouselConfig;
|
||||
contact: ContactFormConfig;
|
||||
}
|
||||
```
|
||||
|
||||
### Template Implementation
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ui-saas-landing-page',
|
||||
template: `
|
||||
<ui-landing-header [config]="config.header"></ui-landing-header>
|
||||
|
||||
<main>
|
||||
<ui-hero-section [config]="config.hero"></ui-hero-section>
|
||||
|
||||
<ui-page-section background="subtle">
|
||||
<ui-feature-grid [config]="config.features"></ui-feature-grid>
|
||||
</ui-page-section>
|
||||
|
||||
<ui-page-section>
|
||||
<ui-testimonial-carousel [config]="config.testimonials"></ui-testimonial-carousel>
|
||||
</ui-page-section>
|
||||
|
||||
<ui-page-section background="accent">
|
||||
<ui-pricing-table [config]="config.pricing"></ui-pricing-table>
|
||||
</ui-page-section>
|
||||
|
||||
<ui-page-section>
|
||||
<ui-faq-section [config]="config.faq"></ui-faq-section>
|
||||
</ui-page-section>
|
||||
|
||||
<ui-cta-section [config]="config.cta"></ui-cta-section>
|
||||
</main>
|
||||
|
||||
<ui-footer-section [config]="config.footer"></ui-footer-section>
|
||||
`,
|
||||
styleUrls: ['./saas-landing-page.component.scss']
|
||||
})
|
||||
export class SaaSLandingPageComponent {
|
||||
@Input() config!: SaaSLandingPageConfig;
|
||||
}
|
||||
```
|
||||
|
||||
## Technical Specifications
|
||||
|
||||
### Performance Requirements
|
||||
- Lazy loading for all images
|
||||
- Code splitting by component category
|
||||
- Tree-shakeable exports
|
||||
- Optimal bundle size (<50kb per component category)
|
||||
- Lighthouse score >90 for demo pages
|
||||
|
||||
### Accessibility Standards
|
||||
- WCAG 2.1 AA compliance
|
||||
- Proper ARIA labels and roles
|
||||
- Keyboard navigation support
|
||||
- Screen reader optimization
|
||||
- Color contrast ratios >4.5:1
|
||||
|
||||
### Browser Support
|
||||
- Chrome/Edge 90+
|
||||
- Firefox 88+
|
||||
- Safari 14+
|
||||
- Mobile browsers (iOS Safari, Chrome Mobile)
|
||||
|
||||
### Testing Strategy
|
||||
- Unit tests: >90% coverage
|
||||
- Component integration tests
|
||||
- Visual regression tests
|
||||
- Performance benchmarks
|
||||
- Accessibility automated testing
|
||||
|
||||
### Documentation Requirements
|
||||
- Storybook integration
|
||||
- API documentation with examples
|
||||
- Usage guidelines
|
||||
- Design system integration guide
|
||||
- Migration guides
|
||||
|
||||
## Integration Checklist
|
||||
|
||||
### Design System Integration
|
||||
- [ ] Import design tokens correctly
|
||||
- [ ] Use consistent spacing hierarchy
|
||||
- [ ] Apply typography scale
|
||||
- [ ] Implement color system
|
||||
- [ ] Follow component naming conventions
|
||||
|
||||
### UI Essentials Integration
|
||||
- [ ] Extend existing components where applicable
|
||||
- [ ] Reuse layout components (Container, Flex, Grid)
|
||||
- [ ] Apply consistent button styles
|
||||
- [ ] Use existing form components
|
||||
- [ ] Maintain consistent component API patterns
|
||||
|
||||
### Animation Integration
|
||||
- [ ] Apply entrance animations
|
||||
- [ ] Add hover/interaction effects
|
||||
- [ ] Implement scroll-triggered animations
|
||||
- [ ] Use consistent easing curves
|
||||
- [ ] Optimize for performance
|
||||
|
||||
### Utilities Integration
|
||||
- [ ] Use shared validation logic
|
||||
- [ ] Apply common helper functions
|
||||
- [ ] Implement consistent error handling
|
||||
- [ ] Use shared interfaces where applicable
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Development Metrics
|
||||
- All components implement consistent APIs
|
||||
- 100% TypeScript coverage
|
||||
- Zero accessibility violations
|
||||
- Performance budgets met
|
||||
- Documentation completeness
|
||||
|
||||
### Usage Metrics
|
||||
- Component adoption rate
|
||||
- Bundle size impact
|
||||
- Build time impact
|
||||
- Developer satisfaction scores
|
||||
- Community contributions
|
||||
|
||||
## Conclusion
|
||||
|
||||
This implementation plan provides a structured approach to building a comprehensive ui-landing-pages library that integrates seamlessly with the existing SSuite ecosystem. By following this phased approach, the team can deliver value incrementally while maintaining high quality standards and consistency across all components.
|
||||
|
||||
The resulting library will enable rapid development of professional landing pages and marketing websites while ensuring accessibility, performance, and maintainability standards are met.
|
||||
245
angular.json
245
angular.json
@@ -3,23 +3,23 @@
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"shared-ui": {
|
||||
"ui-design-system": {
|
||||
"projectType": "library",
|
||||
"root": "projects/shared-ui",
|
||||
"sourceRoot": "projects/shared-ui/src",
|
||||
"root": "projects/ui-design-system",
|
||||
"sourceRoot": "projects/ui-design-system/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "projects/shared-ui/ng-package.json"
|
||||
"project": "projects/ui-design-system/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "projects/shared-ui/tsconfig.lib.prod.json"
|
||||
"tsConfig": "projects/ui-design-system/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "projects/shared-ui/tsconfig.lib.json"
|
||||
"tsConfig": "projects/ui-design-system/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
@@ -27,7 +27,7 @@
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "projects/shared-ui/tsconfig.spec.json",
|
||||
"tsConfig": "projects/ui-design-system/tsconfig.spec.json",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
@@ -196,6 +196,237 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ui-animations": {
|
||||
"projectType": "library",
|
||||
"root": "projects/ui-animations",
|
||||
"sourceRoot": "projects/ui-animations/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "projects/ui-animations/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "projects/ui-animations/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "projects/ui-animations/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "projects/ui-animations/tsconfig.spec.json",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ui-accessibility": {
|
||||
"projectType": "library",
|
||||
"root": "projects/ui-accessibility",
|
||||
"sourceRoot": "projects/ui-accessibility/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "projects/ui-accessibility/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "projects/ui-accessibility/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "projects/ui-accessibility/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "projects/ui-accessibility/tsconfig.spec.json",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ui-data-utils": {
|
||||
"projectType": "library",
|
||||
"root": "projects/ui-data-utils",
|
||||
"sourceRoot": "projects/ui-data-utils/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "projects/ui-data-utils/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "projects/ui-data-utils/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "projects/ui-data-utils/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "projects/ui-data-utils/tsconfig.spec.json",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"hcl-studio": {
|
||||
"projectType": "library",
|
||||
"root": "projects/hcl-studio",
|
||||
"sourceRoot": "projects/hcl-studio/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "projects/hcl-studio/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "projects/hcl-studio/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "projects/hcl-studio/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "projects/hcl-studio/tsconfig.spec.json",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ui-font-manager": {
|
||||
"projectType": "library",
|
||||
"root": "projects/ui-font-manager",
|
||||
"sourceRoot": "projects/ui-font-manager/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "projects/ui-font-manager/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "projects/ui-font-manager/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "projects/ui-font-manager/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "projects/ui-font-manager/tsconfig.spec.json",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ui-code-display": {
|
||||
"projectType": "library",
|
||||
"root": "projects/ui-code-display",
|
||||
"sourceRoot": "projects/ui-code-display/src",
|
||||
"prefix": "ui",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "projects/ui-code-display/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "projects/ui-code-display/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "projects/ui-code-display/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "projects/ui-code-display/tsconfig.spec.json",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ui-backgrounds": {
|
||||
"projectType": "library",
|
||||
"root": "projects/ui-backgrounds",
|
||||
"sourceRoot": "projects/ui-backgrounds/src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "projects/ui-backgrounds/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "projects/ui-backgrounds/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "projects/ui-backgrounds/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "projects/ui-backgrounds/tsconfig.spec.json",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -20,6 +20,7 @@
|
||||
"@fortawesome/free-brands-svg-icons": "^7.0.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.0.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
@@ -29,6 +30,7 @@
|
||||
"@angular/cli": "^19.2.15",
|
||||
"@angular/compiler-cli": "^19.2.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"jasmine-core": "~5.6.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
@@ -5252,6 +5254,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prismjs": {
|
||||
"version": "1.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
|
||||
"integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||
@@ -11266,6 +11274,14 @@
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/proc-log": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"@fortawesome/free-brands-svg-icons": "^7.0.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.0.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
@@ -31,6 +32,7 @@
|
||||
"@angular/cli": "^19.2.15",
|
||||
"@angular/compiler-cli": "^19.2.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"jasmine-core": "~5.6.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { ButtonComponent } from '../../../ui-essentials/src/public-api';
|
||||
import { DashboardComponent } from "./features/dashboard/dashboard.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet, ButtonComponent],
|
||||
imports: [DashboardComponent],
|
||||
template: `
|
||||
|
||||
<ui-button></ui-button>
|
||||
<skyui-dashboard></skyui-dashboard>
|
||||
|
||||
`
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'demo-ui-essentials';
|
||||
}
|
||||
|
||||
@@ -1,8 +1,48 @@
|
||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import { ApplicationConfig, provideZoneChangeDetection, importProvidersFrom } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { UiAccessibilityModule } from 'ui-accessibility';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)]
|
||||
providers: [
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(routes),
|
||||
importProvidersFrom(
|
||||
UiAccessibilityModule.forRoot({
|
||||
announcer: {
|
||||
defaultPoliteness: 'polite',
|
||||
defaultDuration: 4000,
|
||||
enabled: true
|
||||
},
|
||||
focusManagement: {
|
||||
trapFocus: true,
|
||||
restoreFocus: true,
|
||||
focusVisibleEnabled: true
|
||||
},
|
||||
keyboard: {
|
||||
enableShortcuts: true,
|
||||
enableArrowNavigation: true
|
||||
},
|
||||
skipLinks: {
|
||||
enabled: true,
|
||||
position: 'top',
|
||||
links: [
|
||||
{ href: '#main', text: 'Skip to main content' },
|
||||
{ href: '#nav', text: 'Skip to navigation' }
|
||||
]
|
||||
},
|
||||
accessibility: {
|
||||
respectReducedMotion: true,
|
||||
respectHighContrast: true,
|
||||
injectAccessibilityStyles: true,
|
||||
addBodyClasses: true
|
||||
},
|
||||
development: {
|
||||
warnings: true,
|
||||
logging: true
|
||||
}
|
||||
})
|
||||
)
|
||||
]
|
||||
};
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
<div class="accessibility-demo">
|
||||
<!-- Skip Links Demo -->
|
||||
<ui-skip-links></ui-skip-links>
|
||||
|
||||
<header class="demo-header">
|
||||
<h1 id="main">UI Accessibility Library Demo</h1>
|
||||
<p>Interactive examples of accessibility features with WCAG 2.1 Level AA compliance.</p>
|
||||
|
||||
<ui-screen-reader-only>
|
||||
This page demonstrates various accessibility features. Use keyboard shortcuts: Ctrl+Shift+A to make announcements, Ctrl+M to toggle modal.
|
||||
</ui-screen-reader-only>
|
||||
</header>
|
||||
|
||||
<!-- Current Preferences Display -->
|
||||
<section class="demo-section" aria-labelledby="preferences-heading">
|
||||
<h2 id="preferences-heading">Accessibility Preferences</h2>
|
||||
<div class="preferences-display">
|
||||
<p><strong>High Contrast:</strong> {{ currentPreferences.prefersHighContrast ? 'Enabled' : 'Disabled' }}</p>
|
||||
<p><strong>Reduced Motion:</strong> {{ currentPreferences.prefersReducedMotion ? 'Enabled' : 'Disabled' }}</p>
|
||||
<p><strong>Color Scheme:</strong> {{ currentPreferences.colorScheme || 'Not detected' }}</p>
|
||||
<ui-button variant="tonal" size="small" (click)="refreshPreferences()">
|
||||
Refresh Preferences
|
||||
</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Live Announcements Demo -->
|
||||
<section class="demo-section" aria-labelledby="announcements-heading">
|
||||
<h2 id="announcements-heading">Live Announcements</h2>
|
||||
<div class="demo-controls">
|
||||
<div class="input-group">
|
||||
<label for="announcement-text">Announcement Text:</label>
|
||||
<input
|
||||
id="announcement-text"
|
||||
type="text"
|
||||
[(ngModel)]="announcementText"
|
||||
class="demo-input"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="politeness-level">Politeness Level:</label>
|
||||
<select id="politeness-level" [(ngModel)]="selectedPoliteness" class="demo-select">
|
||||
<option value="polite">Polite</option>
|
||||
<option value="assertive">Assertive</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<ui-button (click)="makeAnnouncement()">Make Announcement</ui-button>
|
||||
<ui-button variant="tonal" (click)="makeSequenceAnnouncement()">Sequence Announcement</ui-button>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<ui-button variant="filled" (click)="announceSuccess()">Success Message</ui-button>
|
||||
<ui-button variant="outlined" (click)="announceError()">Error Message</ui-button>
|
||||
<ui-button variant="tonal" (click)="announceLoading()">Loading Message</ui-button>
|
||||
<ui-button variant="outlined" (click)="simulateFormValidation()">Simulate Validation</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Focus Management Demo -->
|
||||
<section class="demo-section" aria-labelledby="focus-heading">
|
||||
<h2 id="focus-heading">Focus Management</h2>
|
||||
|
||||
<div class="focus-demo">
|
||||
<div class="input-group">
|
||||
<label for="monitored-input">Monitored Input (Focus Origin: {{ focusOrigin }}):</label>
|
||||
<input
|
||||
#monitoredInput
|
||||
id="monitored-input"
|
||||
type="text"
|
||||
placeholder="Click, tab, or programmatically focus this input"
|
||||
class="demo-input"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<ui-button (click)="focusViaKeyboard()">Focus via Keyboard</ui-button>
|
||||
<ui-button (click)="focusViaMouse()">Focus via Mouse</ui-button>
|
||||
<ui-button (click)="focusViaProgram()">Focus via Program</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Focus Trap Demo -->
|
||||
<section class="demo-section" aria-labelledby="trap-heading">
|
||||
<h2 id="trap-heading">Focus Trap</h2>
|
||||
|
||||
<div class="trap-controls">
|
||||
<ui-button (click)="toggleFocusTrap()">
|
||||
{{ trapEnabled ? 'Disable' : 'Enable' }} Focus Trap
|
||||
</ui-button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
#trapContainer
|
||||
class="focus-trap-container"
|
||||
[class.trap-enabled]="trapEnabled"
|
||||
[uiFocusTrap]="trapEnabled"
|
||||
[autoFocus]="true"
|
||||
[restoreFocus]="true"
|
||||
>
|
||||
<h3>Focus Trap Container {{ trapEnabled ? '(Active)' : '(Inactive)' }}</h3>
|
||||
<p>When enabled, Tab navigation will be trapped within this container.</p>
|
||||
|
||||
<input type="text" placeholder="First input" class="demo-input">
|
||||
<input type="text" placeholder="Second input" class="demo-input">
|
||||
<ui-button>Trapped Button 1</ui-button>
|
||||
<ui-button variant="tonal">Trapped Button 2</ui-button>
|
||||
<select class="demo-select">
|
||||
<option>Option 1</option>
|
||||
<option>Option 2</option>
|
||||
</select>
|
||||
<textarea placeholder="Textarea" class="demo-textarea"></textarea>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Arrow Navigation Demo -->
|
||||
<section class="demo-section" aria-labelledby="navigation-heading">
|
||||
<h2 id="navigation-heading">Arrow Navigation</h2>
|
||||
|
||||
<div class="navigation-demo">
|
||||
<h3>Vertical Navigation (Use ↑↓ arrows):</h3>
|
||||
<ul
|
||||
class="navigation-list vertical"
|
||||
uiArrowNavigation="vertical"
|
||||
[wrap]="true"
|
||||
(navigationChange)="onNavigationChange($event)"
|
||||
>
|
||||
@for (item of navigationItems; track item.id) {
|
||||
<li
|
||||
[tabindex]="$first ? '0' : '-1'"
|
||||
[class.disabled]="item.disabled"
|
||||
[attr.aria-disabled]="item.disabled"
|
||||
>
|
||||
{{ item.label }}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
<h3>Grid Navigation (Use ↑↓←→ arrows, 3 columns):</h3>
|
||||
<div
|
||||
class="navigation-grid"
|
||||
uiArrowNavigation="grid"
|
||||
[gridColumns]="3"
|
||||
[wrap]="true"
|
||||
(navigationChange)="onGridNavigationChange($event)"
|
||||
>
|
||||
@for (item of gridItems; track item.id) {
|
||||
<button
|
||||
class="grid-item"
|
||||
[tabindex]="$first ? '0' : '-1'"
|
||||
[disabled]="item.disabled"
|
||||
[attr.aria-disabled]="item.disabled"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Modal Demo -->
|
||||
<section class="demo-section" aria-labelledby="modal-heading">
|
||||
<h2 id="modal-heading">Modal with Focus Trap</h2>
|
||||
|
||||
<ui-button (click)="toggleModal()">Open Modal</ui-button>
|
||||
|
||||
@if (showModal) {
|
||||
<div class="modal-backdrop" (click)="toggleModal()">
|
||||
<div
|
||||
class="modal-content"
|
||||
[uiFocusTrap]="true"
|
||||
[autoFocus]="true"
|
||||
[restoreFocus]="true"
|
||||
role="dialog"
|
||||
aria-labelledby="modal-title"
|
||||
aria-modal="true"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<header class="modal-header">
|
||||
<h3 id="modal-title">Demo Modal</h3>
|
||||
<button
|
||||
class="modal-close"
|
||||
(click)="toggleModal()"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>This modal demonstrates focus trapping. Tab navigation is contained within the modal.</p>
|
||||
|
||||
<input type="text" placeholder="Modal input" class="demo-input">
|
||||
<ui-button variant="tonal">Modal Button</ui-button>
|
||||
</div>
|
||||
|
||||
<footer class="modal-footer">
|
||||
<ui-button (click)="toggleModal()">Close</ui-button>
|
||||
<ui-button variant="filled">Save Changes</ui-button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Screen Reader Components -->
|
||||
<section class="demo-section" aria-labelledby="sr-heading">
|
||||
<h2 id="sr-heading">Screen Reader Components</h2>
|
||||
|
||||
<div class="sr-demo">
|
||||
<h3>Screen Reader Only Text:</h3>
|
||||
<p>
|
||||
This text is visible.
|
||||
<ui-screen-reader-only>This text is only for screen readers.</ui-screen-reader-only>
|
||||
This text is visible again.
|
||||
</p>
|
||||
|
||||
<h3>Focusable Screen Reader Text:</h3>
|
||||
<ui-screen-reader-only type="focusable">
|
||||
This text becomes visible when focused (try tabbing through the page).
|
||||
</ui-screen-reader-only>
|
||||
|
||||
<h3>Dynamic Status Messages:</h3>
|
||||
<div class="status-demo">
|
||||
<ui-screen-reader-only type="status" statusType="success">
|
||||
Form submitted successfully!
|
||||
</ui-screen-reader-only>
|
||||
|
||||
<ui-screen-reader-only type="status" statusType="error">
|
||||
Error: Please check your input.
|
||||
</ui-screen-reader-only>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Keyboard Shortcuts Info -->
|
||||
<section class="demo-section" aria-labelledby="shortcuts-heading">
|
||||
<h2 id="shortcuts-heading">Keyboard Shortcuts</h2>
|
||||
|
||||
<div class="shortcuts-info">
|
||||
<dl class="shortcuts-list">
|
||||
<dt>Ctrl + Shift + A</dt>
|
||||
<dd>Make announcement</dd>
|
||||
|
||||
<dt>Ctrl + M</dt>
|
||||
<dd>Toggle modal</dd>
|
||||
|
||||
<dt>Tab / Shift + Tab</dt>
|
||||
<dd>Navigate through interactive elements</dd>
|
||||
|
||||
<dt>Arrow Keys</dt>
|
||||
<dd>Navigate within lists and grids (when focused)</dd>
|
||||
|
||||
<dt>Home / End</dt>
|
||||
<dd>Jump to first/last item in navigable containers</dd>
|
||||
|
||||
<dt>Escape</dt>
|
||||
<dd>Close modal or exit focus trap</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="demo-footer">
|
||||
<p>
|
||||
<ui-screen-reader-only>End of accessibility demo.</ui-screen-reader-only>
|
||||
This demo showcases WCAG 2.1 Level AA compliant accessibility features using semantic design tokens.
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -0,0 +1,476 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.accessibility-demo {
|
||||
padding: $semantic-spacing-layout-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: $semantic-spacing-component-lg * 2;
|
||||
|
||||
h1 {
|
||||
color: #1a1a1a;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-component-lg * 2;
|
||||
padding: $semantic-spacing-component-md $semantic-spacing-component-md;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
|
||||
h2 {
|
||||
color: #1a1a1a;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: $semantic-spacing-component-md / 2;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #1a1a1a;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: $semantic-spacing-component-lg 0 $semantic-spacing-component-lg / 2 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Preferences Display
|
||||
.preferences-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e0e0e0;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
// Demo Controls
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.demo-input {
|
||||
padding: $semantic-spacing-component-md $semantic-spacing-component-md;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
color: #1a1a1a;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.15s ease-out;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #007bff;
|
||||
outline-offset: 2px;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-select {
|
||||
@extend .demo-input;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.demo-textarea {
|
||||
@extend .demo-input;
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
// Focus Demo
|
||||
.focus-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
// Focus Trap Demo
|
||||
.trap-controls {
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.focus-trap-container {
|
||||
padding: $semantic-spacing-component-md * 1.5;
|
||||
border: 2px dashed #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
transition: all 0.3s ease-out;
|
||||
|
||||
&.trap-enabled {
|
||||
border-color: #007bff;
|
||||
background: #f0f8ff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation Demo
|
||||
.navigation-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.navigation-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
li {
|
||||
padding: $semantic-spacing-component-md $semantic-spacing-component-md;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease-out;
|
||||
color: #1a1a1a;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #007bff;
|
||||
outline-offset: -2px;
|
||||
background: #f0f8ff;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
|
||||
.grid-item {
|
||||
padding: $semantic-spacing-component-md;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease-out;
|
||||
font-size: 0.875rem;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #dee2e6;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #007bff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
background: #f8f9fa;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modal Demo
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
min-width: 400px;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
|
||||
.modal-header {
|
||||
padding: $semantic-spacing-component-md $semantic-spacing-component-md;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease-out;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #007bff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: $semantic-spacing-component-md * 1.5;
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-input {
|
||||
width: 100%;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: $semantic-spacing-component-md $semantic-spacing-component-md;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
// Screen Reader Demo
|
||||
.sr-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
margin: $semantic-spacing-component-lg / 2 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
// Keyboard Shortcuts
|
||||
.shortcuts-info {
|
||||
background: #f5f5f5;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.shortcuts-list {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
gap: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
margin: 0;
|
||||
|
||||
dt {
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.875rem;
|
||||
background: #e9ecef;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
.demo-footer {
|
||||
text-align: center;
|
||||
padding: $semantic-spacing-component-md * 2;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
margin-top: $semantic-spacing-component-lg * 2;
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Animations
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 768px) {
|
||||
.accessibility-demo {
|
||||
padding: $semantic-spacing-component-md / 2;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
padding: $semantic-spacing-component-md / 2;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.navigation-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.shortcuts-list {
|
||||
grid-template-columns: 1fr;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
|
||||
dt {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
min-width: 90vw;
|
||||
margin: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
// High Contrast Mode Support
|
||||
@media (prefers-contrast: high) {
|
||||
.demo-section {
|
||||
border-color: ButtonText;
|
||||
}
|
||||
|
||||
.focus-trap-container.trap-enabled {
|
||||
border-color: Highlight;
|
||||
}
|
||||
|
||||
.navigation-list li:focus {
|
||||
outline-color: Highlight;
|
||||
}
|
||||
|
||||
.grid-item:focus {
|
||||
outline-color: Highlight;
|
||||
}
|
||||
}
|
||||
|
||||
// Reduced Motion Support
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01s !important;
|
||||
transition-duration: 0.01s !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AccessibilityDemoComponent } from './accessibility-demo.component';
|
||||
|
||||
describe('AccessibilityDemoComponent', () => {
|
||||
let component: AccessibilityDemoComponent;
|
||||
let fixture: ComponentFixture<AccessibilityDemoComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AccessibilityDemoComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AccessibilityDemoComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
// Import UI Accessibility components and services
|
||||
import {
|
||||
UiAccessibilityModule,
|
||||
LiveAnnouncerService,
|
||||
FocusMonitorService,
|
||||
KeyboardManagerService,
|
||||
HighContrastService,
|
||||
A11yConfigService
|
||||
} from 'ui-accessibility';
|
||||
|
||||
// Import UI Essentials components
|
||||
import { ButtonComponent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accessibility-demo',
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
UiAccessibilityModule,
|
||||
ButtonComponent
|
||||
],
|
||||
templateUrl: './accessibility-demo.component.html',
|
||||
styleUrl: './accessibility-demo.component.scss'
|
||||
})
|
||||
export class AccessibilityDemoComponent implements OnInit, OnDestroy {
|
||||
private liveAnnouncer = inject(LiveAnnouncerService);
|
||||
private focusMonitor = inject(FocusMonitorService);
|
||||
private keyboardManager = inject(KeyboardManagerService);
|
||||
private highContrast = inject(HighContrastService);
|
||||
private a11yConfig = inject(A11yConfigService);
|
||||
|
||||
@ViewChild('trapContainer') trapContainer!: ElementRef;
|
||||
@ViewChild('monitoredInput') monitoredInput!: ElementRef;
|
||||
|
||||
// Demo state
|
||||
trapEnabled = false;
|
||||
showModal = false;
|
||||
currentPreferences: any = {};
|
||||
focusOrigin = '';
|
||||
announcementText = 'Hello, this is a test announcement!';
|
||||
selectedPoliteness: 'polite' | 'assertive' = 'polite';
|
||||
|
||||
// Navigation demo items
|
||||
navigationItems = [
|
||||
{ id: 1, label: 'Profile Settings', disabled: false },
|
||||
{ id: 2, label: 'Account Details', disabled: false },
|
||||
{ id: 3, label: 'Privacy Controls', disabled: true },
|
||||
{ id: 4, label: 'Notifications', disabled: false },
|
||||
{ id: 5, label: 'Security Options', disabled: false }
|
||||
];
|
||||
|
||||
// Grid navigation items
|
||||
gridItems = Array.from({ length: 12 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
label: `Item ${i + 1}`,
|
||||
disabled: i === 5 // Make item 6 disabled
|
||||
}));
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setupKeyboardShortcuts();
|
||||
this.setupFocusMonitoring();
|
||||
this.getCurrentPreferences();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.keyboardManager.unregisterAll();
|
||||
}
|
||||
|
||||
private setupKeyboardShortcuts(): void {
|
||||
// Global shortcuts
|
||||
this.keyboardManager.registerGlobalShortcut('announce', {
|
||||
key: 'a',
|
||||
ctrlKey: true,
|
||||
shiftKey: true,
|
||||
description: 'Make announcement',
|
||||
handler: () => this.makeAnnouncement()
|
||||
});
|
||||
|
||||
this.keyboardManager.registerGlobalShortcut('toggleModal', {
|
||||
key: 'm',
|
||||
ctrlKey: true,
|
||||
description: 'Toggle modal',
|
||||
handler: () => this.toggleModal()
|
||||
});
|
||||
}
|
||||
|
||||
private setupFocusMonitoring(): void {
|
||||
// Monitor focus on the input field
|
||||
setTimeout(() => {
|
||||
if (this.monitoredInput) {
|
||||
this.focusMonitor.monitor(this.monitoredInput).subscribe(focusEvent => {
|
||||
this.focusOrigin = focusEvent.origin || 'none';
|
||||
if (focusEvent.origin) {
|
||||
this.liveAnnouncer.announce(
|
||||
`Input focused via ${focusEvent.origin}`,
|
||||
'polite'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private getCurrentPreferences(): void {
|
||||
this.currentPreferences = this.highContrast.getCurrentPreferences();
|
||||
}
|
||||
|
||||
// Demo actions
|
||||
makeAnnouncement(): void {
|
||||
this.liveAnnouncer.announce(this.announcementText, this.selectedPoliteness);
|
||||
}
|
||||
|
||||
makeSequenceAnnouncement(): void {
|
||||
const messages = [
|
||||
'Starting process...',
|
||||
'Processing data...',
|
||||
'Validating information...',
|
||||
'Process completed successfully!'
|
||||
];
|
||||
this.liveAnnouncer.announceSequence(messages, 'polite', 1500);
|
||||
}
|
||||
|
||||
toggleFocusTrap(): void {
|
||||
this.trapEnabled = !this.trapEnabled;
|
||||
this.liveAnnouncer.announce(
|
||||
`Focus trap ${this.trapEnabled ? 'enabled' : 'disabled'}`,
|
||||
'assertive'
|
||||
);
|
||||
}
|
||||
|
||||
toggleModal(): void {
|
||||
this.showModal = !this.showModal;
|
||||
if (this.showModal) {
|
||||
this.liveAnnouncer.announce('Modal opened', 'assertive');
|
||||
} else {
|
||||
this.liveAnnouncer.announce('Modal closed', 'assertive');
|
||||
}
|
||||
}
|
||||
|
||||
focusViaKeyboard(): void {
|
||||
if (this.monitoredInput) {
|
||||
this.focusMonitor.focusVia(this.monitoredInput, 'keyboard');
|
||||
}
|
||||
}
|
||||
|
||||
focusViaMouse(): void {
|
||||
if (this.monitoredInput) {
|
||||
this.focusMonitor.focusVia(this.monitoredInput, 'mouse');
|
||||
}
|
||||
}
|
||||
|
||||
focusViaProgram(): void {
|
||||
if (this.monitoredInput) {
|
||||
this.focusMonitor.focusVia(this.monitoredInput, 'program');
|
||||
}
|
||||
}
|
||||
|
||||
onNavigationChange(event: any): void {
|
||||
this.liveAnnouncer.announce(
|
||||
`Navigated to ${this.navigationItems[event.nextIndex]?.label}`,
|
||||
'polite'
|
||||
);
|
||||
}
|
||||
|
||||
onGridNavigationChange(event: any): void {
|
||||
this.liveAnnouncer.announce(
|
||||
`Navigated to ${this.gridItems[event.nextIndex]?.label}`,
|
||||
'polite'
|
||||
);
|
||||
}
|
||||
|
||||
announceError(): void {
|
||||
this.liveAnnouncer.announce(
|
||||
'Error: Please fix the required fields before continuing',
|
||||
'assertive'
|
||||
);
|
||||
}
|
||||
|
||||
announceSuccess(): void {
|
||||
this.liveAnnouncer.announce(
|
||||
'Success: Your changes have been saved',
|
||||
'polite'
|
||||
);
|
||||
}
|
||||
|
||||
announceLoading(): void {
|
||||
this.liveAnnouncer.announce(
|
||||
'Loading content, please wait...',
|
||||
'polite'
|
||||
);
|
||||
}
|
||||
|
||||
refreshPreferences(): void {
|
||||
this.getCurrentPreferences();
|
||||
this.liveAnnouncer.announce('Accessibility preferences updated', 'polite');
|
||||
}
|
||||
|
||||
simulateFormValidation(): void {
|
||||
// Simulate a form validation scenario
|
||||
this.liveAnnouncer.announce('Validating form...', 'polite');
|
||||
|
||||
setTimeout(() => {
|
||||
this.liveAnnouncer.announce(
|
||||
'Form validation complete. 2 errors found.',
|
||||
'assertive'
|
||||
);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-lg;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: $semantic-typography-heading-h2-size;
|
||||
margin-bottom: $semantic-spacing-layout-lg;
|
||||
border-bottom: 1px solid $semantic-color-border-subtle;
|
||||
padding-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: $semantic-color-text-secondary;
|
||||
font-size: $semantic-typography-heading-h3-size;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
margin-top: $semantic-spacing-layout-lg;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: $semantic-typography-heading-h4-size;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-xl;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
flex-wrap: wrap;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.demo-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.demo-accordion-container {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
border: 1px solid $semantic-color-border-subtle;
|
||||
|
||||
button {
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
border: 1px solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-easing-standard;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-color: $semantic-color-border-primary;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $semantic-color-primary;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-primary;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-primary;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border: 1px solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
background: $semantic-color-surface-primary;
|
||||
|
||||
h5 {
|
||||
margin: 0 0 $semantic-spacing-component-xs 0;
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.code-example {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: 1px solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
padding: $semantic-spacing-component-md;
|
||||
font-family: $semantic-typography-font-family-mono;
|
||||
font-size: $semantic-typography-font-size-xs;
|
||||
color: $semantic-color-text-primary;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
margin-top: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.sample-content {
|
||||
line-height: $semantic-typography-line-height-relaxed;
|
||||
|
||||
p {
|
||||
margin: 0 0 $semantic-spacing-component-sm 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0 0 $semantic-spacing-component-sm $semantic-spacing-component-lg;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-demo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
color: $semantic-color-primary;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-md;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-column {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-sm;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { AccordionComponent, AccordionItemComponent, AccordionItem, AccordionExpandEvent } from '../../../../../ui-essentials/src/public-api';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-accordion-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, AccordionComponent, AccordionItemComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Accordion Demo</h2>
|
||||
|
||||
<!-- Basic Usage -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Usage</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-column">
|
||||
<div class="demo-item">
|
||||
<h5>Single Expand (Default)</h5>
|
||||
<div class="demo-accordion-container">
|
||||
<ui-accordion>
|
||||
<ui-accordion-item id="basic-1" title="Getting Started">
|
||||
<div class="sample-content">
|
||||
<p>Welcome to our comprehensive guide on getting started with our platform.</p>
|
||||
<p>This section covers the essential steps you need to know to begin your journey.</p>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
<ui-accordion-item id="basic-2" title="Advanced Features">
|
||||
<div class="sample-content">
|
||||
<p>Explore our advanced features designed for power users.</p>
|
||||
<ul>
|
||||
<li>Advanced analytics and reporting</li>
|
||||
<li>Custom integrations and APIs</li>
|
||||
<li>Enterprise-grade security features</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
<ui-accordion-item id="basic-3" title="Troubleshooting">
|
||||
<div class="sample-content">
|
||||
<p>Common issues and their solutions:</p>
|
||||
<ul>
|
||||
<li>Login and authentication problems</li>
|
||||
<li>Performance optimization tips</li>
|
||||
<li>Data import and export issues</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
</ui-accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-column">
|
||||
<div class="demo-item">
|
||||
<h5>Multiple Expand</h5>
|
||||
<div class="demo-accordion-container">
|
||||
<ui-accordion [multiple]="true">
|
||||
<ui-accordion-item id="multi-1" title="Account Settings">
|
||||
<div class="sample-content">
|
||||
<p>Manage your account preferences and personal information.</p>
|
||||
<p>Update your profile, change password, and configure notification settings.</p>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
<ui-accordion-item id="multi-2" title="Privacy Controls">
|
||||
<div class="sample-content">
|
||||
<p>Control how your data is used and shared:</p>
|
||||
<ul>
|
||||
<li>Data sharing preferences</li>
|
||||
<li>Visibility settings</li>
|
||||
<li>Third-party integrations</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
<ui-accordion-item id="multi-3" title="Notifications">
|
||||
<div class="sample-content">
|
||||
<p>Customize your notification preferences to stay informed about what matters most to you.</p>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
</ui-accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-column">
|
||||
<div class="demo-item">
|
||||
<h5>Size: {{ size | titlecase }}</h5>
|
||||
<div class="demo-accordion-container">
|
||||
<ui-accordion [size]="size">
|
||||
<ui-accordion-item [id]="'size-' + size + '-1'" title="Sample Header">
|
||||
<div class="sample-content">
|
||||
<p>This is {{ size }} sized accordion content. The padding and font sizes adjust based on the size variant.</p>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
<ui-accordion-item [id]="'size-' + size + '-2'" title="Another Section">
|
||||
<div class="sample-content">
|
||||
<p>More content to demonstrate the {{ size }} size variant styling.</p>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
</ui-accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Style Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-column">
|
||||
<div class="demo-item">
|
||||
<h5>{{ variant | titlecase }}</h5>
|
||||
<div class="demo-accordion-container">
|
||||
<ui-accordion [variant]="variant">
|
||||
<ui-accordion-item [id]="'variant-' + variant + '-1'" title="Design System">
|
||||
<div class="sample-content">
|
||||
<p>This {{ variant }} variant shows different visual styling approaches for the accordion component.</p>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
<ui-accordion-item [id]="'variant-' + variant + '-2'" title="Component Library">
|
||||
<div class="sample-content">
|
||||
<p>Each variant provides a different visual weight and emphasis level.</p>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
</ui-accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Programmatic API -->
|
||||
<section class="demo-section">
|
||||
<h3>Programmatic Control</h3>
|
||||
<div class="demo-item">
|
||||
<div class="demo-controls">
|
||||
<button (click)="expandItem('api-1')" class="button--primary">Expand Item 1</button>
|
||||
<button (click)="expandItem('api-2')" class="button--primary">Expand Item 2</button>
|
||||
<button (click)="expandItem('api-3')" class="button--primary">Expand Item 3</button>
|
||||
<button (click)="expandAll()">Expand All</button>
|
||||
<button (click)="collapseAll()">Collapse All</button>
|
||||
</div>
|
||||
|
||||
<div class="demo-accordion-container">
|
||||
<ui-accordion
|
||||
#programmaticAccordion
|
||||
[multiple]="true"
|
||||
[items]="programmaticItems"
|
||||
(itemToggle)="handleItemToggle($event)"
|
||||
(expandedItemsChange)="handleExpandedItemsChange($event)">
|
||||
</ui-accordion>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
Current expanded items: {{ expandedItemIds | json }}
|
||||
Last toggled: {{ lastToggledItem | json }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-column">
|
||||
<div class="demo-item">
|
||||
<h5>Disabled Accordion</h5>
|
||||
<div class="demo-accordion-container">
|
||||
<ui-accordion [disabled]="true">
|
||||
<ui-accordion-item id="disabled-1" title="Disabled Section">
|
||||
<div class="sample-content">
|
||||
<p>This entire accordion is disabled and cannot be interacted with.</p>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
<ui-accordion-item id="disabled-2" title="Another Disabled Section">
|
||||
<div class="sample-content">
|
||||
<p>All items in this accordion are disabled.</p>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
</ui-accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-column">
|
||||
<div class="demo-item">
|
||||
<h5>Individual Disabled Items</h5>
|
||||
<div class="demo-accordion-container">
|
||||
<ui-accordion>
|
||||
<ui-accordion-item id="mixed-1" title="Active Section">
|
||||
<div class="sample-content">
|
||||
<p>This section is active and can be expanded/collapsed normally.</p>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
<ui-accordion-item id="mixed-2" title="Disabled Section" [disabled]="true">
|
||||
<div class="sample-content">
|
||||
<p>This individual section is disabled.</p>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
<ui-accordion-item id="mixed-3" title="Another Active Section">
|
||||
<div class="sample-content">
|
||||
<p>This section is also active and functional.</p>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
</ui-accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Custom Content -->
|
||||
<section class="demo-section">
|
||||
<h3>Custom Content</h3>
|
||||
<div class="demo-item">
|
||||
<div class="demo-accordion-container">
|
||||
<ui-accordion>
|
||||
<ui-accordion-item id="custom-1" title="">
|
||||
<div slot="header" class="icon-demo">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 1l2.5 5h5.5l-4.5 3.5 1.5 5.5L8 12l-4.5 3.5L5 10 0.5 6h5.5L8 1z"/>
|
||||
</svg>
|
||||
Custom Header with Icon
|
||||
</div>
|
||||
<div class="sample-content">
|
||||
<p>This accordion item uses custom header content with an icon.</p>
|
||||
<p>The header slot allows for complete customization of the trigger area.</p>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
|
||||
<ui-accordion-item id="custom-2" title="">
|
||||
<div slot="header">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
|
||||
<strong>Rich Header Content</strong>
|
||||
<span style="background: #e3f2fd; color: #1976d2; padding: 2px 8px; border-radius: 12px; font-size: 12px;">
|
||||
New
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sample-content">
|
||||
<p>Complex header layouts are possible with custom slot content.</p>
|
||||
<p>This example shows a header with a badge indicator.</p>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
</ui-accordion>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Example</h3>
|
||||
<div class="demo-item">
|
||||
<h4>Event Handling</h4>
|
||||
<div class="demo-accordion-container">
|
||||
<ui-accordion (itemToggle)="handleInteractiveToggle($event)">
|
||||
<ui-accordion-item id="interactive-1" title="Click to Track Events">
|
||||
<div class="sample-content">
|
||||
<p>This accordion tracks all toggle events. Check the event log below.</p>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
<ui-accordion-item id="interactive-2" title="Another Trackable Item">
|
||||
<div class="sample-content">
|
||||
<p>Each interaction will be logged with detailed event information.</p>
|
||||
</div>
|
||||
</ui-accordion-item>
|
||||
</ui-accordion>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
Event Log ({{ eventLog.length }} events):
|
||||
{{ eventLog.length > 0 ? (eventLog | json) : 'No events yet...' }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './accordion-demo.component.scss'
|
||||
})
|
||||
export class AccordionDemoComponent {
|
||||
@ViewChild('programmaticAccordion') programmaticAccordion!: AccordionComponent;
|
||||
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
variants = ['elevated', 'outlined', 'filled'] as const;
|
||||
|
||||
programmaticItems: AccordionItem[] = [
|
||||
{
|
||||
id: 'api-1',
|
||||
title: 'API Documentation',
|
||||
content: 'Complete API reference with examples and best practices for integration.',
|
||||
expanded: false
|
||||
},
|
||||
{
|
||||
id: 'api-2',
|
||||
title: 'SDK Downloads',
|
||||
content: 'Download our SDKs for various programming languages and frameworks.',
|
||||
expanded: false
|
||||
},
|
||||
{
|
||||
id: 'api-3',
|
||||
title: 'Code Examples',
|
||||
content: 'Real-world code examples and implementation patterns.',
|
||||
expanded: false
|
||||
}
|
||||
];
|
||||
|
||||
expandedItemIds: string[] = [];
|
||||
lastToggledItem: AccordionExpandEvent | null = null;
|
||||
eventLog: AccordionExpandEvent[] = [];
|
||||
|
||||
handleItemToggle(event: AccordionExpandEvent): void {
|
||||
this.lastToggledItem = event;
|
||||
console.log('Accordion item toggled:', event);
|
||||
}
|
||||
|
||||
handleExpandedItemsChange(expandedIds: string[]): void {
|
||||
this.expandedItemIds = expandedIds;
|
||||
console.log('Expanded items changed:', expandedIds);
|
||||
}
|
||||
|
||||
handleInteractiveToggle(event: AccordionExpandEvent): void {
|
||||
this.eventLog.unshift({
|
||||
...event,
|
||||
// Add timestamp for demonstration
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
} as any);
|
||||
|
||||
// Keep only last 5 events
|
||||
if (this.eventLog.length > 5) {
|
||||
this.eventLog = this.eventLog.slice(0, 5);
|
||||
}
|
||||
}
|
||||
|
||||
expandItem(itemId: string): void {
|
||||
this.programmaticAccordion?.expandItem(itemId);
|
||||
}
|
||||
|
||||
expandAll(): void {
|
||||
this.programmaticAccordion?.expandAll();
|
||||
}
|
||||
|
||||
collapseAll(): void {
|
||||
this.programmaticAccordion?.collapseAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
|
||||
h2, h3, h4 {
|
||||
margin: 0 0 $semantic-spacing-component-lg 0;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
h2 {
|
||||
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);
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
padding-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
h3 {
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-variant-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-message {
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border: none;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: $semantic-shadow-button-rest;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: $semantic-spacing-component-md;
|
||||
|
||||
li {
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
|
||||
strong {
|
||||
color: $semantic-color-text-primary;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
|
||||
.demo-column {
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
|
||||
h2 {
|
||||
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);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { AlertComponent } from "../../../../../ui-essentials/src/public-api";
|
||||
|
||||
@Component({
|
||||
selector: 'ui-alert-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FontAwesomeModule, AlertComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Alert Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-column">
|
||||
@for (size of sizes; track size) {
|
||||
<ui-alert [size]="size" title="{{size | titlecase}} Alert">
|
||||
This is a {{ size }} size alert component demonstration.
|
||||
</ui-alert>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Types -->
|
||||
<section class="demo-section">
|
||||
<h3>Variants</h3>
|
||||
<div class="demo-column">
|
||||
<ui-alert variant="primary" title="Primary Alert">
|
||||
This is a primary alert for general information or neutral messages.
|
||||
</ui-alert>
|
||||
|
||||
<ui-alert variant="success" title="Success Alert">
|
||||
Your action was completed successfully! Everything worked as expected.
|
||||
</ui-alert>
|
||||
|
||||
<ui-alert variant="warning" title="Warning Alert">
|
||||
Please review this information carefully before proceeding.
|
||||
</ui-alert>
|
||||
|
||||
<ui-alert variant="danger" title="Error Alert">
|
||||
Something went wrong. Please check your input and try again.
|
||||
</ui-alert>
|
||||
|
||||
<ui-alert variant="info" title="Information Alert">
|
||||
Here's some helpful information you might want to know.
|
||||
</ui-alert>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Without Icons -->
|
||||
<section class="demo-section">
|
||||
<h3>Without Icons</h3>
|
||||
<div class="demo-column">
|
||||
<ui-alert variant="success" title="Success" [showIcon]="false">
|
||||
This alert doesn't show an icon for a cleaner look.
|
||||
</ui-alert>
|
||||
|
||||
<ui-alert variant="warning" [showIcon]="false">
|
||||
This warning alert has no title and no icon.
|
||||
</ui-alert>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Dismissible Alerts -->
|
||||
<section class="demo-section">
|
||||
<h3>Dismissible Alerts</h3>
|
||||
<div class="demo-column">
|
||||
@for (dismissibleAlert of dismissibleAlerts; track dismissibleAlert.id) {
|
||||
<ui-alert
|
||||
[variant]="dismissibleAlert.variant"
|
||||
[title]="dismissibleAlert.title"
|
||||
[dismissible]="true"
|
||||
(dismissed)="dismissAlert(dismissibleAlert.id)">
|
||||
{{ dismissibleAlert.message }}
|
||||
</ui-alert>
|
||||
}
|
||||
|
||||
@if (dismissibleAlerts.length === 0) {
|
||||
<p class="demo-message">All dismissible alerts have been dismissed. <button type="button" (click)="resetDismissibleAlerts()" class="demo-button">Reset Alerts</button></p>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- No Title Alerts -->
|
||||
<section class="demo-section">
|
||||
<h3>Without Titles</h3>
|
||||
<div class="demo-column">
|
||||
<ui-alert variant="info">
|
||||
This is an informational alert without a title. The message stands on its own.
|
||||
</ui-alert>
|
||||
|
||||
<ui-alert variant="danger" [dismissible]="true">
|
||||
This is a dismissible error alert without a title.
|
||||
</ui-alert>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Bold Titles -->
|
||||
<section class="demo-section">
|
||||
<h3>Bold Titles</h3>
|
||||
<div class="demo-column">
|
||||
<ui-alert variant="primary" title="Important Notice" [boldTitle]="true">
|
||||
This alert has a bold title to emphasize importance.
|
||||
</ui-alert>
|
||||
|
||||
<ui-alert variant="warning" title="Critical Warning" [boldTitle]="true" [dismissible]="true">
|
||||
Bold titles help draw attention to critical messages.
|
||||
</ui-alert>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Examples</h3>
|
||||
<div class="demo-column">
|
||||
<ui-alert
|
||||
variant="success"
|
||||
title="Form Submitted Successfully"
|
||||
[dismissible]="true"
|
||||
(dismissed)="handleAlertDismissed('success')">
|
||||
Your form has been submitted and will be processed within 24 hours.
|
||||
</ui-alert>
|
||||
|
||||
<ui-alert
|
||||
variant="info"
|
||||
title="Newsletter Subscription"
|
||||
[dismissible]="true"
|
||||
(dismissed)="handleAlertDismissed('newsletter')">
|
||||
Don't forget to confirm your email address to complete your subscription.
|
||||
</ui-alert>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- All Combinations -->
|
||||
<section class="demo-section">
|
||||
<h3>Size & Variant Combinations</h3>
|
||||
<div class="demo-grid">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-variant-group">
|
||||
<h4>{{ variant | titlecase }}</h4>
|
||||
@for (size of sizes; track size) {
|
||||
<ui-alert
|
||||
[variant]="variant"
|
||||
[size]="size"
|
||||
[title]="size | titlecase"
|
||||
[dismissible]="size === 'lg'">
|
||||
{{ variant | titlecase }} {{ size }} alert
|
||||
</ui-alert>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Accessibility Information -->
|
||||
<section class="demo-section">
|
||||
<h3>Accessibility Features</h3>
|
||||
<div class="demo-info">
|
||||
<ul>
|
||||
<li><strong>Screen Reader Support:</strong> Proper ARIA labels and live regions</li>
|
||||
<li><strong>Keyboard Navigation:</strong> Dismissible alerts can be closed with Enter or Space</li>
|
||||
<li><strong>Focus Management:</strong> Clear focus indicators on dismiss button</li>
|
||||
<li><strong>Semantic HTML:</strong> Proper heading hierarchy and content structure</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './alert-demo.component.scss'
|
||||
})
|
||||
export class AlertDemoComponent {
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
variants = ['primary', 'success', 'warning', 'danger', 'info'] as const;
|
||||
|
||||
dismissibleAlerts = [
|
||||
{
|
||||
id: 1,
|
||||
variant: 'success' as const,
|
||||
title: 'Task Completed',
|
||||
message: 'Your task has been completed successfully and saved to your dashboard.'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
variant: 'warning' as const,
|
||||
title: 'Session Expiring',
|
||||
message: 'Your session will expire in 5 minutes. Please save your work.'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
variant: 'info' as const,
|
||||
title: 'New Feature Available',
|
||||
message: 'Check out our new dashboard analytics feature in the sidebar menu.'
|
||||
}
|
||||
];
|
||||
|
||||
private originalDismissibleAlerts = [...this.dismissibleAlerts];
|
||||
|
||||
dismissAlert(id: number): void {
|
||||
this.dismissibleAlerts = this.dismissibleAlerts.filter(alert => alert.id !== id);
|
||||
console.log(`Alert with ID ${id} dismissed`);
|
||||
}
|
||||
|
||||
resetDismissibleAlerts(): void {
|
||||
this.dismissibleAlerts = [...this.originalDismissibleAlerts];
|
||||
}
|
||||
|
||||
handleAlertDismissed(type: string): void {
|
||||
console.log(`${type} alert dismissed`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { Component, ElementRef, ViewChild } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { UiAnimationsService, AnimateDirective } from 'ui-animations';
|
||||
|
||||
@Component({
|
||||
selector: 'app-animations-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, AnimateDirective],
|
||||
template: `
|
||||
<div class="animations-demo">
|
||||
<h2>UI Animations Demo</h2>
|
||||
|
||||
<!-- Entrance Animations -->
|
||||
<section class="demo-section">
|
||||
<h3>Entrance Animations</h3>
|
||||
<div class="animation-grid">
|
||||
<div class="demo-card animate-fade-in">
|
||||
<h4>Fade In</h4>
|
||||
<p>Basic fade in animation</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-card animate-fade-in-up animation-delay-200">
|
||||
<h4>Fade In Up</h4>
|
||||
<p>Fade in with upward movement</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-card animate-slide-in-up animation-delay-300">
|
||||
<h4>Slide In Up</h4>
|
||||
<p>Slide in from bottom</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-card animate-zoom-in animation-delay-500">
|
||||
<h4>Zoom In</h4>
|
||||
<p>Scale up animation</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Emphasis Animations -->
|
||||
<section class="demo-section">
|
||||
<h3>Emphasis Animations</h3>
|
||||
<div class="animation-grid">
|
||||
<button class="demo-button" (click)="animateElement('bounce')">
|
||||
Bounce
|
||||
</button>
|
||||
|
||||
<button class="demo-button" (click)="animateElement('shake')">
|
||||
Shake
|
||||
</button>
|
||||
|
||||
<button class="demo-button" (click)="animateElement('pulse')">
|
||||
Pulse
|
||||
</button>
|
||||
|
||||
<button class="demo-button" (click)="animateElement('wobble')">
|
||||
Wobble
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div #animationTarget class="demo-target">
|
||||
Click buttons above to animate me!
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Directive Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Animation Directive Examples</h3>
|
||||
<div class="animation-grid">
|
||||
<div class="demo-card" uiAnimate="animate-fade-in-up" [animationTrigger]="'hover'">
|
||||
<h4>Hover to Animate</h4>
|
||||
<p>Uses directive with hover trigger</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-card" uiAnimate="animate-bounce" [animationTrigger]="'click'" [animationOnce]="true">
|
||||
<h4>Click to Animate</h4>
|
||||
<p>Uses directive with click trigger</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Service Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Animation Service Examples</h3>
|
||||
<div class="service-controls">
|
||||
<button (click)="serviceAnimate('animate-tada')">Tada</button>
|
||||
<button (click)="serviceAnimate('animate-jello')">Jello</button>
|
||||
<button (click)="serviceAnimate('animate-heartbeat')">Heartbeat</button>
|
||||
<button (click)="serviceAnimate('animate-rubber-band')">Rubber Band</button>
|
||||
</div>
|
||||
|
||||
<div #serviceTarget class="demo-target service-target">
|
||||
Animation Service Target
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.animations-demo {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.animation-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-target {
|
||||
padding: 2rem;
|
||||
border: 2px dashed #007bff;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
background: #f8f9fa;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.service-target {
|
||||
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.service-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.animation-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.service-controls {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class AnimationsDemoComponent {
|
||||
@ViewChild('animationTarget', { static: true }) animationTarget!: ElementRef<HTMLElement>;
|
||||
@ViewChild('serviceTarget', { static: true }) serviceTarget!: ElementRef<HTMLElement>;
|
||||
|
||||
constructor(private animationService: UiAnimationsService) {}
|
||||
|
||||
animateElement(animationType: string) {
|
||||
const element = this.animationTarget.nativeElement;
|
||||
const animationClass = `animate-${animationType}`;
|
||||
|
||||
// Remove any existing animation classes first
|
||||
element.className = element.className.replace(/animate-\w+/g, '');
|
||||
|
||||
// Add animation class
|
||||
this.animationService.animateOnce(element, animationClass);
|
||||
}
|
||||
|
||||
serviceAnimate(animationClass: string) {
|
||||
const element = this.serviceTarget.nativeElement;
|
||||
|
||||
// Remove any existing animation classes first
|
||||
element.className = element.className.replace(/animate-\w+/g, '');
|
||||
|
||||
// Add animation class with service
|
||||
this.animationService.animateOnce(element, animationClass);
|
||||
}
|
||||
}
|
||||
@@ -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/appbar.component';
|
||||
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,126 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
|
||||
h3 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
button {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border: none;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
cursor: pointer;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: white;
|
||||
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);
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
small {
|
||||
font-family: map-get($semantic-typography-caption, font-family);
|
||||
font-size: map-get($semantic-typography-caption, font-size);
|
||||
font-weight: map-get($semantic-typography-caption, font-weight);
|
||||
line-height: map-get($semantic-typography-caption, line-height);
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { AspectRatioComponent } from '../../../../../ui-essentials/src/lib/components/layout/aspect-ratio';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-aspect-ratio-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, AspectRatioComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Aspect Ratio Demo</h2>
|
||||
<p>Components for maintaining consistent aspect ratios across responsive content.</p>
|
||||
|
||||
<!-- Common Presets -->
|
||||
<section class="demo-section">
|
||||
<h3>Aspect Ratio Presets</h3>
|
||||
<div class="demo-grid">
|
||||
@for (preset of presets; track preset.ratio) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ preset.label }}</h4>
|
||||
<ui-aspect-ratio [ratio]="preset.ratio">
|
||||
<div class="demo-content" [style.background]="getRandomColor()">
|
||||
<span>{{ preset.ratio }} ({{ preset.description }})</span>
|
||||
</div>
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Custom Ratios -->
|
||||
<section class="demo-section">
|
||||
<h3>Custom Aspect Ratios</h3>
|
||||
<div class="demo-grid">
|
||||
@for (custom of customRatios; track custom.ratio) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ custom.label }}</h4>
|
||||
<ui-aspect-ratio [customRatio]="custom.ratio">
|
||||
<div class="demo-content" [style.background]="getRandomColor()">
|
||||
<span>{{ custom.ratio }}</span>
|
||||
</div>
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-item" style="flex: 1;">
|
||||
<h4>Size: {{ size }}</h4>
|
||||
<ui-aspect-ratio ratio="video" [size]="size">
|
||||
<div class="demo-content" style="background: linear-gradient(45deg, #667eea, #764ba2)">
|
||||
<span>{{ size }} size</span>
|
||||
</div>
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Style Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Style Variants</h3>
|
||||
<div class="demo-grid">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ variant | titlecase }}</h4>
|
||||
<ui-aspect-ratio
|
||||
ratio="photo"
|
||||
[variant]="variant"
|
||||
(clicked)="variant === 'interactive' ? handleInteractiveClick($event) : null">
|
||||
<div class="demo-content" [style.background]="getRandomColor()">
|
||||
<span>{{ variant }} variant</span>
|
||||
@if (variant === 'interactive') {
|
||||
<small>Click me!</small>
|
||||
}
|
||||
</div>
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- With Images -->
|
||||
<section class="demo-section">
|
||||
<h3>Image Examples</h3>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>Square Image</h4>
|
||||
<ui-aspect-ratio ratio="square" size="lg">
|
||||
<img
|
||||
src="https://picsum.photos/400/400"
|
||||
alt="Square demo image"
|
||||
style="object-fit: cover; width: 100%; height: 100%;">
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Video Aspect</h4>
|
||||
<ui-aspect-ratio ratio="video" variant="elevated">
|
||||
<img
|
||||
src="https://picsum.photos/800/450"
|
||||
alt="Video aspect demo image"
|
||||
style="object-fit: cover; width: 100%; height: 100%;">
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Portrait Image</h4>
|
||||
<ui-aspect-ratio ratio="portrait" variant="bordered">
|
||||
<img
|
||||
src="https://picsum.photos/400/533"
|
||||
alt="Portrait demo image"
|
||||
style="object-fit: cover; width: 100%; height: 100%;">
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Loading State -->
|
||||
<section class="demo-section">
|
||||
<h3>Loading State</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item" style="max-width: 300px;">
|
||||
<h4>Loading</h4>
|
||||
<ui-aspect-ratio ratio="video" [loading]="true">
|
||||
<!-- Content is hidden when loading -->
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
|
||||
<div class="demo-item" style="max-width: 300px;">
|
||||
<button (click)="toggleLoading()">
|
||||
Toggle Loading: {{ isLoading ? 'ON' : 'OFF' }}
|
||||
</button>
|
||||
<ui-aspect-ratio ratio="square" [loading]="isLoading">
|
||||
<div class="demo-content" style="background: linear-gradient(135deg, #ff6b6b, #4ecdc4)">
|
||||
<span>Dynamic Loading</span>
|
||||
</div>
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Centered Content -->
|
||||
<section class="demo-section">
|
||||
<h3>Content Centering</h3>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<h4>Fill Container</h4>
|
||||
<ui-aspect-ratio ratio="video" variant="bordered">
|
||||
<div class="demo-content" style="background: linear-gradient(45deg, #fa709a, #fee140)">
|
||||
<span>Fills entire container</span>
|
||||
</div>
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>Centered Content</h4>
|
||||
<ui-aspect-ratio ratio="video" variant="bordered" [centerContent]="true">
|
||||
<div style="background: #6c5ce7; color: white; padding: 20px; border-radius: 8px; text-align: center;">
|
||||
<span>I'm centered!</span>
|
||||
</div>
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Examples</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-item">
|
||||
<h4>Click Counter</h4>
|
||||
<ui-aspect-ratio
|
||||
ratio="square"
|
||||
variant="interactive"
|
||||
size="lg"
|
||||
(clicked)="incrementCounter()">
|
||||
<div class="demo-content" style="background: linear-gradient(135deg, #667eea, #764ba2); cursor: pointer;">
|
||||
<div style="text-align: center; color: white;">
|
||||
<div style="font-size: 2rem; font-weight: bold;">{{ clickCount }}</div>
|
||||
<div>Click to increment</div>
|
||||
</div>
|
||||
</div>
|
||||
</ui-aspect-ratio>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './aspect-ratio-demo.component.scss'
|
||||
})
|
||||
export class AspectRatioDemoComponent {
|
||||
presets = [
|
||||
{ ratio: 'square' as const, label: 'Square', description: '1:1' },
|
||||
{ ratio: 'video' as const, label: 'Video', description: '16:9' },
|
||||
{ ratio: 'cinema' as const, label: 'Cinema', description: '21:9' },
|
||||
{ ratio: 'photo' as const, label: 'Photo', description: '3:2' },
|
||||
{ ratio: 'portrait' as const, label: 'Portrait', description: '3:4' },
|
||||
{ ratio: 'golden' as const, label: 'Golden', description: '1.618:1' }
|
||||
];
|
||||
|
||||
customRatios = [
|
||||
{ ratio: '4/3', label: 'Custom 4:3' },
|
||||
{ ratio: '75%', label: 'Custom 75%' },
|
||||
{ ratio: '1.33', label: 'Custom 1.33' },
|
||||
{ ratio: '125%', label: 'Custom 125%' }
|
||||
];
|
||||
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
variants = ['default', 'elevated', 'bordered', 'interactive'] as const;
|
||||
|
||||
clickCount = 0;
|
||||
isLoading = false;
|
||||
|
||||
private colors = [
|
||||
'linear-gradient(45deg, #667eea, #764ba2)',
|
||||
'linear-gradient(135deg, #f093fb, #f5576c)',
|
||||
'linear-gradient(45deg, #4facfe, #00f2fe)',
|
||||
'linear-gradient(135deg, #43e97b, #38f9d7)',
|
||||
'linear-gradient(45deg, #fa709a, #fee140)',
|
||||
'linear-gradient(135deg, #a8edea, #fed6e3)',
|
||||
'linear-gradient(45deg, #ffecd2, #fcb69f)',
|
||||
'linear-gradient(135deg, #ff9a9e, #fecfef)'
|
||||
];
|
||||
|
||||
getRandomColor(): string {
|
||||
return this.colors[Math.floor(Math.random() * this.colors.length)];
|
||||
}
|
||||
|
||||
handleInteractiveClick(event: MouseEvent): void {
|
||||
console.log('Interactive aspect ratio clicked', event);
|
||||
}
|
||||
|
||||
incrementCounter(): void {
|
||||
this.clickCount++;
|
||||
}
|
||||
|
||||
toggleLoading(): void {
|
||||
this.isLoading = !this.isLoading;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
@use '../../../../../ui-design-system/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/autocomplete/autocomplete.component';
|
||||
|
||||
@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/avatar.component';
|
||||
|
||||
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 '../../../../../ui-design-system/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/backdrop/backdrop.component';
|
||||
|
||||
@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,294 @@
|
||||
@use 'ui-design-system/src/styles' as ui;
|
||||
|
||||
.backgrounds-demo {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-900, #1e293b);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.2rem;
|
||||
color: var(--neutral-600, #64748b);
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 4rem;
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary-800, #334155);
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid var(--primary-200, #e2e8f0);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.example-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
padding: 2rem;
|
||||
border-radius: 1rem;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: white;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
opacity: 0.9;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.pattern-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.pattern-demo {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.pattern-name {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
.gradient-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.gradient-demo {
|
||||
aspect-ratio: 16/9;
|
||||
border-radius: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
|
||||
.interactive-demo {
|
||||
padding: 3rem;
|
||||
border-radius: 1rem;
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: white;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
|
||||
position: relative;
|
||||
transition: all 0.5s ease;
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.fullscreen-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.preset-btn {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
color: var(--primary-700, #475569);
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.backgrounds-demo {
|
||||
padding: 1rem;
|
||||
|
||||
.demo-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.example-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.pattern-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
}
|
||||
|
||||
.gradient-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.interactive-demo {
|
||||
padding: 2rem;
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.fullscreen-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.backgrounds-demo {
|
||||
.demo-header h1 {
|
||||
color: var(--neutral-100, #f8fafc);
|
||||
}
|
||||
|
||||
.demo-section h2 {
|
||||
color: var(--neutral-200, #e2e8f0);
|
||||
border-bottom-color: var(--primary-700, #475569);
|
||||
}
|
||||
|
||||
.preset-btn {
|
||||
color: var(--neutral-200, #e2e8f0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// High contrast mode
|
||||
@media (prefers-contrast: high) {
|
||||
.backgrounds-demo {
|
||||
.demo-card,
|
||||
.pattern-demo,
|
||||
.gradient-demo,
|
||||
.interactive-demo {
|
||||
border: 2px solid rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reduced motion
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.backgrounds-demo {
|
||||
* {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
BackgroundDirective,
|
||||
SolidBackgroundComponent,
|
||||
GradientBackgroundComponent,
|
||||
PatternBackgroundComponent,
|
||||
ImageBackgroundComponent,
|
||||
BackgroundService,
|
||||
SolidBackgroundConfig,
|
||||
LinearGradientConfig,
|
||||
PatternConfig,
|
||||
ImageBackgroundConfig
|
||||
} from 'ui-backgrounds';
|
||||
|
||||
@Component({
|
||||
selector: 'app-backgrounds-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
BackgroundDirective,
|
||||
SolidBackgroundComponent,
|
||||
GradientBackgroundComponent,
|
||||
PatternBackgroundComponent,
|
||||
ImageBackgroundComponent
|
||||
],
|
||||
template: `
|
||||
<div class="backgrounds-demo">
|
||||
<div class="demo-header">
|
||||
<h1>UI Backgrounds Demo</h1>
|
||||
<p>Explore different background types and configurations</p>
|
||||
</div>
|
||||
|
||||
<!-- Directive Examples -->
|
||||
<section class="demo-section">
|
||||
<h2>Background Directive Examples</h2>
|
||||
|
||||
<div class="example-grid">
|
||||
<div class="demo-card" [uiBackground]="solidConfig()">
|
||||
<h3>Solid Background</h3>
|
||||
<p>Using the background directive with solid color</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-card" [uiBackground]="gradientConfig()">
|
||||
<h3>Linear Gradient</h3>
|
||||
<p>Beautiful gradient backgrounds</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-card" [uiBackground]="patternConfig()">
|
||||
<h3>Pattern Background</h3>
|
||||
<p>Geometric patterns</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Component Examples -->
|
||||
<section class="demo-section">
|
||||
<h2>Background Components</h2>
|
||||
|
||||
<div class="example-grid">
|
||||
<ui-solid-background color="#4ade80" class="demo-card">
|
||||
<h3>Solid Component</h3>
|
||||
<p>Green background using component</p>
|
||||
</ui-solid-background>
|
||||
|
||||
<ui-gradient-background
|
||||
type="linear"
|
||||
[colors]="['#8b5cf6', '#06b6d4']"
|
||||
direction="45deg"
|
||||
class="demo-card">
|
||||
<h3>Gradient Component</h3>
|
||||
<p>Purple to cyan gradient</p>
|
||||
</ui-gradient-background>
|
||||
|
||||
<ui-pattern-background
|
||||
pattern="dots"
|
||||
primaryColor="#f59e0b"
|
||||
secondaryColor="transparent"
|
||||
[size]="30"
|
||||
[opacity]="0.7"
|
||||
class="demo-card">
|
||||
<h3>Pattern Component</h3>
|
||||
<p>Orange dots pattern</p>
|
||||
</ui-pattern-background>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pattern Showcase -->
|
||||
<section class="demo-section">
|
||||
<h2>Pattern Showcase</h2>
|
||||
|
||||
<div class="pattern-grid">
|
||||
<div
|
||||
*ngFor="let pattern of patterns"
|
||||
class="pattern-demo"
|
||||
[uiBackground]="getPatternConfig(pattern)">
|
||||
<span class="pattern-name">{{ pattern }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gradient Types -->
|
||||
<section class="demo-section">
|
||||
<h2>Gradient Types</h2>
|
||||
|
||||
<div class="gradient-grid">
|
||||
<ui-gradient-background
|
||||
type="linear"
|
||||
[colors]="['#ff6b6b', '#4ecdc4', '#45b7d1']"
|
||||
direction="to right"
|
||||
class="gradient-demo">
|
||||
<span>Linear Gradient</span>
|
||||
</ui-gradient-background>
|
||||
|
||||
<ui-gradient-background
|
||||
type="radial"
|
||||
[colors]="['#667eea', '#764ba2']"
|
||||
shape="circle"
|
||||
class="gradient-demo">
|
||||
<span>Radial Gradient</span>
|
||||
</ui-gradient-background>
|
||||
|
||||
<ui-gradient-background
|
||||
type="conic"
|
||||
[colors]="['#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', '#ff6b6b']"
|
||||
class="gradient-demo">
|
||||
<span>Conic Gradient</span>
|
||||
</ui-gradient-background>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Controls -->
|
||||
<section class="demo-section">
|
||||
<h2>Interactive Background</h2>
|
||||
|
||||
<div class="interactive-demo" [uiBackground]="interactiveConfig()">
|
||||
<div class="controls">
|
||||
<button
|
||||
*ngFor="let config of presetConfigs"
|
||||
(click)="setInteractiveConfig(config)"
|
||||
class="preset-btn">
|
||||
{{ config.name }}
|
||||
</button>
|
||||
</div>
|
||||
<h3>Interactive Background Demo</h3>
|
||||
<p>Click buttons to change the background</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Full Screen Examples -->
|
||||
<section class="demo-section">
|
||||
<h2>Full Screen Backgrounds</h2>
|
||||
|
||||
<div class="fullscreen-controls">
|
||||
<button (click)="toggleFullScreenBackground()" class="action-btn">
|
||||
{{ hasFullScreenBg() ? 'Remove' : 'Add' }} Full Screen Background
|
||||
</button>
|
||||
<button
|
||||
*ngFor="let bg of fullScreenPresets"
|
||||
(click)="setFullScreenBackground(bg)"
|
||||
class="preset-btn">
|
||||
{{ bg.name }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './backgrounds-demo.component.scss'
|
||||
})
|
||||
export class BackgroundsDemoComponent {
|
||||
private readonly backgroundService = signal(new BackgroundService());
|
||||
private fullScreenBackgroundId = signal<string | null>(null);
|
||||
|
||||
// Configuration signals
|
||||
solidConfig = signal<SolidBackgroundConfig>({
|
||||
type: 'solid',
|
||||
color: '#3b82f6'
|
||||
});
|
||||
|
||||
gradientConfig = signal<LinearGradientConfig>({
|
||||
type: 'linear-gradient',
|
||||
direction: 'to bottom right',
|
||||
colors: [
|
||||
{ color: '#ec4899' },
|
||||
{ color: '#8b5cf6' }
|
||||
]
|
||||
});
|
||||
|
||||
patternConfig = signal<PatternConfig>({
|
||||
type: 'pattern',
|
||||
pattern: 'grid',
|
||||
primaryColor: '#1f2937',
|
||||
secondaryColor: 'transparent',
|
||||
size: 20,
|
||||
opacity: 0.3
|
||||
});
|
||||
|
||||
interactiveConfig = signal(this.solidConfig());
|
||||
|
||||
// Pattern list for showcase
|
||||
patterns = [
|
||||
'dots', 'grid', 'stripes', 'diagonal-stripes',
|
||||
'chevron', 'waves', 'circles', 'checkerboard'
|
||||
];
|
||||
|
||||
// Preset configurations
|
||||
presetConfigs = [
|
||||
{
|
||||
name: 'Ocean',
|
||||
config: {
|
||||
type: 'linear-gradient' as const,
|
||||
direction: 'to bottom',
|
||||
colors: [{ color: '#667eea' }, { color: '#764ba2' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Sunset',
|
||||
config: {
|
||||
type: 'linear-gradient' as const,
|
||||
direction: 'to right',
|
||||
colors: [{ color: '#ff6b6b' }, { color: '#feca57' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Forest',
|
||||
config: {
|
||||
type: 'solid' as const,
|
||||
color: '#10b981'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Dots',
|
||||
config: {
|
||||
type: 'pattern' as const,
|
||||
pattern: 'dots' as const,
|
||||
primaryColor: '#6366f1',
|
||||
secondaryColor: 'transparent',
|
||||
size: 25,
|
||||
opacity: 0.6
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Full screen presets
|
||||
fullScreenPresets = [
|
||||
{
|
||||
name: 'Purple Mesh',
|
||||
config: {
|
||||
type: 'linear-gradient' as const,
|
||||
direction: 'to bottom right',
|
||||
colors: [{ color: '#667eea' }, { color: '#764ba2' }, { color: '#f093fb' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Geometric',
|
||||
config: {
|
||||
type: 'pattern' as const,
|
||||
pattern: 'chevron' as const,
|
||||
primaryColor: '#1f2937',
|
||||
secondaryColor: 'transparent',
|
||||
size: 40,
|
||||
opacity: 0.1
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
getPatternConfig(pattern: string): PatternConfig {
|
||||
return {
|
||||
type: 'pattern',
|
||||
pattern: pattern as any,
|
||||
primaryColor: '#6366f1',
|
||||
secondaryColor: 'transparent',
|
||||
size: 25,
|
||||
opacity: 0.4
|
||||
};
|
||||
}
|
||||
|
||||
setInteractiveConfig(preset: any) {
|
||||
this.interactiveConfig.set(preset.config);
|
||||
}
|
||||
|
||||
toggleFullScreenBackground() {
|
||||
const currentId = this.fullScreenBackgroundId();
|
||||
if (currentId) {
|
||||
this.backgroundService().removeBackground(currentId);
|
||||
this.fullScreenBackgroundId.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
setFullScreenBackground(preset: any) {
|
||||
// Remove existing full screen background
|
||||
const currentId = this.fullScreenBackgroundId();
|
||||
if (currentId) {
|
||||
this.backgroundService().removeBackground(currentId);
|
||||
}
|
||||
|
||||
// Add new full screen background
|
||||
const id = this.backgroundService().applyFullScreenBackground(preset.config, {
|
||||
zIndex: -1
|
||||
});
|
||||
this.fullScreenBackgroundId.set(id);
|
||||
}
|
||||
|
||||
hasFullScreenBg() {
|
||||
return this.fullScreenBackgroundId() !== null;
|
||||
}
|
||||
}
|
||||
@@ -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/badge.component';
|
||||
|
||||
@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,79 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
h3 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
p {
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
margin-top: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
.demo-variant {
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-primary;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Override some demo-specific grid heights for better visualization
|
||||
.demo-section ui-bento-grid {
|
||||
min-height: 200px;
|
||||
|
||||
.demo-variant & {
|
||||
min-height: 150px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BentoGridComponent, BentoGridItemComponent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-bento-grid-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, BentoGridComponent, BentoGridItemComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Bento Grid Demo</h2>
|
||||
|
||||
<!-- Basic Grid -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Bento Grid</h3>
|
||||
<ui-bento-grid columns="auto-fit-md" gap="md">
|
||||
<ui-bento-grid-item header="Regular Item">
|
||||
Basic grid item with regular content.
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item featured="md" header="Featured Item" variant="primary">
|
||||
This is a featured item that spans 2 columns and 2 rows.
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item>
|
||||
Another regular item without header.
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item [colSpan]="2" variant="elevated" header="Wide Item">
|
||||
This item spans 2 columns wide.
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item [rowSpan]="2" variant="secondary" header="Tall Item">
|
||||
This item spans 2 rows tall.
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item>Regular item</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Regular item</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item featured="lg" header="Large Featured" variant="elevated">
|
||||
Large featured item spanning 3 columns and 2 rows.
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item>Regular item</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Regular item</ui-bento-grid-item>
|
||||
</ui-bento-grid>
|
||||
</section>
|
||||
|
||||
<!-- Fixed Columns -->
|
||||
<section class="demo-section">
|
||||
<h3>Fixed Column Grids</h3>
|
||||
|
||||
<h4>4 Columns</h4>
|
||||
<ui-bento-grid [columns]="4" gap="sm">
|
||||
<ui-bento-grid-item>Item 1</ui-bento-grid-item>
|
||||
<ui-bento-grid-item [colSpan]="2">Item 2 (2 cols)</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Item 3</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Item 4</ui-bento-grid-item>
|
||||
<ui-bento-grid-item [colSpan]="3">Item 5 (3 cols)</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Item 6</ui-bento-grid-item>
|
||||
</ui-bento-grid>
|
||||
|
||||
<h4>6 Columns</h4>
|
||||
<ui-bento-grid [columns]="6" gap="xs">
|
||||
@for (item of sixColumnItems; track item.id) {
|
||||
<ui-bento-grid-item
|
||||
[colSpan]="item.colSpan"
|
||||
[rowSpan]="item.rowSpan"
|
||||
[variant]="item.variant"
|
||||
[header]="item.header">
|
||||
{{ item.content }}
|
||||
</ui-bento-grid-item>
|
||||
}
|
||||
</ui-bento-grid>
|
||||
</section>
|
||||
|
||||
<!-- Gap Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Gap Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (gap of gaps; track gap) {
|
||||
<div class="demo-variant">
|
||||
<h4>Gap: {{ gap }}</h4>
|
||||
<ui-bento-grid [columns]="3" [gap]="gap">
|
||||
<ui-bento-grid-item>Item A</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Item B</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Item C</ui-bento-grid-item>
|
||||
<ui-bento-grid-item [colSpan]="2">Item D (2 cols)</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Item E</ui-bento-grid-item>
|
||||
</ui-bento-grid>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Sizes -->
|
||||
<section class="demo-section">
|
||||
<h3>Featured Item Sizes</h3>
|
||||
<ui-bento-grid columns="auto-fit-md" gap="md">
|
||||
<ui-bento-grid-item featured="sm" variant="primary" header="Small Featured">
|
||||
2×1 featured item
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item>Regular</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Regular</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item featured="md" variant="secondary" header="Medium Featured">
|
||||
2×2 featured item
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item>Regular</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Regular</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Regular</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item featured="lg" variant="elevated" header="Large Featured">
|
||||
3×2 featured item
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item>Regular</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Regular</ui-bento-grid-item>
|
||||
</ui-bento-grid>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Items -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Items</h3>
|
||||
<ui-bento-grid columns="auto-fit-md" gap="md">
|
||||
<ui-bento-grid-item
|
||||
[interactive]="true"
|
||||
variant="primary"
|
||||
header="Click Me!"
|
||||
(clicked)="handleItemClick('primary')">
|
||||
Interactive primary item
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item
|
||||
[interactive]="true"
|
||||
[colSpan]="2"
|
||||
variant="secondary"
|
||||
header="Wide Interactive"
|
||||
(clicked)="handleItemClick('secondary')">
|
||||
Wide interactive item
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item
|
||||
[interactive]="true"
|
||||
featured="md"
|
||||
variant="elevated"
|
||||
header="Featured Interactive"
|
||||
(clicked)="handleItemClick('featured')">
|
||||
Featured interactive item
|
||||
</ui-bento-grid-item>
|
||||
|
||||
<ui-bento-grid-item>Static item</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>Static item</ui-bento-grid-item>
|
||||
</ui-bento-grid>
|
||||
|
||||
@if (lastClicked) {
|
||||
<p>Last clicked: <strong>{{ lastClicked }}</strong></p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Row Height Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Row Heights</h3>
|
||||
<div class="demo-row">
|
||||
@for (rowHeight of rowHeights; track rowHeight) {
|
||||
<div class="demo-variant">
|
||||
<h4>Rows: {{ rowHeight }}</h4>
|
||||
<ui-bento-grid [columns]="3" [rowHeight]="rowHeight" gap="sm">
|
||||
<ui-bento-grid-item>Small content</ui-bento-grid-item>
|
||||
<ui-bento-grid-item>
|
||||
Longer content that might need more space depending on the row height setting.
|
||||
</ui-bento-grid-item>
|
||||
<ui-bento-grid-item [rowSpan]="2">Tall item</ui-bento-grid-item>
|
||||
<ui-bento-grid-item [colSpan]="2">Wide item</ui-bento-grid-item>
|
||||
</ui-bento-grid>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './bento-grid-demo.component.scss'
|
||||
})
|
||||
export class BentoGridDemoComponent {
|
||||
gaps = ['xs', 'sm', 'md', 'lg'] as const;
|
||||
rowHeights = ['sm', 'md', 'lg'] as const;
|
||||
lastClicked = '';
|
||||
|
||||
sixColumnItems = [
|
||||
{ id: 1, colSpan: 2, rowSpan: 1, variant: 'primary', header: 'Wide', content: '2×1 item' },
|
||||
{ id: 2, colSpan: 1, rowSpan: 2, variant: 'secondary', header: 'Tall', content: '1×2 item' },
|
||||
{ id: 3, colSpan: 1, rowSpan: 1, variant: 'default', header: '', content: 'Regular' },
|
||||
{ id: 4, colSpan: 2, rowSpan: 1, variant: 'elevated', header: 'Featured', content: '2×1 item' },
|
||||
{ id: 5, colSpan: 1, rowSpan: 1, variant: 'default', header: '', content: 'Regular' },
|
||||
{ id: 6, colSpan: 3, rowSpan: 1, variant: 'primary', header: 'Extra Wide', content: '3×1 item' },
|
||||
{ id: 7, colSpan: 1, rowSpan: 1, variant: 'default', header: '', content: 'Regular' },
|
||||
{ id: 8, colSpan: 2, rowSpan: 2, variant: 'elevated', header: 'Large', content: '2×2 featured' },
|
||||
{ id: 9, colSpan: 1, rowSpan: 1, variant: 'default', header: '', content: 'Regular' },
|
||||
{ id: 10, colSpan: 1, rowSpan: 1, variant: 'default', header: '', content: 'Regular' },
|
||||
] as const;
|
||||
|
||||
handleItemClick(type: string): void {
|
||||
this.lastClicked = type;
|
||||
console.log('Bento grid item clicked:', type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding-bottom: 120px; // Extra space to accommodate bottom navigation examples
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&--vertical {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
|
||||
h4 {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
color: $semantic-color-text-primary;
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
}
|
||||
}
|
||||
|
||||
.demo-bottom-nav-container {
|
||||
position: relative;
|
||||
height: 80px;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
overflow: hidden;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
|
||||
// Override the fixed positioning for demo purposes
|
||||
:deep(.ui-bottom-navigation) {
|
||||
position: absolute !important;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
padding: $semantic-spacing-component-md;
|
||||
margin: $semantic-spacing-component-md 0;
|
||||
|
||||
p {
|
||||
margin: 0 0 $semantic-spacing-component-xs 0;
|
||||
color: $semantic-color-text-primary;
|
||||
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);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border: none;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
// Typography
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: $semantic-shadow-button-rest;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { faHome, faSearch, faUser, faHeart, faCog, faShoppingCart, faBell, faBookmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { BottomNavigationComponent, BottomNavigationItem } from '../../../../../ui-essentials/src/lib/components/navigation/bottom-navigation';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-bottom-navigation-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, BottomNavigationComponent, FontAwesomeModule],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Bottom Navigation Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row demo-row--vertical">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ size.toUpperCase() }} Size</h4>
|
||||
<div class="demo-bottom-nav-container">
|
||||
<ui-bottom-navigation
|
||||
[size]="size"
|
||||
[items]="basicItems"
|
||||
[activeItemId]="activeItemIds[size]"
|
||||
(activeItemChanged)="handleActiveItemChange(size, $event)"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
|
||||
<fa-icon [icon]="faHome" data-icon="home"></fa-icon>
|
||||
<fa-icon [icon]="faSearch" data-icon="search"></fa-icon>
|
||||
<fa-icon [icon]="faUser" data-icon="profile"></fa-icon>
|
||||
<fa-icon [icon]="faCog" data-icon="settings"></fa-icon>
|
||||
</ui-bottom-navigation>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Styles -->
|
||||
<section class="demo-section">
|
||||
<h3>Variants</h3>
|
||||
<div class="demo-row demo-row--vertical">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ variant.charAt(0).toUpperCase() + variant.slice(1) }} Variant</h4>
|
||||
<div class="demo-bottom-nav-container">
|
||||
<ui-bottom-navigation
|
||||
[variant]="variant"
|
||||
[items]="basicItems"
|
||||
[activeItemId]="activeItemIds[variant]"
|
||||
(activeItemChanged)="handleActiveItemChange(variant, $event)"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
|
||||
<fa-icon [icon]="faHome" data-icon="home"></fa-icon>
|
||||
<fa-icon [icon]="faSearch" data-icon="search"></fa-icon>
|
||||
<fa-icon [icon]="faUser" data-icon="profile"></fa-icon>
|
||||
<fa-icon [icon]="faCog" data-icon="settings"></fa-icon>
|
||||
</ui-bottom-navigation>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- With Badges -->
|
||||
<section class="demo-section">
|
||||
<h3>With Badges</h3>
|
||||
<div class="demo-bottom-nav-container">
|
||||
<ui-bottom-navigation
|
||||
[items]="itemsWithBadges"
|
||||
[activeItemId]="badgeActiveItemId"
|
||||
(activeItemChanged)="badgeActiveItemId = $event"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
|
||||
<fa-icon [icon]="faHome" data-icon="home"></fa-icon>
|
||||
<fa-icon [icon]="faShoppingCart" data-icon="cart"></fa-icon>
|
||||
<fa-icon [icon]="faBell" data-icon="notifications"></fa-icon>
|
||||
<fa-icon [icon]="faBookmark" data-icon="saved"></fa-icon>
|
||||
<fa-icon [icon]="faUser" data-icon="profile"></fa-icon>
|
||||
</ui-bottom-navigation>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-row demo-row--vertical">
|
||||
<div class="demo-item">
|
||||
<h4>Elevated</h4>
|
||||
<div class="demo-bottom-nav-container">
|
||||
<ui-bottom-navigation
|
||||
[items]="basicItems"
|
||||
[elevated]="true"
|
||||
[activeItemId]="elevatedActiveItemId"
|
||||
(activeItemChanged)="elevatedActiveItemId = $event"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
|
||||
<fa-icon [icon]="faHome" data-icon="home"></fa-icon>
|
||||
<fa-icon [icon]="faSearch" data-icon="search"></fa-icon>
|
||||
<fa-icon [icon]="faUser" data-icon="profile"></fa-icon>
|
||||
<fa-icon [icon]="faCog" data-icon="settings"></fa-icon>
|
||||
</ui-bottom-navigation>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-item">
|
||||
<h4>With Disabled Item</h4>
|
||||
<div class="demo-bottom-nav-container">
|
||||
<ui-bottom-navigation
|
||||
[items]="itemsWithDisabled"
|
||||
[activeItemId]="disabledActiveItemId"
|
||||
(activeItemChanged)="disabledActiveItemId = $event"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
|
||||
<fa-icon [icon]="faHome" data-icon="home"></fa-icon>
|
||||
<fa-icon [icon]="faSearch" data-icon="search"></fa-icon>
|
||||
<fa-icon [icon]="faUser" data-icon="profile"></fa-icon>
|
||||
<fa-icon [icon]="faCog" data-icon="settings"></fa-icon>
|
||||
</ui-bottom-navigation>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Example</h3>
|
||||
<div class="demo-bottom-nav-container">
|
||||
<ui-bottom-navigation
|
||||
[items]="interactiveItems"
|
||||
[activeItemId]="interactiveActiveItemId"
|
||||
(activeItemChanged)="handleInteractiveItemChange($event)"
|
||||
(itemClicked)="handleInteractiveItemClick($event)">
|
||||
|
||||
<fa-icon [icon]="faHome" data-icon="home"></fa-icon>
|
||||
<fa-icon [icon]="faSearch" data-icon="search"></fa-icon>
|
||||
<fa-icon [icon]="faHeart" data-icon="favorites"></fa-icon>
|
||||
<fa-icon [icon]="faUser" data-icon="profile"></fa-icon>
|
||||
</ui-bottom-navigation>
|
||||
</div>
|
||||
|
||||
<div class="demo-info">
|
||||
<p><strong>Active Item:</strong> {{ getActiveItemLabel() }}</p>
|
||||
<p><strong>Click Count:</strong> {{ clickCount }}</p>
|
||||
<p><strong>Last Clicked:</strong> {{ lastClickedItem || 'None' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-controls">
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="toggleHidden()"
|
||||
[class.active]="isHidden">
|
||||
{{ isHidden ? 'Show' : 'Hide' }} Navigation
|
||||
</button>
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="incrementBadge()">
|
||||
Increment Heart Badge
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './bottom-navigation-demo.component.scss'
|
||||
})
|
||||
export class BottomNavigationDemoComponent {
|
||||
// FontAwesome icons
|
||||
faHome = faHome;
|
||||
faSearch = faSearch;
|
||||
faUser = faUser;
|
||||
faHeart = faHeart;
|
||||
faCog = faCog;
|
||||
faShoppingCart = faShoppingCart;
|
||||
faBell = faBell;
|
||||
faBookmark = faBookmark;
|
||||
|
||||
// Demo data
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
variants = ['default', 'primary', 'secondary'] as const;
|
||||
|
||||
// Active item tracking for different variants
|
||||
activeItemIds: Record<string, string> = {
|
||||
sm: 'home',
|
||||
md: 'home',
|
||||
lg: 'home',
|
||||
default: 'home',
|
||||
primary: 'home',
|
||||
secondary: 'home'
|
||||
};
|
||||
|
||||
badgeActiveItemId = 'home';
|
||||
elevatedActiveItemId = 'home';
|
||||
disabledActiveItemId = 'home';
|
||||
interactiveActiveItemId = 'home';
|
||||
|
||||
// Interactive demo state
|
||||
clickCount = 0;
|
||||
lastClickedItem = '';
|
||||
isHidden = false;
|
||||
heartBadgeCount = 3;
|
||||
|
||||
// Item configurations
|
||||
basicItems: BottomNavigationItem[] = [
|
||||
{ id: 'home', label: 'Home', icon: 'home' },
|
||||
{ id: 'search', label: 'Search', icon: 'search' },
|
||||
{ id: 'profile', label: 'Profile', icon: 'profile' },
|
||||
{ id: 'settings', label: 'Settings', icon: 'settings' }
|
||||
];
|
||||
|
||||
itemsWithBadges: BottomNavigationItem[] = [
|
||||
{ id: 'home', label: 'Home', icon: 'home' },
|
||||
{ id: 'cart', label: 'Cart', icon: 'cart', badge: 2 },
|
||||
{ id: 'notifications', label: 'Alerts', icon: 'notifications', badge: '99+' },
|
||||
{ id: 'saved', label: 'Saved', icon: 'saved', badge: 15 },
|
||||
{ id: 'profile', label: 'Profile', icon: 'profile' }
|
||||
];
|
||||
|
||||
itemsWithDisabled: BottomNavigationItem[] = [
|
||||
{ id: 'home', label: 'Home', icon: 'home' },
|
||||
{ id: 'search', label: 'Search', icon: 'search', disabled: true },
|
||||
{ id: 'profile', label: 'Profile', icon: 'profile' },
|
||||
{ id: 'settings', label: 'Settings', icon: 'settings' }
|
||||
];
|
||||
|
||||
get interactiveItems(): BottomNavigationItem[] {
|
||||
return [
|
||||
{ id: 'home', label: 'Home', icon: 'home' },
|
||||
{ id: 'search', label: 'Search', icon: 'search' },
|
||||
{ id: 'favorites', label: 'Favorites', icon: 'favorites', badge: this.heartBadgeCount > 0 ? this.heartBadgeCount : undefined },
|
||||
{ id: 'profile', label: 'Profile', icon: 'profile' }
|
||||
];
|
||||
}
|
||||
|
||||
handleActiveItemChange(context: string, itemId: string): void {
|
||||
this.activeItemIds[context] = itemId;
|
||||
}
|
||||
|
||||
handleItemClick(item: BottomNavigationItem): void {
|
||||
console.log('Item clicked:', item);
|
||||
}
|
||||
|
||||
handleInteractiveItemChange(itemId: string): void {
|
||||
this.interactiveActiveItemId = itemId;
|
||||
}
|
||||
|
||||
handleInteractiveItemClick(item: BottomNavigationItem): void {
|
||||
this.clickCount++;
|
||||
this.lastClickedItem = item.label;
|
||||
console.log('Interactive item clicked:', item);
|
||||
}
|
||||
|
||||
getActiveItemLabel(): string {
|
||||
const activeItem = this.interactiveItems.find(item => item.id === this.interactiveActiveItemId);
|
||||
return activeItem?.label || 'None';
|
||||
}
|
||||
|
||||
toggleHidden(): void {
|
||||
this.isHidden = !this.isHidden;
|
||||
// In a real implementation, you would update the hidden property of the navigation
|
||||
// For demo purposes, this just toggles a state variable
|
||||
}
|
||||
|
||||
incrementBadge(): void {
|
||||
this.heartBadgeCount++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
|
||||
h3 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-column {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
background: $semantic-color-surface-secondary;
|
||||
}
|
||||
|
||||
.inner-content {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
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);
|
||||
}
|
||||
|
||||
.margin-demo-container {
|
||||
background: $semantic-color-surface-elevated;
|
||||
padding: $semantic-spacing-component-xl;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
|
||||
.margin-box {
|
||||
background: $semantic-color-success;
|
||||
color: $semantic-color-on-success;
|
||||
padding: $semantic-spacing-component-md;
|
||||
text-align: center;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-demo {
|
||||
gap: $semantic-spacing-component-md;
|
||||
|
||||
.flex-item {
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-demo {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $semantic-spacing-component-md;
|
||||
|
||||
.grid-item {
|
||||
background: $semantic-color-warning;
|
||||
color: $semantic-color-on-warning;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
text-align: center;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-demo {
|
||||
background: $semantic-color-info;
|
||||
color: $semantic-color-on-info;
|
||||
margin-right: $semantic-spacing-component-sm;
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BoxComponent } from '../../../../../ui-essentials/src/lib/components/layout/box';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-box-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, BoxComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Box Component Demo</h2>
|
||||
|
||||
<!-- Padding Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Padding Utilities</h3>
|
||||
<div class="demo-row">
|
||||
@for (spacing of spacings; track spacing) {
|
||||
<div class="demo-column">
|
||||
<h4>p="{{ spacing }}"</h4>
|
||||
<ui-box [p]="spacing" [border]="true" class="content-box">
|
||||
<div class="inner-content">Content with {{ spacing }} padding</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Directional Padding -->
|
||||
<section class="demo-section">
|
||||
<h3>Directional Padding</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-column">
|
||||
<h4>px="lg" (horizontal)</h4>
|
||||
<ui-box px="lg" [border]="true" class="content-box">
|
||||
<div class="inner-content">Horizontal padding</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
|
||||
<div class="demo-column">
|
||||
<h4>py="lg" (vertical)</h4>
|
||||
<ui-box py="lg" [border]="true" class="content-box">
|
||||
<div class="inner-content">Vertical padding</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
|
||||
<div class="demo-column">
|
||||
<h4>pt="xl" pb="sm"</h4>
|
||||
<ui-box pt="xl" pb="sm" [border]="true" class="content-box">
|
||||
<div class="inner-content">Mixed padding</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Margin Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Margin Utilities</h3>
|
||||
<div class="margin-demo-container">
|
||||
@for (spacing of marginSpacings; track spacing) {
|
||||
<ui-box [m]="spacing" [border]="true" [rounded]="true" class="margin-box">
|
||||
m="{{ spacing }}"
|
||||
</ui-box>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Display Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Display Options</h3>
|
||||
<div class="demo-column">
|
||||
<h4>display="flex"</h4>
|
||||
<ui-box display="flex" p="md" [border]="true" class="flex-demo">
|
||||
<div class="flex-item">Item 1</div>
|
||||
<div class="flex-item">Item 2</div>
|
||||
<div class="flex-item">Item 3</div>
|
||||
</ui-box>
|
||||
|
||||
<h4>display="grid"</h4>
|
||||
<ui-box display="grid" p="md" [border]="true" class="grid-demo">
|
||||
<div class="grid-item">A</div>
|
||||
<div class="grid-item">B</div>
|
||||
<div class="grid-item">C</div>
|
||||
<div class="grid-item">D</div>
|
||||
</ui-box>
|
||||
|
||||
<h4>display="inline-block"</h4>
|
||||
<div>
|
||||
<ui-box display="inline-block" p="sm" [border]="true" class="inline-demo">Box 1</ui-box>
|
||||
<ui-box display="inline-block" p="sm" [border]="true" class="inline-demo">Box 2</ui-box>
|
||||
<ui-box display="inline-block" p="sm" [border]="true" class="inline-demo">Box 3</ui-box>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Visual Styles -->
|
||||
<section class="demo-section">
|
||||
<h3>Visual Styles</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-column">
|
||||
<h4>Basic</h4>
|
||||
<ui-box p="md">
|
||||
<div class="inner-content">Basic box</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
|
||||
<div class="demo-column">
|
||||
<h4>With Border</h4>
|
||||
<ui-box p="md" [border]="true">
|
||||
<div class="inner-content">Bordered box</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
|
||||
<div class="demo-column">
|
||||
<h4>Rounded</h4>
|
||||
<ui-box p="md" [border]="true" [rounded]="true">
|
||||
<div class="inner-content">Rounded box</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
|
||||
<div class="demo-column">
|
||||
<h4>With Shadow</h4>
|
||||
<ui-box p="md" [shadow]="true">
|
||||
<div class="inner-content">Shadow box</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
|
||||
<div class="demo-column">
|
||||
<h4>All Styles</h4>
|
||||
<ui-box p="lg" [border]="true" [rounded]="true" [shadow]="true">
|
||||
<div class="inner-content">Full styled</div>
|
||||
</ui-box>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Complex Layout Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Complex Layout Example</h3>
|
||||
<ui-box p="lg" [border]="true" [rounded]="true" [shadow]="true">
|
||||
<ui-box mb="md">
|
||||
<h4 style="margin: 0;">Card Header</h4>
|
||||
</ui-box>
|
||||
|
||||
<ui-box display="flex" mb="md">
|
||||
<ui-box pr="md" style="flex: 1;">
|
||||
<div class="inner-content">Left column content with some text that wraps nicely.</div>
|
||||
</ui-box>
|
||||
|
||||
<ui-box pl="md" [border]="true" style="flex: 1;">
|
||||
<div class="inner-content">Right column with border and padding.</div>
|
||||
</ui-box>
|
||||
</ui-box>
|
||||
|
||||
<ui-box pt="md" [border]="true" style="border-left: none; border-right: none; border-bottom: none;">
|
||||
<div class="inner-content">Footer with top border only</div>
|
||||
</ui-box>
|
||||
</ui-box>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './box-demo.component.scss'
|
||||
})
|
||||
export class BoxDemoComponent {
|
||||
spacings = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
|
||||
marginSpacings = ['xs', 'sm', 'md', 'lg'] as const;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
|
||||
h3 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
p {
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
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);
|
||||
text-align: center;
|
||||
border: $semantic-border-width-2 dashed;
|
||||
|
||||
&--primary {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-on-primary;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
border-color: $semantic-color-on-secondary;
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: $semantic-color-success;
|
||||
color: $semantic-color-on-success;
|
||||
border-color: $semantic-color-on-success;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background: $semantic-color-warning;
|
||||
color: $semantic-color-on-warning;
|
||||
border-color: $semantic-color-on-warning;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: $semantic-color-danger;
|
||||
color: $semantic-color-on-danger;
|
||||
border-color: $semantic-color-on-danger;
|
||||
}
|
||||
|
||||
&--info {
|
||||
background: $semantic-color-info;
|
||||
color: $semantic-color-on-info;
|
||||
border-color: $semantic-color-on-info;
|
||||
}
|
||||
|
||||
&--dark {
|
||||
background: $semantic-color-text-primary;
|
||||
color: $semantic-color-surface-primary;
|
||||
border-color: $semantic-color-surface-primary;
|
||||
}
|
||||
|
||||
&--inline {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-on-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.viewport-info {
|
||||
margin: $semantic-spacing-layout-section-sm 0;
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.demo-content {
|
||||
font-weight: $semantic-typography-font-weight-bold;
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
padding: $semantic-spacing-component-lg;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
padding-left: $semantic-spacing-component-lg;
|
||||
|
||||
li {
|
||||
margin-bottom: $semantic-spacing-content-list-item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BreakpointContainerComponent } from '../../../../../ui-essentials/src/lib/components/layout/breakpoint-container';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-breakpoint-container-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, BreakpointContainerComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Breakpoint Container Demo</h2>
|
||||
<p>This component shows/hides content based on screen size. Resize your browser to see the effects.</p>
|
||||
|
||||
<!-- Basic Visibility Controls -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Visibility Controls</h3>
|
||||
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>Always Visible</h4>
|
||||
<ui-breakpoint-container visibility="always">
|
||||
<div class="demo-content demo-content--primary">
|
||||
Always visible content
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Mobile Only (< 768px)</h4>
|
||||
<ui-breakpoint-container visibility="mobile-only">
|
||||
<div class="demo-content demo-content--success">
|
||||
Only visible on mobile devices
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Tablet Only (768px - 1024px)</h4>
|
||||
<ui-breakpoint-container visibility="tablet-only">
|
||||
<div class="demo-content demo-content--warning">
|
||||
Only visible on tablets
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Desktop Only (≥ 1024px)</h4>
|
||||
<ui-breakpoint-container visibility="desktop-only">
|
||||
<div class="demo-content demo-content--info">
|
||||
Only visible on desktop
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Hidden Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Hidden Variants</h3>
|
||||
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>Mobile Hidden</h4>
|
||||
<ui-breakpoint-container visibility="mobile-hidden">
|
||||
<div class="demo-content demo-content--danger">
|
||||
Hidden on mobile (< 768px)
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Tablet Hidden</h4>
|
||||
<ui-breakpoint-container visibility="tablet-hidden">
|
||||
<div class="demo-content demo-content--secondary">
|
||||
Hidden on tablets (768px - 1024px)
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Desktop Hidden</h4>
|
||||
<ui-breakpoint-container visibility="desktop-hidden">
|
||||
<div class="demo-content demo-content--dark">
|
||||
Hidden on desktop (≥ 1024px)
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Screen Size Based -->
|
||||
<section class="demo-section">
|
||||
<h3>Screen Size Based</h3>
|
||||
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>Mobile Screen (< 640px)</h4>
|
||||
<ui-breakpoint-container visibility="mobile-screen">
|
||||
<div class="demo-content demo-content--success">
|
||||
Mobile screen size only
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Tablet Screen (640px - 1024px)</h4>
|
||||
<ui-breakpoint-container visibility="tablet-screen">
|
||||
<div class="demo-content demo-content--warning">
|
||||
Tablet screen size only
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Desktop Screen (≥ 1024px)</h4>
|
||||
<ui-breakpoint-container visibility="desktop-screen">
|
||||
<div class="demo-content demo-content--info">
|
||||
Desktop screen size only
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Range Based -->
|
||||
<section class="demo-section">
|
||||
<h3>Range Based Visibility</h3>
|
||||
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>Small to Medium (640px - 768px)</h4>
|
||||
<ui-breakpoint-container visibility="sm-to-md">
|
||||
<div class="demo-content demo-content--primary">
|
||||
Visible between small and medium breakpoints
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Medium to Large (768px - 1024px)</h4>
|
||||
<ui-breakpoint-container visibility="md-to-lg">
|
||||
<div class="demo-content demo-content--secondary">
|
||||
Visible between medium and large breakpoints
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Custom Breakpoint Rules -->
|
||||
<section class="demo-section">
|
||||
<h3>Custom Breakpoint Rules</h3>
|
||||
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>Show from Small (≥ 640px)</h4>
|
||||
<ui-breakpoint-container [showSm]="true">
|
||||
<div class="demo-content demo-content--success">
|
||||
Visible from small breakpoint and up
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Hide from Medium (≥ 768px)</h4>
|
||||
<ui-breakpoint-container [hideMd]="true">
|
||||
<div class="demo-content demo-content--danger">
|
||||
Hidden from medium breakpoint and up
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Show from Large (≥ 1024px)</h4>
|
||||
<ui-breakpoint-container [showLg]="true">
|
||||
<div class="demo-content demo-content--info">
|
||||
Visible from large breakpoint and up
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Display Types -->
|
||||
<section class="demo-section">
|
||||
<h3>Display Types</h3>
|
||||
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>Block Display (Default)</h4>
|
||||
<ui-breakpoint-container displayType="block" visibility="always">
|
||||
<div class="demo-content demo-content--primary">
|
||||
Block display (default)
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Inline Display</h4>
|
||||
<p>
|
||||
This is inline text with
|
||||
<ui-breakpoint-container displayType="inline" visibility="desktop-only">
|
||||
<span class="demo-content demo-content--inline">desktop-only inline content</span>
|
||||
</ui-breakpoint-container>
|
||||
continuing after.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Flex Display</h4>
|
||||
<ui-breakpoint-container displayType="flex" visibility="always">
|
||||
<div class="demo-content demo-content--success" style="flex: 1; margin-right: 8px;">
|
||||
Flex Item 1
|
||||
</div>
|
||||
<div class="demo-content demo-content--warning" style="flex: 1;">
|
||||
Flex Item 2
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Print Media -->
|
||||
<section class="demo-section">
|
||||
<h3>Print Media</h3>
|
||||
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>Print Hidden</h4>
|
||||
<ui-breakpoint-container [printHidden]="true">
|
||||
<div class="demo-content demo-content--secondary">
|
||||
This content is hidden when printing
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Print Only</h4>
|
||||
<ui-breakpoint-container [printOnly]="true">
|
||||
<div class="demo-content demo-content--dark">
|
||||
This content only appears when printing
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Test</h3>
|
||||
<p>Current viewport info: Resize your browser window to test different breakpoints.</p>
|
||||
|
||||
<div class="viewport-info">
|
||||
<ui-breakpoint-container visibility="mobile-screen">
|
||||
<div class="demo-content demo-content--success">
|
||||
📱 Mobile Screen (< 640px)
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
|
||||
<ui-breakpoint-container visibility="tablet-screen">
|
||||
<div class="demo-content demo-content--warning">
|
||||
📟 Tablet Screen (640px - 1024px)
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
|
||||
<ui-breakpoint-container visibility="desktop-screen">
|
||||
<div class="demo-content demo-content--info">
|
||||
🖥️ Desktop Screen (≥ 1024px)
|
||||
</div>
|
||||
</ui-breakpoint-container>
|
||||
</div>
|
||||
|
||||
<p><strong>Test Instructions:</strong></p>
|
||||
<ul>
|
||||
<li>Resize your browser window to see different content appear/disappear</li>
|
||||
<li>Try the print preview to see print-specific content</li>
|
||||
<li>Different display types maintain their layout characteristics</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './breakpoint-container-demo.component.scss'
|
||||
})
|
||||
export class BreakpointContainerDemoComponent {}
|
||||
@@ -0,0 +1,399 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/button.component';
|
||||
import { TextButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/text-button.component';
|
||||
import { GhostButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/ghost-button.component';
|
||||
import { FabComponent } from '../../../../../ui-essentials/src/lib/components/buttons/fab.component';
|
||||
import { SimpleButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/simple-button.component';
|
||||
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/card.component';
|
||||
import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/button.component';
|
||||
|
||||
@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 "../../../../../ui-design-system/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/carousel.component';
|
||||
|
||||
@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,197 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
|
||||
h3 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.demo-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-stack-md;
|
||||
}
|
||||
|
||||
.demo-example {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-boundary {
|
||||
border: 2px dashed $semantic-color-border-secondary;
|
||||
background-color: rgba(128, 128, 128, 0.1);
|
||||
min-height: 100px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
background: $semantic-color-surface-elevated;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: 1px solid $semantic-color-border-primary;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
color: $semantic-color-text-primary;
|
||||
text-align: center;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
.demo-content-small {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
padding: $semantic-spacing-component-xs;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
}
|
||||
|
||||
.demo-inline {
|
||||
margin: 0 $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background: $semantic-color-surface-primary;
|
||||
padding: $semantic-spacing-component-xl;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
border: 1px solid $semantic-color-border-subtle;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
|
||||
h3 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
p {
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-loading {
|
||||
text-align: center;
|
||||
|
||||
.demo-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid $semantic-color-border-subtle;
|
||||
border-top: 4px solid $semantic-color-primary;
|
||||
border-radius: $semantic-border-radius-full;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto $semantic-spacing-component-md auto;
|
||||
}
|
||||
|
||||
p {
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.demo-hero {
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-xl;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
background: $semantic-color-surface-secondary;
|
||||
color: $semantic-color-text-primary;
|
||||
border: 1px solid $semantic-color-border-primary;
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-primary;
|
||||
|
||||
&:hover {
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-example {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CenterComponent } from '../../../../../ui-essentials/src/lib/components/layout/center';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-center-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, CenterComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Center Demo</h2>
|
||||
|
||||
<!-- Axis Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Center Axis</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-example">
|
||||
<h4>Both Axes (Default)</h4>
|
||||
<ui-center [customMinHeight]="'200px'" class="demo-boundary">
|
||||
<div class="demo-content">Centered Both Ways</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Horizontal Only</h4>
|
||||
<ui-center axis="horizontal" [customMinHeight]="'200px'" class="demo-boundary">
|
||||
<div class="demo-content">Centered Horizontally</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Vertical Only</h4>
|
||||
<ui-center axis="vertical" [customMinHeight]="'200px'" class="demo-boundary">
|
||||
<div class="demo-content">Centered Vertically</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Max Width Constraints -->
|
||||
<section class="demo-section">
|
||||
<h3>Max Width Constraints</h3>
|
||||
<div class="demo-column">
|
||||
<div class="demo-example">
|
||||
<h4>Small (640px)</h4>
|
||||
<ui-center maxWidth="sm" padding="md" class="demo-boundary">
|
||||
<div class="demo-content">
|
||||
This content is constrained to a maximum width of 640px and will be centered within its container.
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.
|
||||
</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Medium (768px)</h4>
|
||||
<ui-center maxWidth="md" padding="md" class="demo-boundary">
|
||||
<div class="demo-content">
|
||||
This content is constrained to a maximum width of 768px and will be centered within its container.
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Large (1024px)</h4>
|
||||
<ui-center maxWidth="lg" padding="md" class="demo-boundary">
|
||||
<div class="demo-content">
|
||||
This content is constrained to a maximum width of 1024px and will be centered within its container.
|
||||
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.
|
||||
</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Spacing Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Spacing</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-example">
|
||||
<h4>Small Padding</h4>
|
||||
<ui-center padding="sm" class="demo-boundary">
|
||||
<div class="demo-content">Small Padding</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Medium Padding</h4>
|
||||
<ui-center padding="md" class="demo-boundary">
|
||||
<div class="demo-content">Medium Padding</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Large Padding</h4>
|
||||
<ui-center padding="lg" class="demo-boundary">
|
||||
<div class="demo-content">Large Padding</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Inline Variant -->
|
||||
<section class="demo-section">
|
||||
<h3>Inline Center</h3>
|
||||
<div class="demo-example">
|
||||
<p>
|
||||
Here is some text with an
|
||||
<ui-center [inline]="true" class="demo-inline">
|
||||
<span class="demo-content-small">inline centered element</span>
|
||||
</ui-center>
|
||||
in the middle of the paragraph.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Practical Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Practical Examples</h3>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Card Layout</h4>
|
||||
<ui-center maxWidth="md" padding="lg">
|
||||
<div class="demo-card">
|
||||
<h3>Welcome Message</h3>
|
||||
<p>This card is centered with constrained width for better readability.</p>
|
||||
<button class="demo-button">Get Started</button>
|
||||
</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Loading State</h4>
|
||||
<ui-center [customMinHeight]="'300px'">
|
||||
<div class="demo-loading">
|
||||
<div class="demo-spinner"></div>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Hero Section</h4>
|
||||
<ui-center [customMinHeight]="'400px'" maxWidth="lg" padding="xl">
|
||||
<div class="demo-hero">
|
||||
<h2>Hero Title</h2>
|
||||
<p>A compelling hero message that's perfectly centered and constrained for optimal reading.</p>
|
||||
<button class="demo-button demo-button--primary">Call to Action</button>
|
||||
</div>
|
||||
</ui-center>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './center-demo.component.scss'
|
||||
})
|
||||
export class CenterDemoComponent {}
|
||||
@@ -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/checkbox.component';
|
||||
|
||||
@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/chip.component';
|
||||
import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/button.component';
|
||||
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,71 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-sm;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h1 {
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
border-bottom: 2px solid $semantic-color-border-primary;
|
||||
padding-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
|
||||
h2 {
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
font-size: $base-typography-font-size-xl;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: $semantic-spacing-component-lg 0 $semantic-spacing-component-md 0;
|
||||
font-size: $base-typography-font-size-lg;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
line-height: $base-typography-line-height-relaxed;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-controls {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.theme-btn {
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
border: $semantic-border-button-width $semantic-border-button-style $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: pointer;
|
||||
font-size: $base-typography-font-size-sm;
|
||||
text-transform: capitalize;
|
||||
transition: all $semantic-duration-fast $semantic-easing-standard;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-color: $semantic-color-brand-primary;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $semantic-color-brand-primary;
|
||||
color: $semantic-color-on-brand-primary;
|
||||
border-color: $semantic-color-brand-primary;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: $semantic-border-focus-width solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
CodeSnippetComponent,
|
||||
InlineCodeComponent,
|
||||
CodeBlockComponent,
|
||||
CodeThemeService,
|
||||
CodeTheme
|
||||
} from 'ui-code-display';
|
||||
|
||||
@Component({
|
||||
selector: 'app-code-display-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
CodeSnippetComponent,
|
||||
InlineCodeComponent,
|
||||
CodeBlockComponent
|
||||
],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h1>Code Display Components Demo</h1>
|
||||
|
||||
<section class="demo-section">
|
||||
<h2>Theme Selector</h2>
|
||||
<div class="theme-controls">
|
||||
@for (theme of themes; track theme) {
|
||||
<button
|
||||
(click)="setTheme(theme)"
|
||||
[class.active]="currentTheme() === theme"
|
||||
class="theme-btn">
|
||||
{{ theme }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="demo-section">
|
||||
<h2>Inline Code</h2>
|
||||
<p>
|
||||
Use <ui-inline-code code="console.log('hello')" language="javascript"></ui-inline-code>
|
||||
to output to the console.
|
||||
</p>
|
||||
<p>
|
||||
Install dependencies with <ui-inline-code code="npm install" language="bash" [copyable]="true"></ui-inline-code>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="demo-section">
|
||||
<h2>Code Snippet</h2>
|
||||
<ui-code-snippet
|
||||
[code]="typescriptCode"
|
||||
language="typescript"
|
||||
title="Component Example"
|
||||
size="md"
|
||||
[showLineNumbers]="true"
|
||||
[copyable]="true"
|
||||
[highlightLines]="[3, 5, '7-9']">
|
||||
</ui-code-snippet>
|
||||
</section>
|
||||
|
||||
<section class="demo-section">
|
||||
<h2>Code Block</h2>
|
||||
<ui-code-block
|
||||
[code]="cssCode"
|
||||
language="css"
|
||||
variant="bordered"
|
||||
[showLineNumbers]="true"
|
||||
[copyable]="true">
|
||||
</ui-code-block>
|
||||
</section>
|
||||
|
||||
<section class="demo-section">
|
||||
<h2>Different Sizes</h2>
|
||||
<h3>Small</h3>
|
||||
<ui-code-snippet
|
||||
[code]="shortCode"
|
||||
language="javascript"
|
||||
size="sm"
|
||||
title="Small Size">
|
||||
</ui-code-snippet>
|
||||
|
||||
<h3>Medium (Default)</h3>
|
||||
<ui-code-snippet
|
||||
[code]="shortCode"
|
||||
language="javascript"
|
||||
size="md"
|
||||
title="Medium Size">
|
||||
</ui-code-snippet>
|
||||
|
||||
<h3>Large</h3>
|
||||
<ui-code-snippet
|
||||
[code]="shortCode"
|
||||
language="javascript"
|
||||
size="lg"
|
||||
title="Large Size">
|
||||
</ui-code-snippet>
|
||||
</section>
|
||||
|
||||
<section class="demo-section">
|
||||
<h2>Different Languages</h2>
|
||||
|
||||
<h3>Python</h3>
|
||||
<ui-code-snippet
|
||||
[code]="pythonCode"
|
||||
language="python"
|
||||
title="Python Example">
|
||||
</ui-code-snippet>
|
||||
|
||||
<h3>JSON</h3>
|
||||
<ui-code-snippet
|
||||
[code]="jsonCode"
|
||||
language="json"
|
||||
title="Configuration">
|
||||
</ui-code-snippet>
|
||||
|
||||
<h3>HTML</h3>
|
||||
<ui-code-snippet
|
||||
[code]="htmlCode"
|
||||
language="html"
|
||||
title="Template">
|
||||
</ui-code-snippet>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './code-display-demo.component.scss'
|
||||
})
|
||||
export class CodeDisplayDemoComponent {
|
||||
|
||||
constructor(private themeService: CodeThemeService) {
|
||||
this.themes = this.themeService.getAvailableThemes();
|
||||
this.currentTheme = this.themeService.theme;
|
||||
}
|
||||
|
||||
readonly themes: any[];
|
||||
readonly currentTheme: any;
|
||||
|
||||
readonly typescriptCode = `import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-example',
|
||||
template: \`
|
||||
<div class="container">
|
||||
<h1>{{ title }}</h1>
|
||||
<p>{{ description }}</p>
|
||||
</div>
|
||||
\`
|
||||
})
|
||||
export class ExampleComponent {
|
||||
title = 'Hello World';
|
||||
description = 'This is a demo component';
|
||||
}`;
|
||||
|
||||
readonly cssCode = `.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
|
||||
h1 {
|
||||
color: var(--primary-color);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
}
|
||||
}`;
|
||||
|
||||
readonly shortCode = `const greeting = 'Hello, World!';
|
||||
console.log(greeting);`;
|
||||
|
||||
readonly pythonCode = `def fibonacci(n):
|
||||
if n <= 1:
|
||||
return n
|
||||
return fibonacci(n-1) + fibonacci(n-2)
|
||||
|
||||
# Generate first 10 Fibonacci numbers
|
||||
for i in range(10):
|
||||
print(f"F({i}) = {fibonacci(i)}")`;
|
||||
|
||||
readonly jsonCode = `{
|
||||
"name": "ui-code-display",
|
||||
"version": "1.0.0",
|
||||
"description": "Code display components for Angular",
|
||||
"dependencies": {
|
||||
"@angular/core": "^19.0.0",
|
||||
"prismjs": "^1.30.0"
|
||||
},
|
||||
"keywords": [
|
||||
"angular",
|
||||
"code",
|
||||
"syntax-highlighting",
|
||||
"prism"
|
||||
]
|
||||
}`;
|
||||
|
||||
readonly htmlCode = `<div class="card">
|
||||
<header class="card-header">
|
||||
<h2>Card Title</h2>
|
||||
<button class="close-btn">×</button>
|
||||
</header>
|
||||
|
||||
<div class="card-body">
|
||||
<p>This is the card content.</p>
|
||||
<a href="#" class="btn btn-primary">Action</a>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
setTheme(theme: CodeTheme): void {
|
||||
this.themeService.setTheme(theme);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-xl;
|
||||
|
||||
h3 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-layout-section-md;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
@media (max-width: calc(#{$semantic-breakpoint-md} - 1px)) {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-layout-section-sm;
|
||||
|
||||
> * {
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: $semantic-spacing-layout-section-md;
|
||||
|
||||
@media (max-width: calc(#{$semantic-breakpoint-sm} - 1px)) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
|
||||
p {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
border: $semantic-border-width-2 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
margin-top: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-form {
|
||||
.demo-form-info {
|
||||
margin-top: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-container;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
|
||||
pre {
|
||||
font-family: $semantic-typography-font-family-mono;
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
color: $semantic-color-text-secondary;
|
||||
background: $semantic-color-surface-primary;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-theme-preview {
|
||||
margin-top: $semantic-spacing-component-lg;
|
||||
|
||||
.theme-sample {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
text-align: center;
|
||||
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);
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.event-log {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
|
||||
.event-entry {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
font-family: $semantic-typography-font-family-mono;
|
||||
font-size: $semantic-typography-font-size-sm;
|
||||
transition: background-color $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&--recent {
|
||||
background-color: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-focus;
|
||||
}
|
||||
|
||||
.event-type {
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
color: $semantic-color-primary;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.event-value {
|
||||
flex: 1;
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0 $semantic-spacing-component-sm;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.event-time {
|
||||
color: $semantic-color-text-tertiary;
|
||||
font-size: $semantic-typography-font-size-xs;
|
||||
min-width: 80px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clear-log-btn {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
background: $semantic-color-danger;
|
||||
color: $semantic-color-on-danger;
|
||||
border: none;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
font-family: map-get($semantic-typography-button-small, font-family);
|
||||
font-size: map-get($semantic-typography-button-small, font-size);
|
||||
font-weight: map-get($semantic-typography-button-small, font-weight);
|
||||
line-height: map-get($semantic-typography-button-small, line-height);
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: $semantic-shadow-button-rest;
|
||||
}
|
||||
}
|
||||
|
||||
.preset-colors {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
|
||||
.preset-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(40px, 1fr));
|
||||
gap: $semantic-spacing-component-sm;
|
||||
max-width: 400px;
|
||||
margin-top: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.preset-color-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: $semantic-border-width-2 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: $semantic-shadow-elevation-3;
|
||||
border-color: $semantic-color-border-focus;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(1.05);
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: calc(#{$semantic-breakpoint-md} - 1px)) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.preset-colors {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: calc(#{$semantic-breakpoint-sm} - 1px)) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-xs;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.preset-colors {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
|
||||
.preset-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(35px, 1fr));
|
||||
gap: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
.preset-color-btn {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
.event-log {
|
||||
.event-entry {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
|
||||
.event-type,
|
||||
.event-value,
|
||||
.event-time {
|
||||
min-width: unset;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule, ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms';
|
||||
import { ColorPickerComponent } from '../../../../../ui-essentials/src/lib/components/forms/color-picker/color-picker.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-color-picker-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ColorPickerComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Color Picker Component Showcase</h2>
|
||||
|
||||
<!-- Basic Color Picker -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Color Picker</h3>
|
||||
<div class="demo-row">
|
||||
<ui-color-picker
|
||||
label="Choose a color"
|
||||
description="Select your preferred color"
|
||||
[(ngModel)]="basicColor"
|
||||
(colorChange)="onColorChange('basic', $event)"
|
||||
/>
|
||||
<div class="demo-info">
|
||||
<p><strong>Selected:</strong> {{ basicColor() }}</p>
|
||||
<div class="color-preview" [style.background-color]="basicColor()"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<ui-color-picker
|
||||
label="Small (sm)"
|
||||
size="sm"
|
||||
[(ngModel)]="sizeSmall"
|
||||
(colorChange)="onColorChange('size-sm', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<ui-color-picker
|
||||
label="Medium (md)"
|
||||
size="md"
|
||||
[(ngModel)]="sizeMedium"
|
||||
(colorChange)="onColorChange('size-md', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<ui-color-picker
|
||||
label="Large (lg)"
|
||||
size="lg"
|
||||
[(ngModel)]="sizeLarge"
|
||||
(colorChange)="onColorChange('size-lg', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Color Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Color Variants</h3>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<ui-color-picker
|
||||
label="Primary"
|
||||
variant="primary"
|
||||
[(ngModel)]="variantPrimary"
|
||||
(colorChange)="onColorChange('variant-primary', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<ui-color-picker
|
||||
label="Secondary"
|
||||
variant="secondary"
|
||||
[(ngModel)]="variantSecondary"
|
||||
(colorChange)="onColorChange('variant-secondary', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<ui-color-picker
|
||||
label="Success"
|
||||
variant="success"
|
||||
[(ngModel)]="variantSuccess"
|
||||
(colorChange)="onColorChange('variant-success', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<ui-color-picker
|
||||
label="Danger"
|
||||
variant="danger"
|
||||
[(ngModel)]="variantDanger"
|
||||
(colorChange)="onColorChange('variant-danger', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-grid">
|
||||
<div class="demo-item">
|
||||
<ui-color-picker
|
||||
label="Default State"
|
||||
[(ngModel)]="stateDefault"
|
||||
(colorChange)="onColorChange('state-default', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<ui-color-picker
|
||||
label="Error State"
|
||||
state="error"
|
||||
description="This field has an error"
|
||||
[(ngModel)]="stateError"
|
||||
(colorChange)="onColorChange('state-error', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<ui-color-picker
|
||||
label="Disabled State"
|
||||
[disabled]="true"
|
||||
[(ngModel)]="stateDisabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Required Field -->
|
||||
<section class="demo-section">
|
||||
<h3>Required Field</h3>
|
||||
<div class="demo-row">
|
||||
<ui-color-picker
|
||||
label="Brand Color"
|
||||
description="Choose your brand's primary color"
|
||||
[required]="true"
|
||||
[(ngModel)]="requiredColor"
|
||||
(colorChange)="onColorChange('required', $event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Reactive Forms Integration -->
|
||||
<section class="demo-section">
|
||||
<h3>Reactive Forms Integration</h3>
|
||||
<form [formGroup]="colorForm" class="demo-form">
|
||||
<div class="demo-row">
|
||||
<ui-color-picker
|
||||
label="Background Color"
|
||||
description="Select background color for your theme"
|
||||
formControlName="backgroundColor"
|
||||
(colorChange)="onColorChange('form-bg', $event)"
|
||||
/>
|
||||
<ui-color-picker
|
||||
label="Text Color"
|
||||
description="Select text color for your theme"
|
||||
formControlName="textColor"
|
||||
(colorChange)="onColorChange('form-text', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="demo-form-info">
|
||||
<h4>Form Values:</h4>
|
||||
<pre>{{ getFormValues() }}</pre>
|
||||
</div>
|
||||
<div class="demo-theme-preview">
|
||||
<h4>Theme Preview:</h4>
|
||||
<div
|
||||
class="theme-sample"
|
||||
[style.background-color]="colorForm.get('backgroundColor')?.value || '#FFFFFF'"
|
||||
[style.color]="colorForm.get('textColor')?.value || '#000000'">
|
||||
Sample text with selected colors
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Demo -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Demo</h3>
|
||||
<div class="demo-row">
|
||||
<ui-color-picker
|
||||
label="Interactive Color"
|
||||
description="Watch the events in the console"
|
||||
[(ngModel)]="interactiveColor"
|
||||
(colorChange)="onInteractiveColorChange($event)"
|
||||
(colorFocus)="onColorFocus($event)"
|
||||
(colorBlur)="onColorBlur($event)"
|
||||
/>
|
||||
<div class="demo-info">
|
||||
<h4>Event Log:</h4>
|
||||
<div class="event-log">
|
||||
@for (event of eventLog(); track event.id) {
|
||||
<div class="event-entry" [class.event-entry--recent]="event.recent">
|
||||
<span class="event-type">{{ event.type }}</span>
|
||||
<span class="event-value">{{ event.value }}</span>
|
||||
<span class="event-time">{{ event.time }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<button class="clear-log-btn" (click)="clearEventLog()">Clear Log</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Preset Colors Demo -->
|
||||
<section class="demo-section">
|
||||
<h3>Preset Colors</h3>
|
||||
<div class="demo-row">
|
||||
<ui-color-picker
|
||||
label="Select or enter color"
|
||||
[(ngModel)]="presetColor"
|
||||
(colorChange)="onColorChange('preset', $event)"
|
||||
/>
|
||||
<div class="preset-colors">
|
||||
<h4>Quick Colors:</h4>
|
||||
<div class="preset-grid">
|
||||
@for (color of presetColors; track color.name) {
|
||||
<button
|
||||
class="preset-color-btn"
|
||||
[style.background-color]="color.hex"
|
||||
[attr.title]="color.name + ' (' + color.hex + ')'"
|
||||
(click)="selectPresetColor(color.hex)">
|
||||
<span class="sr-only">{{ color.name }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './color-picker-demo.component.scss'
|
||||
})
|
||||
export class ColorPickerDemoComponent {
|
||||
// Basic demo colors
|
||||
basicColor = signal<string>('#3498db');
|
||||
|
||||
// Size variants
|
||||
sizeSmall = signal<string>('#e74c3c');
|
||||
sizeMedium = signal<string>('#2ecc71');
|
||||
sizeLarge = signal<string>('#f39c12');
|
||||
|
||||
// Color variants
|
||||
variantPrimary = signal<string>('#3498db');
|
||||
variantSecondary = signal<string>('#95a5a6');
|
||||
variantSuccess = signal<string>('#2ecc71');
|
||||
variantDanger = signal<string>('#e74c3c');
|
||||
|
||||
// States
|
||||
stateDefault = signal<string>('#9b59b6');
|
||||
stateError = signal<string>('#e74c3c');
|
||||
stateDisabled = signal<string>('#bdc3c7');
|
||||
|
||||
// Required field
|
||||
requiredColor = signal<string>('#1abc9c');
|
||||
|
||||
// Interactive demo
|
||||
interactiveColor = signal<string>('#34495e');
|
||||
eventLog = signal<Array<{id: number, type: string, value: string, time: string, recent: boolean}>>([]);
|
||||
private eventIdCounter = 0;
|
||||
|
||||
// Preset colors
|
||||
presetColor = signal<string>('#ff6b6b');
|
||||
presetColors = [
|
||||
{ name: 'Red', hex: '#e74c3c' },
|
||||
{ name: 'Orange', hex: '#f39c12' },
|
||||
{ name: 'Yellow', hex: '#f1c40f' },
|
||||
{ name: 'Green', hex: '#2ecc71' },
|
||||
{ name: 'Blue', hex: '#3498db' },
|
||||
{ name: 'Purple', hex: '#9b59b6' },
|
||||
{ name: 'Pink', hex: '#e91e63' },
|
||||
{ name: 'Teal', hex: '#1abc9c' },
|
||||
{ name: 'Gray', hex: '#95a5a6' },
|
||||
{ name: 'Black', hex: '#2c3e50' },
|
||||
{ name: 'White', hex: '#ecf0f1' },
|
||||
{ name: 'Brown', hex: '#8d4925' }
|
||||
];
|
||||
|
||||
// Reactive forms
|
||||
colorForm = new FormGroup({
|
||||
backgroundColor: new FormControl('#ffffff'),
|
||||
textColor: new FormControl('#333333')
|
||||
});
|
||||
|
||||
onColorChange(source: string, color: string): void {
|
||||
console.log(`Color changed (${source}):`, color);
|
||||
}
|
||||
|
||||
onInteractiveColorChange(color: string): void {
|
||||
this.addEventToLog('change', color);
|
||||
}
|
||||
|
||||
onColorFocus(event: FocusEvent): void {
|
||||
this.addEventToLog('focus', 'Color picker focused');
|
||||
}
|
||||
|
||||
onColorBlur(event: FocusEvent): void {
|
||||
this.addEventToLog('blur', 'Color picker blurred');
|
||||
}
|
||||
|
||||
private addEventToLog(type: string, value: string): void {
|
||||
const newEvent = {
|
||||
id: this.eventIdCounter++,
|
||||
type,
|
||||
value,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
recent: true
|
||||
};
|
||||
|
||||
const currentLog = this.eventLog();
|
||||
const updatedLog = [newEvent, ...currentLog.slice(0, 9)].map((event, index) => ({
|
||||
...event,
|
||||
recent: index === 0
|
||||
}));
|
||||
|
||||
this.eventLog.set(updatedLog);
|
||||
|
||||
// Remove recent flag after animation
|
||||
setTimeout(() => {
|
||||
const logWithoutRecent = this.eventLog().map(event => ({
|
||||
...event,
|
||||
recent: false
|
||||
}));
|
||||
this.eventLog.set(logWithoutRecent);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
clearEventLog(): void {
|
||||
this.eventLog.set([]);
|
||||
this.eventIdCounter = 0;
|
||||
}
|
||||
|
||||
selectPresetColor(color: string): void {
|
||||
this.presetColor.set(color);
|
||||
}
|
||||
|
||||
getFormValues(): string {
|
||||
return JSON.stringify(this.colorForm.value, null, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
.demo-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
color: #555;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: 1rem;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
min-height: 200px;
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.6;
|
||||
text-align: justify;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 1rem 0;
|
||||
padding-left: 1.5rem;
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-note {
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #90caf9;
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #1565c0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
|
||||
&:has(input[type="checkbox"]) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-select {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
font-size: 0.9rem;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #2196f3;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
&:has(input[type="checkbox"]) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ColumnComponent } from '../../../../../ui-essentials/src/lib/components/layout/column';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-column-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, ColumnComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Column Layout Demo</h2>
|
||||
|
||||
<!-- Column Count Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Column Counts</h3>
|
||||
<div class="demo-row">
|
||||
@for (count of columnCounts; track count) {
|
||||
<div class="demo-card">
|
||||
<h4>{{ count }} Column{{ count !== '1' ? 's' : '' }}</h4>
|
||||
<ui-column [count]="count" [gap]="'md'" class="demo-content">
|
||||
<p>{{ sampleText }}</p>
|
||||
</ui-column>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gap Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Gap Sizes (2 Columns)</h3>
|
||||
<div class="demo-row">
|
||||
@for (gap of gaps; track gap) {
|
||||
<div class="demo-card">
|
||||
<h4>Gap: {{ gap }}</h4>
|
||||
<ui-column [count]="'2'" [gap]="gap" class="demo-content">
|
||||
<p>{{ shortText }}</p>
|
||||
</ui-column>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Rule Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Column Rules (3 Columns)</h3>
|
||||
<div class="demo-row">
|
||||
@for (rule of rules; track rule) {
|
||||
<div class="demo-card">
|
||||
<h4>Rule: {{ rule }}</h4>
|
||||
<ui-column [count]="'3'" [gap]="'lg'" [rule]="rule" [ruleWidth]="'2'" class="demo-content">
|
||||
<p>{{ mediumText }}</p>
|
||||
</ui-column>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Fill Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Fill Types (3 Columns)</h3>
|
||||
<div class="demo-row">
|
||||
@for (fill of fills; track fill) {
|
||||
<div class="demo-card">
|
||||
<h4>Fill: {{ fill }}</h4>
|
||||
<ui-column [count]="'3'" [gap]="'md'" [fill]="fill" class="demo-content">
|
||||
<p>{{ variableText }}</p>
|
||||
</ui-column>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Responsive Behavior -->
|
||||
<section class="demo-section">
|
||||
<h3>Responsive Layout</h3>
|
||||
<p class="demo-note">Resize your browser to see responsive column behavior</p>
|
||||
<div class="demo-card">
|
||||
<h4>Responsive 4-Column Layout</h4>
|
||||
<ui-column [count]="'4'" [gap]="'md'" [responsive]="true" [rule]="'solid'" class="demo-content">
|
||||
<h5>Section 1</h5>
|
||||
<p>{{ sampleText }}</p>
|
||||
<h5>Section 2</h5>
|
||||
<p>{{ shortText }}</p>
|
||||
<h5>Section 3</h5>
|
||||
<p>{{ mediumText }}</p>
|
||||
<h5>Section 4</h5>
|
||||
<p>{{ variableText }}</p>
|
||||
</ui-column>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Real-world Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Article Layout Example</h3>
|
||||
<div class="demo-card">
|
||||
<ui-column [count]="'2'" [gap]="'xl'" [rule]="'solid'" [ruleWidth]="'1'" class="demo-content">
|
||||
<h4>The Future of Web Development</h4>
|
||||
<p>{{ longText }}</p>
|
||||
<p>{{ mediumText }}</p>
|
||||
<h5>Key Benefits</h5>
|
||||
<ul>
|
||||
<li>Improved readability through better text flow</li>
|
||||
<li>Enhanced visual hierarchy with column structures</li>
|
||||
<li>Responsive design that adapts to screen sizes</li>
|
||||
<li>Better content organization for long-form text</li>
|
||||
</ul>
|
||||
<p>{{ shortText }}</p>
|
||||
</ui-column>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Controls -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Example</h3>
|
||||
<div class="demo-controls">
|
||||
<label>
|
||||
Columns:
|
||||
<select [(ngModel)]="selectedCount" class="demo-select">
|
||||
@for (count of columnCounts; track count) {
|
||||
<option [value]="count">{{ count }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Gap:
|
||||
<select [(ngModel)]="selectedGap" class="demo-select">
|
||||
@for (gap of gaps; track gap) {
|
||||
<option [value]="gap">{{ gap }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Rule:
|
||||
<select [(ngModel)]="selectedRule" class="demo-select">
|
||||
@for (rule of rules; track rule) {
|
||||
<option [value]="rule">{{ rule }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="selectedResponsive">
|
||||
Responsive
|
||||
</label>
|
||||
</div>
|
||||
<div class="demo-card">
|
||||
<ui-column
|
||||
[count]="selectedCount"
|
||||
[gap]="selectedGap"
|
||||
[rule]="selectedRule"
|
||||
[ruleWidth]="'2'"
|
||||
[responsive]="selectedResponsive"
|
||||
class="demo-content">
|
||||
<h4>Interactive Column Layout</h4>
|
||||
<p>{{ sampleText }}</p>
|
||||
<p>{{ mediumText }}</p>
|
||||
<p>{{ shortText }}</p>
|
||||
</ui-column>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './column-demo.component.scss'
|
||||
})
|
||||
export class ColumnDemoComponent {
|
||||
columnCounts = ['1', '2', '3', '4', '5', '6'] as const;
|
||||
gaps = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
|
||||
rules = ['none', 'solid', 'dashed', 'dotted'] as const;
|
||||
fills = ['auto', 'balance', 'balance-all'] as const;
|
||||
|
||||
// Interactive controls
|
||||
selectedCount = '3' as const;
|
||||
selectedGap = 'md' as const;
|
||||
selectedRule = 'solid' as const;
|
||||
selectedResponsive = true;
|
||||
|
||||
// Sample text content
|
||||
shortText = "This is a short paragraph to demonstrate column layout behavior with minimal text content.";
|
||||
|
||||
mediumText = "This is a medium-length paragraph that provides a good example of how text flows within column layouts. It contains enough content to show wrapping and distribution across multiple columns while remaining readable.";
|
||||
|
||||
sampleText = "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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.";
|
||||
|
||||
longText = "Web development has evolved significantly over the past decade, with new frameworks, tools, and methodologies emerging to help developers create more efficient and maintainable applications. Modern web development practices emphasize component-based architecture, responsive design, and accessibility as core principles. The adoption of TypeScript has brought static typing to JavaScript, reducing runtime errors and improving developer productivity. CSS Grid and Flexbox have revolutionized layout design, making it easier to create complex, responsive layouts without relying on external frameworks.";
|
||||
|
||||
variableText = "This text demonstrates variable length content. Some paragraphs are longer, others shorter. This variation helps test how well the column layout handles different content lengths and maintains visual balance across columns.";
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-lg;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: $semantic-spacing-layout-section-xl;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
margin: 0 0 $semantic-spacing-component-md 0;
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
.demo-title-icon {
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
margin: 0;
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
|
||||
kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px $semantic-spacing-component-xs;
|
||||
margin: 0 2px;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
font-family: map-get($semantic-typography-caption, font-family);
|
||||
font-size: map-get($semantic-typography-caption, font-size);
|
||||
font-weight: map-get($semantic-typography-caption, font-weight);
|
||||
line-height: map-get($semantic-typography-caption, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-xl;
|
||||
}
|
||||
|
||||
.demo-section-title {
|
||||
margin: 0 0 $semantic-spacing-component-lg 0;
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-actions {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
border: $semantic-border-width-1 solid transparent;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
background: transparent;
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
text-decoration: none;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-primary;
|
||||
|
||||
&:hover {
|
||||
background: darken($semantic-color-primary, 10%);
|
||||
border-color: darken($semantic-color-primary, 10%);
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
border-color: $semantic-color-secondary;
|
||||
|
||||
&:hover {
|
||||
background: darken($semantic-color-secondary, 10%);
|
||||
border-color: darken($semantic-color-secondary, 10%);
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&--outline {
|
||||
color: $semantic-color-text-primary;
|
||||
border-color: $semantic-color-border-primary;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-color: $semantic-color-border-secondary;
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: $semantic-spacing-component-lg;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-config-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
.demo-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
font-family: map-get($semantic-typography-label, font-family);
|
||||
font-size: map-get($semantic-typography-label, font-size);
|
||||
font-weight: map-get($semantic-typography-label, font-weight);
|
||||
line-height: map-get($semantic-typography-label, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.demo-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: $semantic-color-primary;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.demo-input,
|
||||
.demo-select {
|
||||
padding: $semantic-spacing-interactive-input-padding-y $semantic-spacing-interactive-input-padding-x;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
background: $semantic-color-surface-primary;
|
||||
font-family: map-get($semantic-typography-input, font-family);
|
||||
font-size: map-get($semantic-typography-input, font-size);
|
||||
font-weight: map-get($semantic-typography-input, font-weight);
|
||||
line-height: map-get($semantic-typography-input, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
transition: border-color $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $semantic-color-focus;
|
||||
box-shadow: 0 0 0 2px rgba($semantic-color-focus, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.demo-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-feature {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
background: $semantic-color-surface-primary;
|
||||
text-align: center;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
border-color: $semantic-color-primary;
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: $semantic-spacing-component-sm 0 $semantic-spacing-component-xs 0;
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-feature-icon {
|
||||
font-size: 2rem;
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
.demo-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-stat {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
background: $semantic-color-surface-primary;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.demo-stat-value {
|
||||
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);
|
||||
color: $semantic-color-primary;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
.demo-stat-label {
|
||||
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-secondary;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.demo-shortcuts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-shortcut {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
background: $semantic-color-surface-primary;
|
||||
|
||||
span {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
padding: 0 $semantic-spacing-component-xs;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
font-family: map-get($semantic-typography-caption, font-family);
|
||||
font-size: map-get($semantic-typography-caption, font-size);
|
||||
font-weight: map-get($semantic-typography-caption, font-weight);
|
||||
line-height: map-get($semantic-typography-caption, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// Toast animation styles
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: calc(#{$semantic-breakpoint-md} - 1px)) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.demo-config-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-features {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-stats {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.demo-shortcuts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: calc(#{$semantic-breakpoint-sm} - 1px)) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
|
||||
.demo-title-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
import { Component, OnInit, OnDestroy, ViewChild, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import {
|
||||
faHome,
|
||||
faCog,
|
||||
faUser,
|
||||
faSearch,
|
||||
faFile,
|
||||
faEdit,
|
||||
faEye,
|
||||
faTools,
|
||||
faQuestion,
|
||||
faPlus,
|
||||
faSave,
|
||||
faCopy,
|
||||
faPaste,
|
||||
faUndo,
|
||||
faRedo,
|
||||
faKeyboard,
|
||||
faPalette,
|
||||
faRocket
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { Command, CommandCategory, CommandExecutionContext, CommandPaletteComponent, CommandPaletteService, createShortcuts, GlobalKeyboardDirective } from '../../../../../ui-essentials/src/lib/components/overlays/command-palette';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'ui-command-palette-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FontAwesomeModule,
|
||||
CommandPaletteComponent,
|
||||
GlobalKeyboardDirective
|
||||
],
|
||||
template: `
|
||||
<div class="demo-container" uiGlobalKeyboard [shortcuts]="globalShortcuts" (shortcutTriggered)="handleGlobalShortcut($event)">
|
||||
<div class="demo-header">
|
||||
<h2 class="demo-title">
|
||||
<fa-icon [icon]="faPalette" class="demo-title-icon"></fa-icon>
|
||||
Command Palette Demo
|
||||
</h2>
|
||||
<p class="demo-description">
|
||||
A powerful command palette for quick navigation and actions. Press <kbd>Ctrl+K</kbd> (or <kbd>⌘K</kbd> on Mac) to open.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Quick Start Section -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section-title">Quick Start</h3>
|
||||
<div class="demo-actions">
|
||||
<button
|
||||
class="demo-button demo-button--primary"
|
||||
(click)="openCommandPalette()"
|
||||
>
|
||||
<fa-icon [icon]="faKeyboard"></fa-icon>
|
||||
Open Command Palette
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="demo-button demo-button--secondary"
|
||||
(click)="registerSampleCommands()"
|
||||
>
|
||||
<fa-icon [icon]="faPlus"></fa-icon>
|
||||
Add Sample Commands
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="demo-button demo-button--outline"
|
||||
(click)="clearCommands()"
|
||||
>
|
||||
Clear Commands
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Configuration Section -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section-title">Configuration</h3>
|
||||
<div class="demo-config-grid">
|
||||
<div class="demo-config-item">
|
||||
<label class="demo-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="config.showCategories"
|
||||
(change)="updateConfig('showCategories', $event)"
|
||||
class="demo-checkbox"
|
||||
/>
|
||||
Show Categories
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="demo-config-item">
|
||||
<label class="demo-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="config.showShortcuts"
|
||||
(change)="updateConfig('showShortcuts', $event)"
|
||||
class="demo-checkbox"
|
||||
/>
|
||||
Show Shortcuts
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="demo-config-item">
|
||||
<label class="demo-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="config.showRecent"
|
||||
(change)="updateConfig('showRecent', $event)"
|
||||
class="demo-checkbox"
|
||||
/>
|
||||
Show Recent Commands
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="demo-config-item">
|
||||
<label class="demo-label">
|
||||
Max Results:
|
||||
<input
|
||||
type="number"
|
||||
[value]="config.maxResults"
|
||||
(input)="updateConfig('maxResults', $event)"
|
||||
class="demo-input"
|
||||
min="1"
|
||||
max="50"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="demo-config-item">
|
||||
<label class="demo-label">
|
||||
Recent Limit:
|
||||
<input
|
||||
type="number"
|
||||
[value]="config.recentLimit"
|
||||
(input)="updateConfig('recentLimit', $event)"
|
||||
class="demo-input"
|
||||
min="1"
|
||||
max="20"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="demo-config-item">
|
||||
<label class="demo-label">
|
||||
Size:
|
||||
<select
|
||||
[value]="paletteSize"
|
||||
(change)="updatePaletteSize($event)"
|
||||
class="demo-select"
|
||||
>
|
||||
<option value="md">Medium</option>
|
||||
<option value="lg">Large</option>
|
||||
<option value="xl">Extra Large</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section-title">Features</h3>
|
||||
<div class="demo-features">
|
||||
<div class="demo-feature">
|
||||
<fa-icon [icon]="faKeyboard" class="demo-feature-icon"></fa-icon>
|
||||
<h4>Keyboard Navigation</h4>
|
||||
<p>Full keyboard support with arrow keys, Enter, and Escape</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-feature">
|
||||
<fa-icon [icon]="faSearch" class="demo-feature-icon"></fa-icon>
|
||||
<h4>Fuzzy Search</h4>
|
||||
<p>Intelligent search with typo tolerance and keyword matching</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-feature">
|
||||
<fa-icon [icon]="faRocket" class="demo-feature-icon"></fa-icon>
|
||||
<h4>Quick Actions</h4>
|
||||
<p>Execute commands instantly with global keyboard shortcuts</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-feature">
|
||||
<fa-icon [icon]="faCog" class="demo-feature-icon"></fa-icon>
|
||||
<h4>Categorization</h4>
|
||||
<p>Organize commands by category for better discoverability</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-feature">
|
||||
<fa-icon [icon]="faCopy" class="demo-feature-icon"></fa-icon>
|
||||
<h4>Recent Commands</h4>
|
||||
<p>Track and show recently used commands for quick access</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-feature">
|
||||
<fa-icon [icon]="faEye" class="demo-feature-icon"></fa-icon>
|
||||
<h4>Context Aware</h4>
|
||||
<p>Commands can be shown or hidden based on application state</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Statistics -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section-title">Usage Statistics</h3>
|
||||
<div class="demo-stats">
|
||||
<div class="demo-stat">
|
||||
<div class="demo-stat-value">{{ registeredCommandsCount }}</div>
|
||||
<div class="demo-stat-label">Registered Commands</div>
|
||||
</div>
|
||||
<div class="demo-stat">
|
||||
<div class="demo-stat-value">{{ recentCommandsCount }}</div>
|
||||
<div class="demo-stat-label">Recent Commands</div>
|
||||
</div>
|
||||
<div class="demo-stat">
|
||||
<div class="demo-stat-value">{{ executionCount }}</div>
|
||||
<div class="demo-stat-label">Commands Executed</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Keyboard Shortcuts Guide -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section-title">Keyboard Shortcuts</h3>
|
||||
<div class="demo-shortcuts">
|
||||
<div class="demo-shortcut">
|
||||
<kbd class="demo-kbd">Ctrl</kbd> + <kbd class="demo-kbd">K</kbd>
|
||||
<span>Open Command Palette</span>
|
||||
</div>
|
||||
<div class="demo-shortcut">
|
||||
<kbd class="demo-kbd">↑</kbd> / <kbd class="demo-kbd">↓</kbd>
|
||||
<span>Navigate Results</span>
|
||||
</div>
|
||||
<div class="demo-shortcut">
|
||||
<kbd class="demo-kbd">Enter</kbd>
|
||||
<span>Execute Selected Command</span>
|
||||
</div>
|
||||
<div class="demo-shortcut">
|
||||
<kbd class="demo-kbd">Esc</kbd>
|
||||
<span>Close Palette</span>
|
||||
</div>
|
||||
<div class="demo-shortcut">
|
||||
<kbd class="demo-kbd">Tab</kbd>
|
||||
<span>Navigate Results (Alternative)</span>
|
||||
</div>
|
||||
<div class="demo-shortcut">
|
||||
<kbd class="demo-kbd">/</kbd>
|
||||
<span>Quick Search</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Command Palette Component -->
|
||||
<ui-command-palette
|
||||
#commandPalette
|
||||
[size]="paletteSize"
|
||||
[showFooter]="true"
|
||||
[autoFocus]="true"
|
||||
(opened)="onPaletteOpened()"
|
||||
(closed)="onPaletteClosed()"
|
||||
(commandExecuted)="onCommandExecuted($event)"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './command-palette-demo.component.scss'
|
||||
})
|
||||
export class CommandPaletteDemoComponent implements OnInit, OnDestroy {
|
||||
@ViewChild('commandPalette') commandPalette!: CommandPaletteComponent;
|
||||
|
||||
private router = inject(Router);
|
||||
private commandService = inject(CommandPaletteService);
|
||||
|
||||
// Icons
|
||||
readonly faPalette = faPalette;
|
||||
readonly faKeyboard = faKeyboard;
|
||||
readonly faPlus = faPlus;
|
||||
readonly faSearch = faSearch;
|
||||
readonly faRocket = faRocket;
|
||||
readonly faCog = faCog;
|
||||
readonly faCopy = faCopy;
|
||||
readonly faEye = faEye;
|
||||
readonly faHome = faHome;
|
||||
readonly faUser = faUser;
|
||||
readonly faFile = faFile;
|
||||
readonly faEdit = faEdit;
|
||||
readonly faTools = faTools;
|
||||
readonly faQuestion = faQuestion;
|
||||
readonly faSave = faSave;
|
||||
readonly faUndo = faUndo;
|
||||
readonly faRedo = faRedo;
|
||||
readonly faPaste = faPaste;
|
||||
|
||||
// Component state
|
||||
paletteSize: 'md' | 'lg' | 'xl' = 'lg';
|
||||
executionCount = 0;
|
||||
|
||||
config = {
|
||||
showCategories: true,
|
||||
showShortcuts: true,
|
||||
showRecent: true,
|
||||
maxResults: 20,
|
||||
recentLimit: 5
|
||||
};
|
||||
|
||||
globalShortcuts = [
|
||||
createShortcuts().commandPalette(),
|
||||
createShortcuts().quickSearch(),
|
||||
createShortcuts().create('/', { preventDefault: true })
|
||||
];
|
||||
|
||||
get registeredCommandsCount(): number {
|
||||
return this.commandService.commands().length;
|
||||
}
|
||||
|
||||
get recentCommandsCount(): number {
|
||||
return this.commandService.recentCommands().length;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.registerSampleCommands();
|
||||
this.applyConfig();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Clean up registered commands
|
||||
this.commandService.clearCommands();
|
||||
}
|
||||
|
||||
openCommandPalette(): void {
|
||||
this.commandPalette.open();
|
||||
}
|
||||
|
||||
handleGlobalShortcut(event: any): void {
|
||||
const shortcut = event.shortcut;
|
||||
|
||||
if (shortcut.key === 'k' && (shortcut.ctrlKey || shortcut.metaKey)) {
|
||||
this.commandPalette.toggle();
|
||||
} else if (shortcut.key === '/') {
|
||||
this.commandPalette.open();
|
||||
}
|
||||
}
|
||||
|
||||
registerSampleCommands(): void {
|
||||
const sampleCommands: Command[] = [
|
||||
// Navigation Commands
|
||||
{
|
||||
id: 'nav-home',
|
||||
title: 'Go Home',
|
||||
description: 'Navigate to the home page',
|
||||
icon: faHome,
|
||||
category: CommandCategory.NAVIGATION,
|
||||
keywords: ['home', 'dashboard', 'main'],
|
||||
shortcut: ['Ctrl', 'H'],
|
||||
handler: () => this.showToast('Navigating to home...'),
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'nav-profile',
|
||||
title: 'View Profile',
|
||||
description: 'Go to user profile page',
|
||||
icon: faUser,
|
||||
category: CommandCategory.NAVIGATION,
|
||||
keywords: ['profile', 'user', 'account'],
|
||||
handler: () => this.showToast('Opening profile...'),
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
id: 'nav-settings',
|
||||
title: 'Open Settings',
|
||||
description: 'Access application settings',
|
||||
icon: faCog,
|
||||
category: CommandCategory.SETTINGS,
|
||||
keywords: ['settings', 'preferences', 'config'],
|
||||
shortcut: ['Ctrl', ','],
|
||||
handler: () => this.showToast('Opening settings...'),
|
||||
order: 1
|
||||
},
|
||||
|
||||
// File Commands
|
||||
{
|
||||
id: 'file-new',
|
||||
title: 'New File',
|
||||
description: 'Create a new file',
|
||||
icon: faFile,
|
||||
category: CommandCategory.FILE,
|
||||
keywords: ['new', 'create', 'file', 'document'],
|
||||
shortcut: ['Ctrl', 'N'],
|
||||
handler: () => this.showToast('Creating new file...'),
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'file-save',
|
||||
title: 'Save File',
|
||||
description: 'Save the current file',
|
||||
icon: faSave,
|
||||
category: CommandCategory.FILE,
|
||||
keywords: ['save', 'file', 'document'],
|
||||
shortcut: ['Ctrl', 'S'],
|
||||
handler: () => this.showToast('Saving file...'),
|
||||
order: 2
|
||||
},
|
||||
|
||||
// Edit Commands
|
||||
{
|
||||
id: 'edit-copy',
|
||||
title: 'Copy',
|
||||
description: 'Copy selected content',
|
||||
icon: faCopy,
|
||||
category: CommandCategory.EDIT,
|
||||
keywords: ['copy', 'clipboard'],
|
||||
shortcut: ['Ctrl', 'C'],
|
||||
handler: () => this.showToast('Content copied!'),
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'edit-paste',
|
||||
title: 'Paste',
|
||||
description: 'Paste from clipboard',
|
||||
icon: faPaste,
|
||||
category: CommandCategory.EDIT,
|
||||
keywords: ['paste', 'clipboard'],
|
||||
shortcut: ['Ctrl', 'V'],
|
||||
handler: () => this.showToast('Content pasted!'),
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
id: 'edit-undo',
|
||||
title: 'Undo',
|
||||
description: 'Undo last action',
|
||||
icon: faUndo,
|
||||
category: CommandCategory.EDIT,
|
||||
keywords: ['undo', 'revert'],
|
||||
shortcut: ['Ctrl', 'Z'],
|
||||
handler: () => this.showToast('Action undone!'),
|
||||
order: 3
|
||||
},
|
||||
{
|
||||
id: 'edit-redo',
|
||||
title: 'Redo',
|
||||
description: 'Redo last undone action',
|
||||
icon: faRedo,
|
||||
category: CommandCategory.EDIT,
|
||||
keywords: ['redo', 'repeat'],
|
||||
shortcut: ['Ctrl', 'Y'],
|
||||
handler: () => this.showToast('Action redone!'),
|
||||
order: 4
|
||||
},
|
||||
|
||||
// Search Commands
|
||||
{
|
||||
id: 'search-global',
|
||||
title: 'Global Search',
|
||||
description: 'Search across all content',
|
||||
icon: faSearch,
|
||||
category: CommandCategory.SEARCH,
|
||||
keywords: ['search', 'find', 'global'],
|
||||
shortcut: ['Ctrl', 'Shift', 'F'],
|
||||
handler: () => this.showToast('Opening global search...'),
|
||||
order: 1
|
||||
},
|
||||
|
||||
// Tools Commands
|
||||
{
|
||||
id: 'tools-theme',
|
||||
title: 'Toggle Theme',
|
||||
description: 'Switch between light and dark theme',
|
||||
icon: faTools,
|
||||
category: CommandCategory.TOOLS,
|
||||
keywords: ['theme', 'dark', 'light', 'toggle'],
|
||||
handler: () => this.showToast('Theme toggled!'),
|
||||
order: 1
|
||||
},
|
||||
|
||||
// Help Commands
|
||||
{
|
||||
id: 'help-docs',
|
||||
title: 'Documentation',
|
||||
description: 'Open help documentation',
|
||||
icon: faQuestion,
|
||||
category: CommandCategory.HELP,
|
||||
keywords: ['help', 'docs', 'documentation'],
|
||||
handler: () => this.showToast('Opening documentation...'),
|
||||
order: 1
|
||||
},
|
||||
|
||||
// Action Commands
|
||||
{
|
||||
id: 'action-refresh',
|
||||
title: 'Refresh Page',
|
||||
description: 'Reload the current page',
|
||||
icon: faRocket,
|
||||
category: CommandCategory.ACTIONS,
|
||||
keywords: ['refresh', 'reload', 'update'],
|
||||
shortcut: ['F5'],
|
||||
handler: () => this.showToast('Page refreshed!'),
|
||||
order: 1
|
||||
}
|
||||
];
|
||||
|
||||
this.commandService.registerCommands(sampleCommands);
|
||||
}
|
||||
|
||||
clearCommands(): void {
|
||||
this.commandService.clearCommands();
|
||||
this.commandService.clearRecentCommands();
|
||||
}
|
||||
|
||||
updateConfig(key: string, event: Event): void {
|
||||
const target = event.target as HTMLInputElement;
|
||||
let value: any = target.type === 'checkbox' ? target.checked :
|
||||
target.type === 'number' ? parseInt(target.value, 10) :
|
||||
target.value;
|
||||
|
||||
(this.config as any)[key] = value;
|
||||
this.applyConfig();
|
||||
}
|
||||
|
||||
updatePaletteSize(event: Event): void {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
this.paletteSize = target.value as 'md' | 'lg' | 'xl';
|
||||
}
|
||||
|
||||
private applyConfig(): void {
|
||||
this.commandService.updateConfig({
|
||||
showCategories: this.config.showCategories,
|
||||
showShortcuts: this.config.showShortcuts,
|
||||
showRecent: this.config.showRecent,
|
||||
maxResults: this.config.maxResults,
|
||||
recentLimit: this.config.recentLimit
|
||||
});
|
||||
}
|
||||
|
||||
onPaletteOpened(): void {
|
||||
console.log('Command palette opened');
|
||||
}
|
||||
|
||||
onPaletteClosed(): void {
|
||||
console.log('Command palette closed');
|
||||
}
|
||||
|
||||
onCommandExecuted(event: { commandId: string; context: CommandExecutionContext }): void {
|
||||
this.executionCount++;
|
||||
console.log('Command executed:', event);
|
||||
}
|
||||
|
||||
private showToast(message: string): void {
|
||||
// Simple toast implementation for demo
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'demo-toast';
|
||||
toast.textContent = message;
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
z-index: 10000;
|
||||
font-size: 14px;
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOut 0.3s ease forwards';
|
||||
setTimeout(() => document.body.removeChild(toast), 300);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
@use '../../../../../ui-design-system/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
|
||||
|
||||
// Ensure proper centering
|
||||
&.ui-container {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
&.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 {
|
||||
// Ensure containers behave properly
|
||||
.ui-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// 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,258 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ContainerComponent } from '../../../../../ui-essentials/src/lib/components/layout/container/container.component';
|
||||
import { GridSystemComponent } from '../../../../../ui-essentials/src/lib/components/layout/grid-system/grid-system.component';
|
||||
|
||||
@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,448 @@
|
||||
.demo-container {
|
||||
padding: 24px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 48px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 24px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
h4 {
|
||||
background: #f7fafc;
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-demo-container {
|
||||
height: 400px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #f8f9fa;
|
||||
|
||||
&--interactive {
|
||||
height: 600px;
|
||||
border: 2px solid #4299e1;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
|
||||
select,
|
||||
input[type="checkbox"] {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #d2d6dc;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #4299e1;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Demo content styling for dashboard shell slots
|
||||
.demo-content {
|
||||
padding: 20px;
|
||||
|
||||
h4, h5 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.6;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: 16px;
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.5;
|
||||
color: #4a5568;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar demo styling
|
||||
nav[slot="sidebar"] {
|
||||
h5 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
h6 {
|
||||
margin: 16px 0 8px 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #4a5568;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: #4a5568;
|
||||
|
||||
&:hover {
|
||||
background: #edf2f7;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #4299e1;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
// Demo buttons
|
||||
.demo-button {
|
||||
background: #4299e1;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:hover {
|
||||
background: #3182ce;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #4299e1;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background: #38a169;
|
||||
|
||||
&:hover {
|
||||
background: #2f855a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-button-sm {
|
||||
background: transparent;
|
||||
color: #4a5568;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f7fafc;
|
||||
border-color: #cbd5e0;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #4299e1;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Content area styling
|
||||
.content-area {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.metric-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: #ffffff;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
color: #2d3748;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.chart-placeholder {
|
||||
background: #ffffff;
|
||||
padding: 40px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
color: #a0aec0;
|
||||
font-size: 1.125rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.user-list,
|
||||
.project-list {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.user-item,
|
||||
.project-item {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
color: #4a5568;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-groups {
|
||||
background: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.setting-group {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h6 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #4a5568;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
// Notification styling
|
||||
.notification-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: #ebf8ff;
|
||||
border: 1px solid #bee3f8;
|
||||
border-radius: 6px;
|
||||
color: #2b6cb0;
|
||||
font-size: 0.875rem;
|
||||
|
||||
.dismiss-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #2b6cb0;
|
||||
cursor: pointer;
|
||||
font-size: 1.125rem;
|
||||
padding: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: #bee3f8;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Footer styling
|
||||
.footer-detailed {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.footer-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage information
|
||||
.usage-info {
|
||||
background: #f8f9fa;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: #2d3748;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: 24px;
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.6;
|
||||
color: #4a5568;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
background: #edf2f7;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
font-size: 0.875rem;
|
||||
color: #2d3748;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.demo-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-demo-container {
|
||||
&--interactive {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
label {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.demo-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dashboard-demo-container {
|
||||
height: 300px;
|
||||
|
||||
&--interactive {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DashboardShellComponent } from '../../../../../ui-essentials/src/lib/components/layout/dashboard-shell/dashboard-shell.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-dashboard-shell-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, DashboardShellComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Dashboard Shell Demo</h2>
|
||||
|
||||
<!-- Variant Demonstrations -->
|
||||
<section class="demo-section">
|
||||
<h3>Visual Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-card">
|
||||
<h4>{{ variant | titlecase }}</h4>
|
||||
<div class="dashboard-demo-container">
|
||||
<ui-dashboard-shell
|
||||
[variant]="variant"
|
||||
sidebarWidth="sm"
|
||||
headerHeight="sm"
|
||||
footerVariant="minimal">
|
||||
|
||||
<div slot="header">
|
||||
<h4>{{ variant | titlecase }} App</h4>
|
||||
</div>
|
||||
|
||||
<div slot="header-actions">
|
||||
<span>🌙</span>
|
||||
<span>🔔</span>
|
||||
<span>👤</span>
|
||||
</div>
|
||||
|
||||
<nav slot="sidebar">
|
||||
<ul>
|
||||
<li>🏠 Dashboard</li>
|
||||
<li>📊 Analytics</li>
|
||||
<li>⚙️ Settings</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<nav slot="breadcrumbs">
|
||||
<span>Home / Dashboard / {{ variant | titlecase }}</span>
|
||||
</nav>
|
||||
|
||||
<div slot="notifications">
|
||||
<p>📢 Welcome to {{ variant }} variant!</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<h4>Main Content</h4>
|
||||
<p>This is the main content area for {{ variant }} dashboard variant.</p>
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<span>© 2024 Dashboard App</span>
|
||||
</div>
|
||||
</ui-dashboard-shell>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Configurations</h3>
|
||||
<div class="demo-row">
|
||||
@for (config of sizeConfigs; track config.name) {
|
||||
<div class="demo-card">
|
||||
<h4>{{ config.name }}</h4>
|
||||
<div class="dashboard-demo-container">
|
||||
<ui-dashboard-shell
|
||||
[sidebarWidth]="config.sidebarWidth"
|
||||
[headerHeight]="config.headerHeight"
|
||||
[footerVariant]="config.footerVariant">
|
||||
|
||||
<div slot="header">
|
||||
<h4>{{ config.name }} Dashboard</h4>
|
||||
</div>
|
||||
|
||||
<div slot="header-actions">
|
||||
<span>🔍</span>
|
||||
<span>📱</span>
|
||||
</div>
|
||||
|
||||
<nav slot="sidebar">
|
||||
<h5>Navigation</h5>
|
||||
<ul>
|
||||
<li>📈 Overview</li>
|
||||
<li>📋 Tasks</li>
|
||||
<li>💼 Projects</li>
|
||||
<li>👥 Team</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<nav slot="breadcrumbs">
|
||||
<span>Dashboard / {{ config.name }} Layout</span>
|
||||
</nav>
|
||||
|
||||
<div class="demo-content">
|
||||
<h4>{{ config.name }} Content</h4>
|
||||
<p>Sidebar: {{ config.sidebarWidth }}, Header: {{ config.headerHeight }}, Footer: {{ config.footerVariant }}</p>
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<span>{{ config.name }} Footer</span>
|
||||
<span>v1.0.0</span>
|
||||
</div>
|
||||
</ui-dashboard-shell>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Layout Options -->
|
||||
<section class="demo-section">
|
||||
<h3>Layout Options</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-card">
|
||||
<h4>No Footer</h4>
|
||||
<div class="dashboard-demo-container">
|
||||
<ui-dashboard-shell
|
||||
[showFooter]="false"
|
||||
sidebarWidth="sm">
|
||||
|
||||
<div slot="header">
|
||||
<h4>Clean Dashboard</h4>
|
||||
</div>
|
||||
|
||||
<div slot="header-actions">
|
||||
<span>🎨</span>
|
||||
</div>
|
||||
|
||||
<nav slot="sidebar">
|
||||
<ul>
|
||||
<li>🖼️ Gallery</li>
|
||||
<li>🎵 Music</li>
|
||||
<li>📝 Notes</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="demo-content">
|
||||
<h4>Clean Layout</h4>
|
||||
<p>Dashboard without footer for cleaner appearance.</p>
|
||||
</div>
|
||||
</ui-dashboard-shell>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h4>Minimal UI</h4>
|
||||
<div class="dashboard-demo-container">
|
||||
<ui-dashboard-shell
|
||||
[showBreadcrumbs]="false"
|
||||
[showNotifications]="false"
|
||||
sidebarWidth="sm"
|
||||
footerVariant="minimal">
|
||||
|
||||
<div slot="header">
|
||||
<h4>Minimal App</h4>
|
||||
</div>
|
||||
|
||||
<div slot="header-actions">
|
||||
<span>⚡</span>
|
||||
</div>
|
||||
|
||||
<nav slot="sidebar">
|
||||
<ul>
|
||||
<li>🚀 Launch</li>
|
||||
<li>📊 Stats</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="demo-content">
|
||||
<h4>Minimal Interface</h4>
|
||||
<p>Streamlined dashboard without breadcrumbs and notifications.</p>
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<span>Minimal</span>
|
||||
</div>
|
||||
</ui-dashboard-shell>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Dashboard</h3>
|
||||
<div class="demo-controls">
|
||||
<label>
|
||||
Variant:
|
||||
<select [(ngModel)]="interactiveVariant">
|
||||
<option value="default">Default</option>
|
||||
<option value="bordered">Bordered</option>
|
||||
<option value="elevated">Elevated</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Sidebar Width:
|
||||
<select [(ngModel)]="interactiveSidebarWidth">
|
||||
<option value="sm">Small</option>
|
||||
<option value="md">Medium</option>
|
||||
<option value="lg">Large</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Header Height:
|
||||
<select [(ngModel)]="interactiveHeaderHeight">
|
||||
<option value="sm">Small</option>
|
||||
<option value="md">Medium</option>
|
||||
<option value="lg">Large</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Footer Variant:
|
||||
<select [(ngModel)]="interactiveFooterVariant">
|
||||
<option value="minimal">Minimal</option>
|
||||
<option value="standard">Standard</option>
|
||||
<option value="detailed">Detailed</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="interactiveSidebarCollapsed">
|
||||
Sidebar Collapsed
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="interactiveMobileMenuOpen">
|
||||
Mobile Menu Open
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="interactiveShowFooter">
|
||||
Show Footer
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="interactiveShowBreadcrumbs">
|
||||
Show Breadcrumbs
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="interactiveShowNotifications">
|
||||
Show Notifications
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-demo-container dashboard-demo-container--interactive">
|
||||
<ui-dashboard-shell
|
||||
[variant]="interactiveVariant"
|
||||
[sidebarWidth]="interactiveSidebarWidth"
|
||||
[headerHeight]="interactiveHeaderHeight"
|
||||
[footerVariant]="interactiveFooterVariant"
|
||||
[sidebarCollapsed]="interactiveSidebarCollapsed"
|
||||
[mobileMenuOpen]="interactiveMobileMenuOpen"
|
||||
[showFooter]="interactiveShowFooter"
|
||||
[showBreadcrumbs]="interactiveShowBreadcrumbs"
|
||||
[showNotifications]="interactiveShowNotifications"
|
||||
(sidebarToggled)="handleSidebarToggle($event)"
|
||||
(mobileMenuToggled)="handleMobileMenuToggle($event)"
|
||||
(mobileBackdropClicked)="handleMobileBackdropClick()">
|
||||
|
||||
<div slot="header">
|
||||
<h4>Interactive Dashboard</h4>
|
||||
</div>
|
||||
|
||||
<div slot="header-actions">
|
||||
<button (click)="toggleTheme()" class="demo-button-sm">
|
||||
{{ isDarkTheme ? '☀️' : '🌙' }}
|
||||
</button>
|
||||
<button (click)="showNotification()" class="demo-button-sm">
|
||||
🔔 {{ notificationCount }}
|
||||
</button>
|
||||
<button (click)="toggleSidebar()" class="demo-button-sm">
|
||||
{{ interactiveSidebarCollapsed ? '➡️' : '⬅️' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav slot="sidebar">
|
||||
<h5>Main Navigation</h5>
|
||||
<ul>
|
||||
<li (click)="setActiveSection('dashboard')">
|
||||
<span [class.active]="activeSection === 'dashboard'">🏠 Dashboard</span>
|
||||
</li>
|
||||
<li (click)="setActiveSection('analytics')">
|
||||
<span [class.active]="activeSection === 'analytics'">📊 Analytics</span>
|
||||
</li>
|
||||
<li (click)="setActiveSection('users')">
|
||||
<span [class.active]="activeSection === 'users'">👥 Users</span>
|
||||
</li>
|
||||
<li (click)="setActiveSection('projects')">
|
||||
<span [class.active]="activeSection === 'projects'">💼 Projects</span>
|
||||
</li>
|
||||
<li (click)="setActiveSection('settings')">
|
||||
<span [class.active]="activeSection === 'settings'">⚙️ Settings</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@if (!interactiveSidebarCollapsed) {
|
||||
<div class="sidebar-section">
|
||||
<h6>Quick Actions</h6>
|
||||
<button (click)="performAction('new')" class="demo-button">New Project</button>
|
||||
<button (click)="performAction('export')" class="demo-button">Export Data</button>
|
||||
</div>
|
||||
}
|
||||
</nav>
|
||||
|
||||
<nav slot="breadcrumbs">
|
||||
<span>Home / {{ activeSection | titlecase }} / {{ interactiveVariant | titlecase }}</span>
|
||||
</nav>
|
||||
|
||||
<div slot="notifications">
|
||||
@if (hasNewNotifications) {
|
||||
<div class="notification-item">
|
||||
<span>🎉 New feature available! Click to explore the {{ activeSection }} section.</span>
|
||||
<button (click)="dismissNotification()" class="dismiss-btn">×</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<h4>{{ activeSection | titlecase }} Section</h4>
|
||||
<p>Current configuration:</p>
|
||||
<ul>
|
||||
<li><strong>Variant:</strong> {{ interactiveVariant }}</li>
|
||||
<li><strong>Sidebar Width:</strong> {{ interactiveSidebarWidth }}</li>
|
||||
<li><strong>Header Height:</strong> {{ interactiveHeaderHeight }}</li>
|
||||
<li><strong>Footer Variant:</strong> {{ interactiveFooterVariant }}</li>
|
||||
<li><strong>Sidebar Collapsed:</strong> {{ interactiveSidebarCollapsed ? 'Yes' : 'No' }}</li>
|
||||
<li><strong>Theme:</strong> {{ isDarkTheme ? 'Dark' : 'Light' }}</li>
|
||||
</ul>
|
||||
|
||||
<div class="content-area">
|
||||
<h5>{{ activeSection | titlecase }} Content</h5>
|
||||
@switch (activeSection) {
|
||||
@case ('dashboard') {
|
||||
<p>📈 Welcome to your dashboard overview. Here you can see key metrics and quick actions.</p>
|
||||
<div class="metric-cards">
|
||||
<div class="metric-card">Total Users: 1,234</div>
|
||||
<div class="metric-card">Active Projects: 42</div>
|
||||
<div class="metric-card">Revenue: $12,345</div>
|
||||
</div>
|
||||
}
|
||||
@case ('analytics') {
|
||||
<p>📊 Analytics and reporting section. View detailed insights about your application usage.</p>
|
||||
<div class="chart-placeholder">📈 Chart Placeholder</div>
|
||||
}
|
||||
@case ('users') {
|
||||
<p>👥 User management section. Manage user accounts, permissions, and profiles.</p>
|
||||
<div class="user-list">
|
||||
<div class="user-item">John Doe - Admin</div>
|
||||
<div class="user-item">Jane Smith - User</div>
|
||||
<div class="user-item">Bob Johnson - Manager</div>
|
||||
</div>
|
||||
}
|
||||
@case ('projects') {
|
||||
<p>💼 Project management section. Track and manage your ongoing projects.</p>
|
||||
<div class="project-list">
|
||||
<div class="project-item">🚀 Website Redesign - In Progress</div>
|
||||
<div class="project-item">📱 Mobile App - Planning</div>
|
||||
<div class="project-item">🔧 API Integration - Completed</div>
|
||||
</div>
|
||||
}
|
||||
@case ('settings') {
|
||||
<p>⚙️ Application settings and configuration options.</p>
|
||||
<div class="settings-groups">
|
||||
<div class="setting-group">
|
||||
<h6>Appearance</h6>
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="isDarkTheme">
|
||||
Dark Theme
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<h6>Notifications</h6>
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="emailNotifications">
|
||||
Email Notifications
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@default {
|
||||
<p>Select a section from the sidebar to view its content.</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button (click)="performAction('save')" class="demo-button primary">Save Changes</button>
|
||||
<button (click)="performAction('cancel')" class="demo-button">Cancel</button>
|
||||
<button (click)="performAction('refresh')" class="demo-button">Refresh Data</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
@if (interactiveFooterVariant === 'detailed') {
|
||||
<div class="footer-detailed">
|
||||
<div class="footer-section">
|
||||
<span>© 2024 Interactive Dashboard</span>
|
||||
<span>Version 1.2.3</span>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<span>Last Updated: {{ lastUpdated }}</span>
|
||||
<span>Status: {{ connectionStatus }}</span>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (interactiveFooterVariant === 'standard') {
|
||||
<span>© 2024 Interactive Dashboard - v1.2.3</span>
|
||||
<span>Status: {{ connectionStatus }}</span>
|
||||
} @else {
|
||||
<span>© 2024 Dashboard</span>
|
||||
}
|
||||
</div>
|
||||
</ui-dashboard-shell>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Usage Information</h3>
|
||||
<div class="usage-info">
|
||||
<h4>Key Features:</h4>
|
||||
<ul>
|
||||
<li>🏗️ Complete application shell structure</li>
|
||||
<li>📱 Mobile-responsive with hamburger menu</li>
|
||||
<li>🎨 Multiple visual variants (default, bordered, elevated)</li>
|
||||
<li>📏 Configurable sizing for header, sidebar, and footer</li>
|
||||
<li>🧭 Integrated breadcrumbs navigation</li>
|
||||
<li>🔔 Notifications area with live region support</li>
|
||||
<li>♿ Full accessibility support with ARIA labels</li>
|
||||
<li>🎛️ Flexible content projection slots</li>
|
||||
<li>⚡ Event emitters for user interactions</li>
|
||||
</ul>
|
||||
|
||||
<h4>Content Projection Slots:</h4>
|
||||
<ul>
|
||||
<li><code>slot="header"</code> - Header title and branding</li>
|
||||
<li><code>slot="header-actions"</code> - Theme switcher, notifications, user menu</li>
|
||||
<li><code>slot="sidebar"</code> - Main navigation content</li>
|
||||
<li><code>slot="breadcrumbs"</code> - Breadcrumb navigation</li>
|
||||
<li><code>slot="notifications"</code> - System notifications and alerts</li>
|
||||
<li><code>slot="footer"</code> - Footer content and links</li>
|
||||
<li>Default slot - Main page content</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './dashboard-shell-demo.component.scss'
|
||||
})
|
||||
export class DashboardShellDemoComponent {
|
||||
variants = ['default', 'bordered', 'elevated'] as const;
|
||||
|
||||
sizeConfigs = [
|
||||
{ name: 'Compact', sidebarWidth: 'sm' as const, headerHeight: 'sm' as const, footerVariant: 'minimal' as const },
|
||||
{ name: 'Standard', sidebarWidth: 'md' as const, headerHeight: 'md' as const, footerVariant: 'standard' as const },
|
||||
{ name: 'Spacious', sidebarWidth: 'lg' as const, headerHeight: 'lg' as const, footerVariant: 'detailed' as const }
|
||||
];
|
||||
|
||||
// Interactive demo properties
|
||||
interactiveVariant: 'default' | 'bordered' | 'elevated' = 'default';
|
||||
interactiveSidebarWidth: 'sm' | 'md' | 'lg' = 'md';
|
||||
interactiveHeaderHeight: 'sm' | 'md' | 'lg' = 'md';
|
||||
interactiveFooterVariant: 'minimal' | 'standard' | 'detailed' = 'standard';
|
||||
interactiveSidebarCollapsed = false;
|
||||
interactiveMobileMenuOpen = false;
|
||||
interactiveShowFooter = true;
|
||||
interactiveShowBreadcrumbs = true;
|
||||
interactiveShowNotifications = true;
|
||||
|
||||
// Demo state
|
||||
activeSection = 'dashboard';
|
||||
isDarkTheme = false;
|
||||
notificationCount = 3;
|
||||
hasNewNotifications = true;
|
||||
emailNotifications = true;
|
||||
lastUpdated = new Date().toLocaleTimeString();
|
||||
connectionStatus = 'Connected';
|
||||
|
||||
handleSidebarToggle(collapsed: boolean): void {
|
||||
this.interactiveSidebarCollapsed = collapsed;
|
||||
console.log('Sidebar toggled:', collapsed);
|
||||
}
|
||||
|
||||
handleMobileMenuToggle(open: boolean): void {
|
||||
this.interactiveMobileMenuOpen = open;
|
||||
console.log('Mobile menu toggled:', open);
|
||||
}
|
||||
|
||||
handleMobileBackdropClick(): void {
|
||||
this.interactiveMobileMenuOpen = false;
|
||||
console.log('Mobile backdrop clicked - closing menu');
|
||||
}
|
||||
|
||||
toggleSidebar(): void {
|
||||
this.interactiveSidebarCollapsed = !this.interactiveSidebarCollapsed;
|
||||
}
|
||||
|
||||
toggleTheme(): void {
|
||||
this.isDarkTheme = !this.isDarkTheme;
|
||||
console.log('Theme toggled:', this.isDarkTheme ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
showNotification(): void {
|
||||
this.notificationCount++;
|
||||
this.hasNewNotifications = true;
|
||||
console.log('New notification added');
|
||||
}
|
||||
|
||||
dismissNotification(): void {
|
||||
this.hasNewNotifications = false;
|
||||
console.log('Notification dismissed');
|
||||
}
|
||||
|
||||
setActiveSection(section: string): void {
|
||||
this.activeSection = section;
|
||||
this.lastUpdated = new Date().toLocaleTimeString();
|
||||
console.log('Active section changed to:', section);
|
||||
}
|
||||
|
||||
performAction(action: string): void {
|
||||
console.log('Action performed:', action);
|
||||
this.lastUpdated = new Date().toLocaleTimeString();
|
||||
|
||||
switch (action) {
|
||||
case 'save':
|
||||
this.connectionStatus = 'Saving...';
|
||||
setTimeout(() => {
|
||||
this.connectionStatus = 'Saved';
|
||||
setTimeout(() => this.connectionStatus = 'Connected', 2000);
|
||||
}, 1000);
|
||||
break;
|
||||
case 'refresh':
|
||||
this.connectionStatus = 'Refreshing...';
|
||||
setTimeout(() => this.connectionStatus = 'Connected', 1500);
|
||||
break;
|
||||
case 'new':
|
||||
case 'export':
|
||||
case 'cancel':
|
||||
// Handle other actions
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,741 @@
|
||||
import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
// Import all utilities from ui-data-utils
|
||||
import {
|
||||
// Types
|
||||
SortConfig, FilterConfig, PaginationConfig,
|
||||
// Sorting
|
||||
sortBy, sortByMultiple, createComparator,
|
||||
// Filtering
|
||||
filterBy, filterByMultiple, searchFilter,
|
||||
// Pagination
|
||||
paginate, calculatePages, getPaginationRange,
|
||||
// Transformation
|
||||
groupBy, aggregate, pluck, flatten, pivot, unique, frequency
|
||||
} from 'ui-data-utils';
|
||||
|
||||
interface SampleData {
|
||||
id: number;
|
||||
name: string;
|
||||
department: string;
|
||||
salary: number;
|
||||
hireDate: string;
|
||||
active: boolean;
|
||||
skills: string[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-data-utils-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>UI Data Utils Demo</h2>
|
||||
<p>Comprehensive demonstration of data manipulation utilities</p>
|
||||
|
||||
<!-- Sample Data Display -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Sample Employee Data ({{ sampleData.length }} records)</h3>
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">
|
||||
<h4>Raw Data Preview:</h4>
|
||||
<pre style="max-height: 200px; overflow-y: auto;">{{ getSampleDataPreview() }}</pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Sorting Utilities Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>🔄 Sorting Utilities</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
||||
<!-- Single Sort -->
|
||||
<div>
|
||||
<h4>Single Property Sort</h4>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label>Sort by: </label>
|
||||
<select [(ngModel)]="sortField" (ngModelChange)="applySingleSort()">
|
||||
<option value="name">Name</option>
|
||||
<option value="department">Department</option>
|
||||
<option value="salary">Salary</option>
|
||||
<option value="hireDate">Hire Date</option>
|
||||
</select>
|
||||
<label style="margin-left: 1rem;">
|
||||
<input type="checkbox" [(ngModel)]="sortDesc" (ngModelChange)="applySingleSort()">
|
||||
Descending
|
||||
</label>
|
||||
</div>
|
||||
<div style="background: #e3f2fd; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||||
<pre>{{ getSortedDataPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Multi-column Sort -->
|
||||
<div>
|
||||
<h4>Multi-column Sort</h4>
|
||||
<p style="font-size: 0.9em; color: #666;">Department → Salary → Name</p>
|
||||
<div style="background: #e8f5e8; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||||
<pre>{{ getMultiSortedDataPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Filtering Utilities Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>🔍 Filtering Utilities</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
||||
<!-- Search Filter -->
|
||||
<div>
|
||||
<h4>Search Filter</h4>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search in name, department..."
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="applySearchFilter()"
|
||||
style="width: 100%; padding: 0.5rem; margin-bottom: 1rem;"
|
||||
>
|
||||
<div style="background: #fff3cd; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||||
<pre>{{ getSearchFilteredDataPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Property Filter -->
|
||||
<div>
|
||||
<h4>Property Filters</h4>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label>Department: </label>
|
||||
<select [(ngModel)]="filterDepartment" (ngModelChange)="applyPropertyFilter()">
|
||||
<option value="">All Departments</option>
|
||||
<option value="Engineering">Engineering</option>
|
||||
<option value="Marketing">Marketing</option>
|
||||
<option value="Sales">Sales</option>
|
||||
<option value="HR">HR</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label>Min Salary: </label>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="minSalary"
|
||||
(ngModelChange)="applyPropertyFilter()"
|
||||
placeholder="50000"
|
||||
style="width: 120px; padding: 0.25rem;"
|
||||
>
|
||||
</div>
|
||||
<div style="background: #f3e5f5; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||||
<pre>{{ getPropertyFilteredDataPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pagination Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>📄 Pagination Utilities</h3>
|
||||
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
|
||||
<label>Page Size: </label>
|
||||
<select [(ngModel)]="pageSize" (ngModelChange)="updatePagination()">
|
||||
<option [value]="3">3</option>
|
||||
<option [value]="5">5</option>
|
||||
<option [value]="7">7</option>
|
||||
<option [value]="10">10</option>
|
||||
</select>
|
||||
|
||||
<label>Page: </label>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="currentPage"
|
||||
(ngModelChange)="updatePagination()"
|
||||
[min]="1"
|
||||
[max]="paginationResult().totalPages"
|
||||
style="width: 60px; padding: 0.25rem;"
|
||||
>
|
||||
|
||||
<span>of {{ paginationResult().totalPages }}</span>
|
||||
|
||||
<button
|
||||
(click)="previousPage()"
|
||||
[disabled]="!paginationResult().hasPrevious"
|
||||
style="padding: 0.25rem 0.5rem;"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
(click)="nextPage()"
|
||||
[disabled]="!paginationResult().hasNext"
|
||||
style="padding: 0.25rem 0.5rem;"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<strong>Page Navigation: </strong>
|
||||
@for (page of pageRange(); track page) {
|
||||
@if (page === 'ellipsis') {
|
||||
<span style="margin: 0 0.5rem;">...</span>
|
||||
} @else {
|
||||
<button
|
||||
(click)="goToPage(page)"
|
||||
[style.font-weight]="page === currentPage() ? 'bold' : 'normal'"
|
||||
[style.background]="page === currentPage() ? '#007bff' : '#f8f9fa'"
|
||||
[style.color]="page === currentPage() ? 'white' : 'black'"
|
||||
style="padding: 0.25rem 0.5rem; margin: 0 0.125rem; border: 1px solid #ccc; cursor: pointer;"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div style="background: #e1f5fe; padding: 1rem; border-radius: 4px;">
|
||||
<p><strong>Showing:</strong> {{ getItemRangeText() }}</p>
|
||||
<pre style="max-height: 150px; overflow-y: auto;">{{ getPaginatedDataPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Transformation Utilities Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>🔄 Transformation Utilities</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
||||
<!-- Group By -->
|
||||
<div>
|
||||
<h4>Group By Department</h4>
|
||||
<div style="background: #f1f8e9; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||||
<pre>{{ getGroupedDataPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aggregation -->
|
||||
<div>
|
||||
<h4>Salary Statistics by Department</h4>
|
||||
<div style="background: #fce4ec; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||||
<pre>{{ getAggregationPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pluck -->
|
||||
<div>
|
||||
<h4>Extract Names & Salaries</h4>
|
||||
<div style="background: #f3e5f5; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||||
<pre>{{ getPluckedDataPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pivot Table -->
|
||||
<div>
|
||||
<h4>Pivot: Department vs Active Status</h4>
|
||||
<div style="background: #fff3e0; padding: 1rem; border-radius: 4px; max-height: 200px; overflow-y: auto;">
|
||||
<pre>{{ getPivotDataPreview() }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Combined Operations Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>🔄 Combined Operations</h3>
|
||||
<p>Demonstrate chaining multiple operations: Filter → Sort → Paginate</p>
|
||||
|
||||
<div style="display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap;">
|
||||
<label>
|
||||
Filter Active:
|
||||
<select [(ngModel)]="combinedActiveFilter" (ngModelChange)="updateCombinedDemo()">
|
||||
<option value="">All</option>
|
||||
<option value="true">Active Only</option>
|
||||
<option value="false">Inactive Only</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Sort By:
|
||||
<select [(ngModel)]="combinedSortField" (ngModelChange)="updateCombinedDemo()">
|
||||
<option value="name">Name</option>
|
||||
<option value="salary">Salary</option>
|
||||
<option value="hireDate">Hire Date</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="combinedSortDesc" (ngModelChange)="updateCombinedDemo()">
|
||||
Descending
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="background: #e8f5e8; padding: 1rem; border-radius: 4px;">
|
||||
<p><strong>Pipeline:</strong>
|
||||
{{ sampleData.length }} records →
|
||||
{{ combinedFiltered().length }} filtered →
|
||||
{{ combinedSorted().length }} sorted →
|
||||
{{ combinedPaginated().data.length }} on page {{ combinedPaginated().page }}
|
||||
</p>
|
||||
<pre style="max-height: 200px; overflow-y: auto;">{{ getCombinedResultPreview() }}</pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Performance Metrics -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>📊 Performance & Statistics</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; text-align: center;">
|
||||
<h4>Total Records</h4>
|
||||
<div style="font-size: 2rem; font-weight: bold; color: #007bff;">{{ sampleData.length }}</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; text-align: center;">
|
||||
<h4>Departments</h4>
|
||||
<div style="font-size: 2rem; font-weight: bold; color: #28a745;">{{ getDepartmentCount() }}</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; text-align: center;">
|
||||
<h4>Avg Salary</h4>
|
||||
<div style="font-size: 2rem; font-weight: bold; color: #ffc107;">{{ getAverageSalary() }}</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; text-align: center;">
|
||||
<h4>Active Employees</h4>
|
||||
<div style="font-size: 2rem; font-weight: bold; color: #17a2b8;">{{ getActiveCount() }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Code Examples -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>💻 Code Examples</h3>
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-radius: 4px;">
|
||||
|
||||
<h4>Sorting Examples:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>// Single sort
|
||||
const sorted = sortBy(data, {{ '{' }} key: 'salary', direction: 'desc', dataType: 'number' {{ '}' }});
|
||||
|
||||
// Multi-column sort
|
||||
const multiSorted = sortByMultiple(data, {{ '{' }}
|
||||
sorts: [
|
||||
{{ '{' }} key: 'department', direction: 'asc', dataType: 'string' {{ '}' }},
|
||||
{{ '{' }} key: 'salary', direction: 'desc', dataType: 'number' {{ '}' }}
|
||||
]
|
||||
{{ '}' }});</code></pre>
|
||||
|
||||
<h4>Filtering Examples:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>// Property filter
|
||||
const filtered = filterBy(data, {{ '{' }}
|
||||
key: 'salary',
|
||||
operator: 'greater_than',
|
||||
value: 50000,
|
||||
dataType: 'number'
|
||||
{{ '}' }});
|
||||
|
||||
// Search across multiple properties
|
||||
const searched = searchFilter(data, 'engineer', ['name', 'department']);</code></pre>
|
||||
|
||||
<h4>Pagination Examples:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>// Paginate data
|
||||
const paginatedResult = paginate(data, {{ '{' }} page: 1, pageSize: 10 {{ '}' }});
|
||||
|
||||
// Get page range for UI
|
||||
const range = getPaginationRange(currentPage, totalPages, 7);</code></pre>
|
||||
|
||||
<h4>Transformation Examples:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>// Group by property
|
||||
const groups = groupBy(data, 'department');
|
||||
|
||||
// Aggregate values
|
||||
const stats = aggregate(data, 'salary', ['sum', 'avg', 'min', 'max']);
|
||||
|
||||
// Extract specific properties
|
||||
const names = pluck(data, ['name', 'salary']);</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Library Info -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>📚 Library Information</h3>
|
||||
<div style="background: #e3f2fd; padding: 1.5rem; border-radius: 4px;">
|
||||
<h4>UI Data Utils Features:</h4>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;">
|
||||
<div>
|
||||
<h5>🔄 Sorting</h5>
|
||||
<ul style="margin: 0; padding-left: 1.5rem;">
|
||||
<li>Single & multi-column sorting</li>
|
||||
<li>Type-aware comparisons</li>
|
||||
<li>Stable sort algorithms</li>
|
||||
<li>Custom comparator functions</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h5>🔍 Filtering</h5>
|
||||
<ul style="margin: 0; padding-left: 1.5rem;">
|
||||
<li>Multiple filter operators</li>
|
||||
<li>Search across properties</li>
|
||||
<li>Complex filter combinations</li>
|
||||
<li>Type-specific filtering</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h5>📄 Pagination</h5>
|
||||
<ul style="margin: 0; padding-left: 1.5rem;">
|
||||
<li>Client-side pagination</li>
|
||||
<li>Page range calculations</li>
|
||||
<li>Pagination metadata</li>
|
||||
<li>Navigation utilities</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h5>🔄 Transformation</h5>
|
||||
<ul style="margin: 0; padding-left: 1.5rem;">
|
||||
<li>Group by operations</li>
|
||||
<li>Data aggregations</li>
|
||||
<li>Property extraction</li>
|
||||
<li>Pivot tables</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
h2 {
|
||||
color: hsl(279, 14%, 11%);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
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;
|
||||
}
|
||||
|
||||
h5 {
|
||||
color: hsl(279, 14%, 45%);
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid #e9ecef;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
code {
|
||||
color: hsl(279, 14%, 15%);
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input, select {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class DataUtilsDemoComponent {
|
||||
// Sample data
|
||||
sampleData: SampleData[] = [
|
||||
{ id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, hireDate: '2022-01-15', active: true, skills: ['JavaScript', 'Angular', 'TypeScript'] },
|
||||
{ id: 2, name: 'Bob Smith', department: 'Marketing', salary: 65000, hireDate: '2021-03-22', active: true, skills: ['SEO', 'Content Marketing'] },
|
||||
{ id: 3, name: 'Charlie Brown', department: 'Engineering', salary: 87000, hireDate: '2023-02-10', active: false, skills: ['React', 'Node.js'] },
|
||||
{ id: 4, name: 'Diana Davis', department: 'Sales', salary: 72000, hireDate: '2020-11-05', active: true, skills: ['CRM', 'Negotiation'] },
|
||||
{ id: 5, name: 'Eve Wilson', department: 'Engineering', salary: 102000, hireDate: '2021-08-18', active: true, skills: ['Python', 'Machine Learning'] },
|
||||
{ id: 6, name: 'Frank Miller', department: 'HR', salary: 58000, hireDate: '2022-05-30', active: false, skills: ['Recruiting', 'Employee Relations'] },
|
||||
{ id: 7, name: 'Grace Lee', department: 'Marketing', salary: 69000, hireDate: '2023-01-12', active: true, skills: ['Brand Management', 'Social Media'] },
|
||||
{ id: 8, name: 'Henry Taylor', department: 'Sales', salary: 78000, hireDate: '2021-12-03', active: true, skills: ['Sales Strategy', 'Lead Generation'] },
|
||||
{ id: 9, name: 'Ivy Chen', department: 'Engineering', salary: 91000, hireDate: '2022-09-14', active: true, skills: ['Vue.js', 'GraphQL'] },
|
||||
{ id: 10, name: 'Jack White', department: 'HR', salary: 62000, hireDate: '2020-04-25', active: false, skills: ['Payroll', 'Benefits Administration'] }
|
||||
];
|
||||
|
||||
// Sorting controls
|
||||
sortField = signal<keyof SampleData>('name');
|
||||
sortDesc = signal<boolean>(false);
|
||||
|
||||
// Filtering controls
|
||||
searchTerm = signal<string>('');
|
||||
filterDepartment = signal<string>('');
|
||||
minSalary = signal<number | null>(null);
|
||||
|
||||
// Pagination controls
|
||||
currentPage = signal<number>(1);
|
||||
pageSize = signal<number>(5);
|
||||
|
||||
// Combined demo controls
|
||||
combinedActiveFilter = signal<string>('');
|
||||
combinedSortField = signal<keyof SampleData>('name');
|
||||
combinedSortDesc = signal<boolean>(false);
|
||||
|
||||
// Computed data
|
||||
sortedData = computed(() => {
|
||||
const config: SortConfig<SampleData> = {
|
||||
key: this.sortField(),
|
||||
direction: this.sortDesc() ? 'desc' : 'asc',
|
||||
dataType: this.getDataType(this.sortField())
|
||||
};
|
||||
return sortBy(this.sampleData, config);
|
||||
});
|
||||
|
||||
multiSortedData = computed(() => {
|
||||
return sortByMultiple(this.sampleData, {
|
||||
sorts: [
|
||||
{ key: 'department', direction: 'asc', dataType: 'string' },
|
||||
{ key: 'salary', direction: 'desc', dataType: 'number' },
|
||||
{ key: 'name', direction: 'asc', dataType: 'string' }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
searchFilteredData = computed(() => {
|
||||
if (!this.searchTerm()) return this.sampleData;
|
||||
return searchFilter(this.sampleData, this.searchTerm(), ['name', 'department']);
|
||||
});
|
||||
|
||||
propertyFilteredData = computed(() => {
|
||||
let filtered = this.sampleData;
|
||||
|
||||
const filters: FilterConfig<SampleData>[] = [];
|
||||
|
||||
if (this.filterDepartment()) {
|
||||
filters.push({
|
||||
key: 'department',
|
||||
operator: 'equals',
|
||||
value: this.filterDepartment(),
|
||||
dataType: 'string'
|
||||
});
|
||||
}
|
||||
|
||||
if (this.minSalary() !== null) {
|
||||
filters.push({
|
||||
key: 'salary',
|
||||
operator: 'greater_than_equal',
|
||||
value: this.minSalary(),
|
||||
dataType: 'number'
|
||||
});
|
||||
}
|
||||
|
||||
return filters.length > 0 ? filterByMultiple(filtered, filters) : filtered;
|
||||
});
|
||||
|
||||
paginationResult = computed(() => {
|
||||
return paginate(this.sampleData, {
|
||||
page: this.currentPage(),
|
||||
pageSize: this.pageSize()
|
||||
});
|
||||
});
|
||||
|
||||
pageRange = computed(() => {
|
||||
return getPaginationRange(this.currentPage(), this.paginationResult().totalPages, 7);
|
||||
});
|
||||
|
||||
// Combined operations
|
||||
combinedFiltered = computed(() => {
|
||||
if (!this.combinedActiveFilter()) return this.sampleData;
|
||||
return filterBy(this.sampleData, {
|
||||
key: 'active',
|
||||
operator: 'equals',
|
||||
value: this.combinedActiveFilter() === 'true',
|
||||
dataType: 'boolean'
|
||||
});
|
||||
});
|
||||
|
||||
combinedSorted = computed(() => {
|
||||
return sortBy(this.combinedFiltered(), {
|
||||
key: this.combinedSortField(),
|
||||
direction: this.combinedSortDesc() ? 'desc' : 'asc',
|
||||
dataType: this.getDataType(this.combinedSortField())
|
||||
});
|
||||
});
|
||||
|
||||
combinedPaginated = computed(() => {
|
||||
return paginate(this.combinedSorted(), { page: 1, pageSize: 5 });
|
||||
});
|
||||
|
||||
// Helper methods
|
||||
private getDataType(key: keyof SampleData): 'string' | 'number' | 'date' | 'boolean' {
|
||||
switch (key) {
|
||||
case 'salary': case 'id': return 'number';
|
||||
case 'hireDate': return 'date';
|
||||
case 'active': return 'boolean';
|
||||
default: return 'string';
|
||||
}
|
||||
}
|
||||
|
||||
getSampleDataPreview(): string {
|
||||
return JSON.stringify(this.sampleData.slice(0, 3), null, 2);
|
||||
}
|
||||
|
||||
getSortedDataPreview(): string {
|
||||
return JSON.stringify(this.sortedData().slice(0, 3).map(item => ({ name: item.name, [this.sortField()]: item[this.sortField()] })), null, 2);
|
||||
}
|
||||
|
||||
getMultiSortedDataPreview(): string {
|
||||
return JSON.stringify(this.multiSortedData().slice(0, 4).map(item => ({
|
||||
name: item.name,
|
||||
department: item.department,
|
||||
salary: item.salary
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getSearchFilteredDataPreview(): string {
|
||||
return JSON.stringify(this.searchFilteredData().slice(0, 3).map(item => ({
|
||||
name: item.name,
|
||||
department: item.department
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getPropertyFilteredDataPreview(): string {
|
||||
return JSON.stringify(this.propertyFilteredData().slice(0, 3).map(item => ({
|
||||
name: item.name,
|
||||
department: item.department,
|
||||
salary: item.salary
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getPaginatedDataPreview(): string {
|
||||
return JSON.stringify(this.paginationResult().data.map(item => ({
|
||||
name: item.name,
|
||||
department: item.department
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getGroupedDataPreview(): string {
|
||||
const grouped = groupBy(this.sampleData, 'department');
|
||||
return JSON.stringify(grouped.map(group => ({
|
||||
department: group.key,
|
||||
count: group.count,
|
||||
employees: group.items.map(emp => emp.name)
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getAggregationPreview(): string {
|
||||
const grouped = groupBy(this.sampleData, 'department');
|
||||
const result = grouped.map(group => ({
|
||||
department: group.key,
|
||||
...aggregate(group.items, 'salary', ['sum', 'avg', 'min', 'max', 'count'])
|
||||
}));
|
||||
return JSON.stringify(result, null, 2);
|
||||
}
|
||||
|
||||
getPluckedDataPreview(): string {
|
||||
const plucked = pluck(this.sampleData, ['name', 'salary']);
|
||||
return JSON.stringify(plucked.slice(0, 5), null, 2);
|
||||
}
|
||||
|
||||
getPivotDataPreview(): string {
|
||||
const pivotData = this.sampleData.map(emp => ({
|
||||
department: emp.department,
|
||||
status: emp.active ? 'Active' : 'Inactive',
|
||||
count: 1
|
||||
}));
|
||||
|
||||
const pivotResult = pivot(pivotData, 'department', 'status', 'count', 'sum');
|
||||
return JSON.stringify(pivotResult, null, 2);
|
||||
}
|
||||
|
||||
getCombinedResultPreview(): string {
|
||||
return JSON.stringify(this.combinedPaginated().data.map(item => ({
|
||||
name: item.name,
|
||||
department: item.department,
|
||||
salary: item.salary,
|
||||
active: item.active
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getItemRangeText(): string {
|
||||
const result = this.paginationResult();
|
||||
if (result.totalItems === 0) return '0 of 0';
|
||||
return `${result.startIndex + 1}-${result.endIndex + 1} of ${result.totalItems}`;
|
||||
}
|
||||
|
||||
getDepartmentCount(): number {
|
||||
return unique(this.sampleData, 'department').length;
|
||||
}
|
||||
|
||||
getAverageSalary(): string {
|
||||
const avg = aggregate(this.sampleData, 'salary', ['avg']);
|
||||
return '$' + Math.round(avg['avg'] || 0).toLocaleString();
|
||||
}
|
||||
|
||||
getActiveCount(): number {
|
||||
const activeCount = this.sampleData.filter(emp => emp.active).length;
|
||||
return Math.round((activeCount / this.sampleData.length) * 100);
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
applySingleSort(): void {
|
||||
// Triggers computed update
|
||||
}
|
||||
|
||||
applySearchFilter(): void {
|
||||
// Triggers computed update
|
||||
}
|
||||
|
||||
applyPropertyFilter(): void {
|
||||
// Triggers computed update
|
||||
}
|
||||
|
||||
updatePagination(): void {
|
||||
// Ensure page is within bounds
|
||||
const maxPage = this.paginationResult().totalPages;
|
||||
if (this.currentPage() > maxPage) {
|
||||
this.currentPage.set(maxPage || 1);
|
||||
}
|
||||
}
|
||||
|
||||
previousPage(): void {
|
||||
if (this.paginationResult().hasPrevious) {
|
||||
this.currentPage.update(page => page - 1);
|
||||
}
|
||||
}
|
||||
|
||||
nextPage(): void {
|
||||
if (this.paginationResult().hasNext) {
|
||||
this.currentPage.update(page => page + 1);
|
||||
}
|
||||
}
|
||||
|
||||
goToPage(page: number | string): void {
|
||||
if (typeof page === 'number') {
|
||||
this.currentPage.set(page);
|
||||
}
|
||||
}
|
||||
|
||||
updateCombinedDemo(): void {
|
||||
// Triggers computed update
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
}
|
||||
492
projects/demo-ui-essentials/src/app/demos/demos.routes.ts
Normal file
492
projects/demo-ui-essentials/src/app/demos/demos.routes.ts
Normal file
@@ -0,0 +1,492 @@
|
||||
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 { KanbanBoardDemoComponent } from './kanban-board-demo/kanban-board-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 { BottomNavigationDemoComponent } from './bottom-navigation-demo/bottom-navigation-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';
|
||||
import { ProgressCircleDemoComponent } from './progress-circle-demo/progress-circle-demo.component';
|
||||
import { RangeSliderDemoComponent } from './range-slider-demo/range-slider-demo.component';
|
||||
import { ColorPickerDemoComponent } from './color-picker-demo/color-picker-demo.component';
|
||||
import { DividerDemoComponent } from './divider-demo/divider-demo.component';
|
||||
import { TooltipDemoComponent } from './tooltip-demo/tooltip-demo.component';
|
||||
import { AccordionDemoComponent } from './accordion-demo/accordion-demo.component';
|
||||
import { PopoverDemoComponent } from './popover-demo/popover-demo.component';
|
||||
import { AlertDemoComponent } from './alert-demo/alert-demo.component';
|
||||
import { SnackbarDemoComponent } from './snackbar-demo/snackbar-demo.component';
|
||||
import { ToastDemoComponent } from './toast-demo/toast-demo.component';
|
||||
import { TreeViewDemoComponent } from './tree-view-demo/tree-view-demo.component';
|
||||
import { TimelineDemoComponent } from './timeline-demo/timeline-demo.component';
|
||||
import { StepperDemoComponent } from './stepper-demo/stepper-demo.component';
|
||||
import { FabMenuDemoComponent } from './fab-menu-demo/fab-menu-demo.component';
|
||||
import { EnhancedTableDemoComponent } from './enhanced-table-demo/enhanced-table-demo.component';
|
||||
import { SplitButtonDemoComponent } from './split-button-demo/split-button-demo.component';
|
||||
import { CommandPaletteDemoComponent } from './command-palette-demo/command-palette-demo.component';
|
||||
import { TransferListDemoComponent } from './transfer-list-demo/transfer-list-demo.component';
|
||||
import { FloatingToolbarDemoComponent } from './floating-toolbar-demo/floating-toolbar-demo.component';
|
||||
import { TagInputDemoComponent } from './tag-input-demo/tag-input-demo.component';
|
||||
import { IconButtonDemoComponent } from './icon-button-demo/icon-button-demo.component';
|
||||
import { StackDemoComponent } from './stack-demo/stack-demo.component';
|
||||
import { BoxDemoComponent } from './box-demo/box-demo.component';
|
||||
import { CenterDemoComponent } from './center-demo/center-demo.component';
|
||||
import { AspectRatioDemoComponent } from './aspect-ratio-demo/aspect-ratio-demo.component';
|
||||
import { BentoGridDemoComponent } from './bento-grid-demo/bento-grid-demo.component';
|
||||
import { BreakpointContainerDemoComponent } from './breakpoint-container-demo/breakpoint-container-demo.component';
|
||||
import { SectionDemoComponent } from './section-demo/section-demo.component';
|
||||
import { FlexDemoComponent } from './flex-demo/flex-demo.component';
|
||||
import { ColumnDemoComponent } from './column-demo/column-demo.component';
|
||||
import { SidebarLayoutDemoComponent } from './sidebar-layout-demo/sidebar-layout-demo.component';
|
||||
import { ScrollContainerDemoComponent } from './scroll-container-demo/scroll-container-demo.component';
|
||||
import { TabsContainerDemoComponent } from './tabs-container-demo/tabs-container-demo.component';
|
||||
import { DashboardShellDemoComponent } from './dashboard-shell-demo/dashboard-shell-demo.component';
|
||||
import { GridContainerDemoComponent } from './grid-container-demo/grid-container-demo.component';
|
||||
import { FeedLayoutDemoComponent } from './feed-layout-demo/feed-layout-demo.component';
|
||||
import { ListDetailLayoutDemoComponent } from './list-detail-layout-demo/list-detail-layout-demo.component';
|
||||
import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo/supporting-pane-layout-demo.component';
|
||||
import { MasonryDemoComponent } from './masonry-demo/masonry-demo.component';
|
||||
import { InfiniteScrollContainerDemoComponent } from './infinite-scroll-container-demo/infinite-scroll-container-demo.component';
|
||||
import { StickyLayoutDemoComponent } from './sticky-layout-demo/sticky-layout-demo.component';
|
||||
import { SplitViewDemoComponent } from './split-view-demo/split-view-demo.component';
|
||||
import { GalleryGridDemoComponent } from './gallery-grid-demo/gallery-grid-demo.component';
|
||||
import { AnimationsDemoComponent } from './animations-demo/animations-demo.component';
|
||||
import { AccessibilityDemoComponent } from './accessibility-demo/accessibility-demo.component';
|
||||
import { SelectDemoComponent } from './select-demo/select-demo.component';
|
||||
import { TextareaDemoComponent } from './textarea-demo/textarea-demo.component';
|
||||
import { DataUtilsDemoComponent } from './data-utils-demo/data-utils-demo.component';
|
||||
import { HclStudioDemoComponent } from './hcl-studio-demo/hcl-studio-demo.component';
|
||||
import { FontManagerDemoComponent } from './font-manager-demo/font-manager-demo.component';
|
||||
import { CodeDisplayDemoComponent } from './code-display-demo/code-display-demo.component';
|
||||
import { BackgroundsDemoComponent } from './backgrounds-demo/backgrounds-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 ("bottom-navigation") {
|
||||
<ui-bottom-navigation-demo></ui-bottom-navigation-demo>
|
||||
}
|
||||
|
||||
@case ("avatar") {
|
||||
<ui-avatar-demo></ui-avatar-demo>
|
||||
}
|
||||
|
||||
@case ("buttons") {
|
||||
<ui-button-demo></ui-button-demo>
|
||||
}
|
||||
|
||||
@case ("icon-button") {
|
||||
<ui-icon-button-demo></ui-icon-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 ("kanban-board") {
|
||||
<ui-kanban-board-demo></ui-kanban-board-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 ("select") {
|
||||
<ui-select-demo></ui-select-demo>
|
||||
}
|
||||
|
||||
@case ("textarea") {
|
||||
<ui-textarea-demo></ui-textarea-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>
|
||||
}
|
||||
|
||||
@case ("progress-circle") {
|
||||
<ui-progress-circle-demo></ui-progress-circle-demo>
|
||||
}
|
||||
|
||||
@case ("range-slider") {
|
||||
<ui-range-slider-demo></ui-range-slider-demo>
|
||||
}
|
||||
|
||||
@case ("color-picker") {
|
||||
<ui-color-picker-demo></ui-color-picker-demo>
|
||||
}
|
||||
|
||||
@case ("divider") {
|
||||
<ui-divider-demo></ui-divider-demo>
|
||||
}
|
||||
|
||||
@case ("tooltip") {
|
||||
<ui-tooltip-demo></ui-tooltip-demo>
|
||||
}
|
||||
|
||||
@case ("accordion") {
|
||||
<ui-accordion-demo></ui-accordion-demo>
|
||||
}
|
||||
|
||||
@case ("popover") {
|
||||
<ui-popover-demo></ui-popover-demo>
|
||||
}
|
||||
|
||||
@case ("alert") {
|
||||
<ui-alert-demo></ui-alert-demo>
|
||||
}
|
||||
|
||||
@case ("snackbar") {
|
||||
<ui-snackbar-demo></ui-snackbar-demo>
|
||||
}
|
||||
|
||||
@case ("toast") {
|
||||
<ui-toast-demo></ui-toast-demo>
|
||||
}
|
||||
|
||||
@case ("tree-view") {
|
||||
<ui-tree-view-demo></ui-tree-view-demo>
|
||||
}
|
||||
|
||||
@case ("timeline") {
|
||||
<ui-timeline-demo></ui-timeline-demo>
|
||||
}
|
||||
|
||||
@case ("stepper") {
|
||||
<ui-stepper-demo></ui-stepper-demo>
|
||||
}
|
||||
|
||||
@case ("fab-menu") {
|
||||
<ui-fab-menu-demo></ui-fab-menu-demo>
|
||||
}
|
||||
|
||||
@case ("enhanced-table") {
|
||||
<ui-enhanced-table-demo></ui-enhanced-table-demo>
|
||||
}
|
||||
|
||||
@case ("split-button") {
|
||||
<ui-split-button-demo></ui-split-button-demo>
|
||||
}
|
||||
|
||||
@case ("command-palette") {
|
||||
<ui-command-palette-demo></ui-command-palette-demo>
|
||||
}
|
||||
|
||||
@case ("floating-toolbar") {
|
||||
<ui-floating-toolbar-demo></ui-floating-toolbar-demo>
|
||||
}
|
||||
|
||||
@case ("transfer-list") {
|
||||
<ui-transfer-list-demo></ui-transfer-list-demo>
|
||||
}
|
||||
|
||||
@case ("tag-input") {
|
||||
<ui-tag-input-demo></ui-tag-input-demo>
|
||||
}
|
||||
|
||||
@case ("stack") {
|
||||
<ui-stack-demo></ui-stack-demo>
|
||||
}
|
||||
|
||||
@case ("box") {
|
||||
<ui-box-demo></ui-box-demo>
|
||||
}
|
||||
|
||||
@case ("center") {
|
||||
<ui-center-demo></ui-center-demo>
|
||||
}
|
||||
|
||||
@case ("aspect-ratio") {
|
||||
<ui-aspect-ratio-demo></ui-aspect-ratio-demo>
|
||||
}
|
||||
|
||||
@case ("bento-grid") {
|
||||
<ui-bento-grid-demo></ui-bento-grid-demo>
|
||||
}
|
||||
|
||||
@case ("breakpoint-container") {
|
||||
<ui-breakpoint-container-demo></ui-breakpoint-container-demo>
|
||||
}
|
||||
|
||||
@case ("section") {
|
||||
<ui-section-demo></ui-section-demo>
|
||||
}
|
||||
|
||||
@case ("flex") {
|
||||
<ui-flex-demo></ui-flex-demo>
|
||||
}
|
||||
|
||||
@case ("column") {
|
||||
<ui-column-demo></ui-column-demo>
|
||||
}
|
||||
|
||||
@case ("sidebar-layout") {
|
||||
<ui-sidebar-layout-demo></ui-sidebar-layout-demo>
|
||||
}
|
||||
|
||||
@case ("scroll-container") {
|
||||
<ui-scroll-container-demo></ui-scroll-container-demo>
|
||||
}
|
||||
|
||||
@case ("infinite-scroll-container") {
|
||||
<ui-infinite-scroll-container-demo></ui-infinite-scroll-container-demo>
|
||||
}
|
||||
|
||||
@case ("tabs-container") {
|
||||
<ui-tabs-container-demo></ui-tabs-container-demo>
|
||||
}
|
||||
|
||||
@case ("dashboard-shell") {
|
||||
<ui-dashboard-shell-demo></ui-dashboard-shell-demo>
|
||||
}
|
||||
|
||||
@case ("grid-container") {
|
||||
<ui-grid-container-demo></ui-grid-container-demo>
|
||||
}
|
||||
|
||||
@case ("feed-layout") {
|
||||
<ui-feed-layout-demo></ui-feed-layout-demo>
|
||||
}
|
||||
|
||||
@case ("list-detail-layout") {
|
||||
<ui-list-detail-layout-demo></ui-list-detail-layout-demo>
|
||||
}
|
||||
|
||||
@case ("supporting-pane-layout") {
|
||||
<ui-supporting-pane-layout-demo></ui-supporting-pane-layout-demo>
|
||||
}
|
||||
|
||||
@case ("masonry") {
|
||||
<ui-masonry-demo></ui-masonry-demo>
|
||||
}
|
||||
|
||||
@case ("sticky-layout") {
|
||||
<ui-sticky-layout-demo></ui-sticky-layout-demo>
|
||||
}
|
||||
|
||||
@case ("split-view") {
|
||||
<ui-split-view-demo></ui-split-view-demo>
|
||||
}
|
||||
|
||||
@case ("gallery-grid") {
|
||||
<ui-gallery-grid-demo></ui-gallery-grid-demo>
|
||||
}
|
||||
|
||||
@case ("animations") {
|
||||
<app-animations-demo></app-animations-demo>
|
||||
}
|
||||
|
||||
@case ("accessibility") {
|
||||
<app-accessibility-demo></app-accessibility-demo>
|
||||
}
|
||||
|
||||
@case ("data-utils") {
|
||||
<ui-data-utils-demo></ui-data-utils-demo>
|
||||
}
|
||||
|
||||
@case ("hcl-studio") {
|
||||
<app-hcl-studio-demo></app-hcl-studio-demo>
|
||||
}
|
||||
|
||||
@case ("font-manager") {
|
||||
<app-font-manager-demo></app-font-manager-demo>
|
||||
}
|
||||
|
||||
@case ("code-display") {
|
||||
<app-code-display-demo></app-code-display-demo>
|
||||
}
|
||||
|
||||
@case ("backgrounds") {
|
||||
<app-backgrounds-demo></app-backgrounds-demo>
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
`,
|
||||
imports: [AvatarDemoComponent, ButtonDemoComponent, IconButtonDemoComponent, CardDemoComponent,
|
||||
ChipDemoComponent, TableDemoComponent, BadgeDemoComponent,
|
||||
MenuDemoComponent, InputDemoComponent, KanbanBoardDemoComponent,
|
||||
RadioDemoComponent, CheckboxDemoComponent,
|
||||
SearchDemoComponent, SwitchDemoComponent, ProgressDemoComponent,
|
||||
AppbarDemoComponent, BottomNavigationDemoComponent, FontAwesomeDemoComponent, ImageContainerDemoComponent,
|
||||
CarouselDemoComponent, VideoPlayerDemoComponent, ListDemoComponent,
|
||||
ModalDemoComponent, DrawerDemoComponent, DatePickerDemoComponent, TimePickerDemoComponent,
|
||||
GridSystemDemoComponent, SpacerDemoComponent, ContainerDemoComponent, PaginationDemoComponent,
|
||||
SkeletonLoaderDemoComponent, EmptyStateDemoComponent, FileUploadDemoComponent, FormFieldDemoComponent,
|
||||
AutocompleteDemoComponent, BackdropDemoComponent, OverlayContainerDemoComponent, LoadingSpinnerDemoComponent,
|
||||
ProgressCircleDemoComponent, RangeSliderDemoComponent, ColorPickerDemoComponent, DividerDemoComponent, TooltipDemoComponent, AccordionDemoComponent,
|
||||
PopoverDemoComponent, AlertDemoComponent, SnackbarDemoComponent, ToastDemoComponent, TreeViewDemoComponent, TimelineDemoComponent, StepperDemoComponent, FabMenuDemoComponent, EnhancedTableDemoComponent, SplitButtonDemoComponent, CommandPaletteDemoComponent, FloatingToolbarDemoComponent, TransferListDemoComponent, TagInputDemoComponent, StackDemoComponent, BoxDemoComponent, CenterDemoComponent, AspectRatioDemoComponent, BentoGridDemoComponent, BreakpointContainerDemoComponent, SectionDemoComponent, FlexDemoComponent, ColumnDemoComponent, SidebarLayoutDemoComponent, ScrollContainerDemoComponent, InfiniteScrollContainerDemoComponent, TabsContainerDemoComponent, DashboardShellDemoComponent, GridContainerDemoComponent, FeedLayoutDemoComponent, ListDetailLayoutDemoComponent, SupportingPaneLayoutDemoComponent, MasonryDemoComponent, StickyLayoutDemoComponent, SplitViewDemoComponent, GalleryGridDemoComponent, AnimationsDemoComponent, AccessibilityDemoComponent, SelectDemoComponent, TextareaDemoComponent, DataUtilsDemoComponent, HclStudioDemoComponent, FontManagerDemoComponent, CodeDisplayDemoComponent, BackgroundsDemoComponent]
|
||||
})
|
||||
|
||||
|
||||
export class DemoRoutes {
|
||||
|
||||
@Input() route: string;
|
||||
|
||||
constructor() {
|
||||
this.route = "home"
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
@use "../../../../../ui-design-system/src/styles/semantic/index" as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-md;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-lg;
|
||||
|
||||
h3 {
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: $semantic-typography-font-size-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
align-items: center;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: $semantic-color-text-secondary;
|
||||
font-size: $semantic-typography-font-size-md;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: $semantic-typography-font-size-md;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DividerComponent } from '../../../../../ui-essentials/src/lib/components/layout';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-divider-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, DividerComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Divider Demo</h2>
|
||||
|
||||
<!-- Orientation -->
|
||||
<section class="demo-section">
|
||||
<h3>Orientation</h3>
|
||||
<div class="demo-row">
|
||||
<div style="width: 100%;">
|
||||
<p>Content above</p>
|
||||
<ui-divider orientation="horizontal"></ui-divider>
|
||||
<p>Content below</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo-row" style="height: 100px; display: flex; align-items: center;">
|
||||
<p>Left content</p>
|
||||
<ui-divider orientation="vertical"></ui-divider>
|
||||
<p>Right content</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Style Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Style Variants</h3>
|
||||
<div class="demo-column">
|
||||
<div>
|
||||
<p>Solid divider</p>
|
||||
<ui-divider variant="solid"></ui-divider>
|
||||
</div>
|
||||
<div>
|
||||
<p>Dashed divider</p>
|
||||
<ui-divider variant="dashed"></ui-divider>
|
||||
</div>
|
||||
<div>
|
||||
<p>Dotted divider</p>
|
||||
<ui-divider variant="dotted"></ui-divider>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Thickness Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Thickness</h3>
|
||||
<div class="demo-column">
|
||||
<div>
|
||||
<p>Thin divider</p>
|
||||
<ui-divider thickness="thin"></ui-divider>
|
||||
</div>
|
||||
<div>
|
||||
<p>Default thickness</p>
|
||||
<ui-divider thickness="default"></ui-divider>
|
||||
</div>
|
||||
<div>
|
||||
<p>Thick divider</p>
|
||||
<ui-divider thickness="thick"></ui-divider>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- With Content -->
|
||||
<section class="demo-section">
|
||||
<h3>With Content</h3>
|
||||
<div class="demo-column">
|
||||
<ui-divider>OR</ui-divider>
|
||||
<ui-divider>Section Break</ui-divider>
|
||||
<ui-divider>More Content</ui-divider>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Combined Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Combined Examples</h3>
|
||||
<div class="demo-column">
|
||||
<ui-divider variant="dashed" thickness="thin">Dashed Thin</ui-divider>
|
||||
<ui-divider variant="dotted" thickness="thick">Dotted Thick</ui-divider>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Vertical Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Vertical Examples</h3>
|
||||
<div style="display: flex; height: 80px; align-items: center; gap: 16px;">
|
||||
<span>Item 1</span>
|
||||
<ui-divider orientation="vertical" variant="solid" thickness="thin"></ui-divider>
|
||||
<span>Item 2</span>
|
||||
<ui-divider orientation="vertical" variant="dashed"></ui-divider>
|
||||
<span>Item 3</span>
|
||||
<ui-divider orientation="vertical" variant="dotted" thickness="thick"></ui-divider>
|
||||
<span>Item 4</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './divider-demo.component.scss'
|
||||
})
|
||||
export class DividerDemoComponent {}
|
||||
@@ -0,0 +1,158 @@
|
||||
@use '../../../../../ui-design-system/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 '../../../../../ui-design-system/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/empty-state/empty-state.component';
|
||||
|
||||
@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,387 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.enhanced-table-demo {
|
||||
padding: $semantic-spacing-layout-section-lg;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
h3 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
p {
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
}
|
||||
|
||||
// Demo Controls
|
||||
.demo-controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
|
||||
.demo-control-group {
|
||||
h3 {
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: pointer;
|
||||
|
||||
input[type="checkbox"] {
|
||||
accent-color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $semantic-color-focus;
|
||||
box-shadow: 0 0 0 2px rgba($semantic-color-focus, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
color: $semantic-color-text-tertiary;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-primary;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
margin: $semantic-spacing-component-xs;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
border-color: $semantic-color-border-primary;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: $semantic-color-surface-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
&--small {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
font-size: map-get($semantic-typography-button-small, font-size);
|
||||
}
|
||||
}
|
||||
|
||||
// Performance Metrics
|
||||
.performance-metrics {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
border-left: 4px solid $semantic-color-info;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
|
||||
.metric-label {
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Table Container
|
||||
.table-container {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
overflow: hidden;
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
}
|
||||
|
||||
// Cell Templates
|
||||
.salary-cell {
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
color: $semantic-color-success;
|
||||
}
|
||||
|
||||
.performance-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.performance-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-full;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.performance-fill {
|
||||
height: 100%;
|
||||
border-radius: $semantic-border-radius-full;
|
||||
transition: width $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
|
||||
&.performance-excellent {
|
||||
background: $semantic-color-success;
|
||||
}
|
||||
|
||||
&.performance-good {
|
||||
background: $semantic-color-info;
|
||||
}
|
||||
|
||||
&.performance-average {
|
||||
background: $semantic-color-warning;
|
||||
}
|
||||
|
||||
&.performance-poor {
|
||||
background: $semantic-color-danger;
|
||||
}
|
||||
}
|
||||
|
||||
.performance-text {
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
min-width: 35px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
border: none;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&--primary {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
|
||||
&:hover {
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: $semantic-color-danger;
|
||||
color: $semantic-color-on-danger;
|
||||
|
||||
&:hover {
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
// State Display
|
||||
.state-display {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
}
|
||||
|
||||
.state-section {
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
|
||||
h4 {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.state-content {
|
||||
p {
|
||||
margin: 0;
|
||||
color: $semantic-color-text-tertiary;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.sort-item,
|
||||
.filter-item {
|
||||
padding: $semantic-spacing-component-xs 0;
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
color: $semantic-color-text-primary;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Feature Demo
|
||||
.feature-demo {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
border-left: 4px solid $semantic-color-primary;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
h4 {
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 1200px) {
|
||||
.enhanced-table-demo {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.state-display {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.performance-cell {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
|
||||
.performance-bar {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
// Print styles
|
||||
@media print {
|
||||
.demo-controls,
|
||||
.performance-metrics,
|
||||
.state-display,
|
||||
.feature-demo {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,719 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit, TemplateRef, ViewChild, AfterViewInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
EnhancedTableComponent,
|
||||
EnhancedTableColumn,
|
||||
EnhancedTableVariant,
|
||||
TableSort,
|
||||
TableFilter,
|
||||
EnhancedTableSortEvent,
|
||||
EnhancedTableFilterEvent,
|
||||
EnhancedTableColumnResizeEvent,
|
||||
EnhancedTableColumnReorderEvent,
|
||||
VirtualScrollConfig
|
||||
} from '../../../../../ui-essentials/src/lib/components/data-display/table/enhanced-table.component';
|
||||
import { StatusBadgeComponent } from '../../../../../ui-essentials/src/lib/components/feedback/status-badge.component';
|
||||
|
||||
interface Employee {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
department: string;
|
||||
position: string;
|
||||
salary: number;
|
||||
status: 'active' | 'inactive' | 'pending';
|
||||
joinDate: string;
|
||||
performance: number;
|
||||
location: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-enhanced-table-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
EnhancedTableComponent,
|
||||
StatusBadgeComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Enhanced Table Component Showcase</h2>
|
||||
<p style="margin-bottom: 2rem; color: #6c757d;">Enterprise-grade data table with virtual scrolling, column resizing/reordering, and advanced filtering.</p>
|
||||
|
||||
<!-- Basic Enhanced Table -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Basic Enhanced Table</h3>
|
||||
<ui-enhanced-table
|
||||
[data]="smallDataset"
|
||||
[columns]="basicColumns"
|
||||
variant="default"
|
||||
[virtualScrolling]="{ enabled: false, itemHeight: 48, buffer: 5 }"
|
||||
[showFilterRow]="true"
|
||||
[maxHeight]="'400px'"
|
||||
(sortChange)="handleSort($event)"
|
||||
(filterChange)="handleFilter($event)"
|
||||
(columnResize)="handleColumnResize($event)"
|
||||
(columnReorder)="handleColumnReorder($event)"
|
||||
(rowClick)="handleRowClick($event)">
|
||||
</ui-enhanced-table>
|
||||
</section>
|
||||
|
||||
<!-- Table Variants -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Table Variants</h3>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Striped Enhanced Table</h4>
|
||||
<ui-enhanced-table
|
||||
[data]="smallDataset.slice(0, 5)"
|
||||
[columns]="basicColumns"
|
||||
variant="striped"
|
||||
[virtualScrolling]="{ enabled: false, itemHeight: 48, buffer: 5 }"
|
||||
[showFilterRow]="false"
|
||||
[maxHeight]="'300px'">
|
||||
</ui-enhanced-table>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Bordered Enhanced Table</h4>
|
||||
<ui-enhanced-table
|
||||
[data]="smallDataset.slice(0, 5)"
|
||||
[columns]="basicColumns"
|
||||
variant="bordered"
|
||||
[virtualScrolling]="{ enabled: false, itemHeight: 48, buffer: 5 }"
|
||||
[showFilterRow]="false"
|
||||
[maxHeight]="'300px'">
|
||||
</ui-enhanced-table>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4>Minimal Enhanced Table</h4>
|
||||
<ui-enhanced-table
|
||||
[data]="smallDataset.slice(0, 5)"
|
||||
[columns]="basicColumns"
|
||||
variant="minimal"
|
||||
[virtualScrolling]="{ enabled: false, itemHeight: 48, buffer: 5 }"
|
||||
[showFilterRow]="false"
|
||||
[maxHeight]="'300px'">
|
||||
</ui-enhanced-table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Virtual Scrolling Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Virtual Scrolling ({{ largeDataset.length.toLocaleString() }} Records)</h3>
|
||||
<p style="margin-bottom: 1rem; color: #6c757d;">
|
||||
Virtual scrolling efficiently handles large datasets by only rendering visible rows.
|
||||
Performance metrics are displayed below the table.
|
||||
</p>
|
||||
|
||||
<ui-enhanced-table
|
||||
[data]="largeDataset"
|
||||
[columns]="performanceColumns"
|
||||
variant="default"
|
||||
[virtualScrolling]="virtualConfig"
|
||||
[showFilterRow]="true"
|
||||
[maxHeight]="'500px'"
|
||||
[cellTemplates]="cellTemplates"
|
||||
(sortChange)="handleSort($event)"
|
||||
(filterChange)="handleFilter($event)"
|
||||
(columnResize)="handleColumnResize($event)"
|
||||
(columnReorder)="handleColumnReorder($event)">
|
||||
</ui-enhanced-table>
|
||||
|
||||
@if (performanceMetrics) {
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4>Performance Metrics:</h4>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 0.5rem;">
|
||||
<div><strong>Render Time:</strong> {{ performanceMetrics.renderTime }}ms</div>
|
||||
<div><strong>Sort Time:</strong> {{ performanceMetrics.sortTime }}ms</div>
|
||||
<div><strong>Filter Time:</strong> {{ performanceMetrics.filterTime }}ms</div>
|
||||
<div><strong>Visible Rows:</strong> {{ performanceMetrics.visibleRows }}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Advanced Features -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Advanced Features with Custom Cell Templates</h3>
|
||||
<ui-enhanced-table
|
||||
[data]="smallDataset"
|
||||
[columns]="advancedColumns"
|
||||
variant="default"
|
||||
[virtualScrolling]="{ enabled: false, itemHeight: 48, buffer: 5 }"
|
||||
[showFilterRow]="true"
|
||||
[maxHeight]="'400px'"
|
||||
[cellTemplates]="cellTemplates"
|
||||
(sortChange)="handleSort($event)"
|
||||
(filterChange)="handleFilter($event)"
|
||||
(rowClick)="handleRowClick($event)">
|
||||
</ui-enhanced-table>
|
||||
|
||||
<!-- 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 #salaryTemplate let-value>
|
||||
<span style="font-weight: 600; color: #28a745;">
|
||||
{{ value | currency:'USD':'symbol':'1.0-0' }}
|
||||
</span>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #performanceTemplate let-value>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<div style="flex: 1; background: #e9ecef; border-radius: 4px; height: 8px; overflow: hidden;">
|
||||
<div
|
||||
[style.width.%]="value"
|
||||
[style.background-color]="getPerformanceColor(value)"
|
||||
style="height: 100%; transition: width 0.3s ease;">
|
||||
</div>
|
||||
</div>
|
||||
<span style="font-size: 0.875rem; min-width: 35px;">{{ value }}%</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #actionsTemplate let-row="row" let-index="index">
|
||||
<div style="display: flex; gap: 0.5rem; justify-content: center;">
|
||||
<button
|
||||
(click)="editEmployee(row)"
|
||||
style="padding: 0.25rem 0.5rem; border: 1px solid #007bff; background: #007bff; color: white; border-radius: 4px; cursor: pointer; font-size: 0.75rem;">
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
(click)="deleteEmployee(row.id)"
|
||||
style="padding: 0.25rem 0.5rem; border: 1px solid #dc3545; background: #dc3545; color: white; border-radius: 4px; cursor: pointer; font-size: 0.75rem;">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</section>
|
||||
|
||||
<!-- Column Configuration Demo -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<h3>Interactive Column Configuration</h3>
|
||||
<div style="display: flex; gap: 2rem; margin-bottom: 1.5rem; flex-wrap: wrap;">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="checkbox" [(ngModel)]="enableResize" (change)="updateTableConfig()">
|
||||
Enable Column Resizing
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="checkbox" [(ngModel)]="enableReorder" (change)="updateTableConfig()">
|
||||
Enable Column Reordering
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="checkbox" [(ngModel)]="showTableFilters" (change)="updateTableConfig()">
|
||||
Show Filters
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="checkbox" [(ngModel)]="enableVirtual" (change)="updateTableConfig()">
|
||||
Enable Virtual Scrolling
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ui-enhanced-table
|
||||
[data]="configDataset"
|
||||
[columns]="configColumns"
|
||||
variant="default"
|
||||
[virtualScrolling]="configVirtualConfig"
|
||||
[showFilterRow]="showTableFilters"
|
||||
[maxHeight]="'400px'"
|
||||
(sortChange)="handleSort($event)"
|
||||
(filterChange)="handleFilter($event)"
|
||||
(columnResize)="handleColumnResize($event)"
|
||||
(columnReorder)="handleColumnReorder($event)">
|
||||
</ui-enhanced-table>
|
||||
</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 Enhanced Table:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-enhanced-table
|
||||
[data]="employees"
|
||||
[columns]="columns"
|
||||
variant="default"
|
||||
[enableColumnResize]="true"
|
||||
[enableColumnReorder]="true"
|
||||
[showFilters]="true"
|
||||
[height]="500"
|
||||
(sort)="handleSort($event)"
|
||||
(filter)="handleFilter($event)">
|
||||
</ui-enhanced-table></code></pre>
|
||||
|
||||
<h4>With Virtual Scrolling:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code><ui-enhanced-table
|
||||
[data]="largeDataset"
|
||||
[columns]="columns"
|
||||
[virtualScrollConfig]="{{ '{' }}
|
||||
enabled: true,
|
||||
itemHeight: 48,
|
||||
bufferSize: 10
|
||||
{{ '}' }}"
|
||||
[height]="600">
|
||||
</ui-enhanced-table></code></pre>
|
||||
|
||||
<h4>Column Configuration:</h4>
|
||||
<pre style="background: #fff; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>columns: EnhancedTableColumn[] = [
|
||||
{{ '{' }}
|
||||
key: 'name',
|
||||
label: 'Name',
|
||||
width: 150,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
filterType: 'text',
|
||||
resizable: true,
|
||||
draggable: true
|
||||
{{ '}' }},
|
||||
{{ '{' }}
|
||||
key: 'salary',
|
||||
label: 'Salary',
|
||||
width: 120,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
filterType: 'number',
|
||||
resizable: true
|
||||
{{ '}' }}
|
||||
];</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)="addRandomEmployee()" style="padding: 0.5rem 1rem; border: 1px solid #28a745; background: #28a745; color: white; border-radius: 4px; cursor: pointer;">
|
||||
Add Random Employee
|
||||
</button>
|
||||
<button (click)="generateLargeDataset()" style="padding: 0.5rem 1rem; border: 1px solid #17a2b8; background: #17a2b8; color: white; border-radius: 4px; cursor: pointer;">
|
||||
Generate Large Dataset (10k)
|
||||
</button>
|
||||
<button (click)="clearAllEmployees()" style="padding: 0.5rem 1rem; border: 1px solid #dc3545; background: #dc3545; color: white; border-radius: 4px; cursor: pointer;">
|
||||
Clear All Data
|
||||
</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;
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class EnhancedTableDemoComponent implements OnInit, AfterViewInit {
|
||||
@ViewChild('statusTemplate') statusTemplate!: TemplateRef<any>;
|
||||
@ViewChild('salaryTemplate') salaryTemplate!: TemplateRef<any>;
|
||||
@ViewChild('performanceTemplate') performanceTemplate!: TemplateRef<any>;
|
||||
@ViewChild('actionsTemplate') actionsTemplate!: TemplateRef<any>;
|
||||
|
||||
// Demo state
|
||||
lastAction = '';
|
||||
performanceMetrics: any = null;
|
||||
|
||||
// Configuration
|
||||
enableResize = true;
|
||||
enableReorder = true;
|
||||
showTableFilters = true;
|
||||
enableVirtual = false;
|
||||
|
||||
// Cell templates
|
||||
cellTemplates: Record<string, TemplateRef<any>> = {};
|
||||
|
||||
// Virtual scroll config
|
||||
virtualConfig: VirtualScrollConfig = {
|
||||
enabled: true,
|
||||
itemHeight: 48,
|
||||
buffer: 10
|
||||
};
|
||||
|
||||
configVirtualConfig: VirtualScrollConfig = {
|
||||
enabled: false,
|
||||
itemHeight: 48,
|
||||
buffer: 10
|
||||
};
|
||||
|
||||
// Sample data
|
||||
smallDataset: Employee[] = [];
|
||||
largeDataset: Employee[] = [];
|
||||
configDataset: Employee[] = [];
|
||||
|
||||
// Column definitions
|
||||
basicColumns: EnhancedTableColumn[] = [
|
||||
{ key: 'name', label: 'Name', width: 150, sortable: true, filterable: true, filterType: 'text', resizable: true, draggable: true },
|
||||
{ key: 'email', label: 'Email', width: 200, sortable: true, filterable: true, filterType: 'text', resizable: true, draggable: true },
|
||||
{ key: 'department', label: 'Department', width: 140, sortable: true, filterable: true, filterType: 'select', filterOptions: this.getDepartmentOptions(), resizable: true, draggable: true },
|
||||
{ key: 'position', label: 'Position', width: 160, sortable: true, filterable: true, filterType: 'text', resizable: true, draggable: true },
|
||||
{ key: 'joinDate', label: 'Join Date', width: 120, sortable: true, filterable: true, filterType: 'date', resizable: true, draggable: true }
|
||||
];
|
||||
|
||||
performanceColumns: EnhancedTableColumn[] = [
|
||||
{ key: 'name', label: 'Name', width: 120, sortable: true, filterable: true, filterType: 'text', resizable: true, draggable: true, sticky: true },
|
||||
{ key: 'department', label: 'Department', width: 120, sortable: true, filterable: true, filterType: 'select', filterOptions: this.getDepartmentOptions(), resizable: true, draggable: true },
|
||||
{ key: 'position', label: 'Position', width: 140, sortable: true, filterable: true, filterType: 'text', resizable: true, draggable: true },
|
||||
{ key: 'salary', label: 'Salary', width: 100, sortable: true, filterable: true, filterType: 'number', resizable: true, draggable: true, align: 'right' },
|
||||
{ key: 'performance', label: 'Performance', width: 120, sortable: true, filterable: true, filterType: 'number', resizable: true, draggable: true },
|
||||
{ key: 'location', label: 'Location', width: 120, sortable: true, filterable: true, filterType: 'select', filterOptions: this.getLocationOptions(), resizable: true, draggable: true }
|
||||
];
|
||||
|
||||
advancedColumns: EnhancedTableColumn[] = [
|
||||
{ key: 'name', label: 'Name', width: 150, sortable: true, filterable: true, filterType: 'text', resizable: true, draggable: true },
|
||||
{ key: 'email', label: 'Email', width: 200, sortable: true, filterable: true, filterType: 'text', resizable: true, draggable: true },
|
||||
{ key: 'status', label: 'Status', width: 100, sortable: true, filterable: true, filterType: 'select', filterOptions: this.getStatusOptions(), resizable: true, draggable: true, align: 'center' },
|
||||
{ key: 'salary', label: 'Salary', width: 120, sortable: true, filterable: true, filterType: 'number', resizable: true, draggable: true, align: 'right' },
|
||||
{ key: 'performance', label: 'Performance', width: 140, sortable: true, filterable: true, filterType: 'number', resizable: true, draggable: true },
|
||||
{ key: 'actions', label: 'Actions', width: 100, resizable: true, align: 'center' }
|
||||
];
|
||||
|
||||
configColumns: EnhancedTableColumn[] = [
|
||||
{ key: 'name', label: 'Name', width: 150, sortable: true, filterable: true, filterType: 'text', resizable: true, draggable: true },
|
||||
{ key: 'department', label: 'Department', width: 140, sortable: true, filterable: true, filterType: 'select', filterOptions: this.getDepartmentOptions(), resizable: true, draggable: true },
|
||||
{ key: 'salary', label: 'Salary', width: 120, sortable: true, filterable: true, filterType: 'number', resizable: true, draggable: true, align: 'right' },
|
||||
{ key: 'joinDate', label: 'Join Date', width: 120, sortable: true, filterable: true, filterType: 'date', resizable: true, draggable: true }
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initializeData();
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
// Set up cell templates after view init
|
||||
setTimeout(() => {
|
||||
this.cellTemplates = {
|
||||
'status': this.statusTemplate,
|
||||
'salary': this.salaryTemplate,
|
||||
'performance': this.performanceTemplate,
|
||||
'actions': this.actionsTemplate
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
handleSort(event: EnhancedTableSortEvent): void {
|
||||
this.lastAction = `Sorted by ${event.sorting.map(s => `${s.column} (${s.direction})`).join(', ')}`;
|
||||
console.log('Sort event:', event);
|
||||
}
|
||||
|
||||
handleFilter(event: EnhancedTableFilterEvent): void {
|
||||
this.lastAction = `Filtered: ${event.filters.length} filters active`;
|
||||
console.log('Filter event:', event);
|
||||
}
|
||||
|
||||
handleColumnResize(event: EnhancedTableColumnResizeEvent): void {
|
||||
this.lastAction = `Resized column "${event.column}" to ${event.width}px`;
|
||||
console.log('Column resize event:', event);
|
||||
}
|
||||
|
||||
handleColumnReorder(event: EnhancedTableColumnReorderEvent): void {
|
||||
this.lastAction = `Reordered columns from index ${event.fromIndex} to ${event.toIndex}`;
|
||||
console.log('Column reorder event:', event);
|
||||
}
|
||||
|
||||
handleRowClick(event: {row: Employee, index: number}): void {
|
||||
this.lastAction = `Clicked row: ${event.row.name} (index: ${event.index})`;
|
||||
console.log('Row click:', event);
|
||||
}
|
||||
|
||||
editEmployee(employee: Employee): void {
|
||||
this.lastAction = `Editing employee: ${employee.name}`;
|
||||
console.log('Edit employee:', employee);
|
||||
}
|
||||
|
||||
// Configuration updates
|
||||
updateTableConfig(): void {
|
||||
this.configVirtualConfig = {
|
||||
...this.configVirtualConfig,
|
||||
enabled: this.enableVirtual
|
||||
};
|
||||
this.lastAction = `Updated table configuration`;
|
||||
}
|
||||
|
||||
// Template helpers
|
||||
getStatusVariant(status: string): 'success' | 'warning' | 'danger' | 'info' | 'neutral' | 'primary' | 'secondary' {
|
||||
switch (status?.toLowerCase()) {
|
||||
case 'active':
|
||||
return 'success';
|
||||
case 'pending':
|
||||
return 'warning';
|
||||
case 'inactive':
|
||||
return 'danger';
|
||||
default:
|
||||
return 'neutral';
|
||||
}
|
||||
}
|
||||
|
||||
getPerformanceColor(performance: number): string {
|
||||
if (performance >= 90) return '#28a745';
|
||||
if (performance >= 80) return '#17a2b8';
|
||||
if (performance >= 70) return '#ffc107';
|
||||
return '#dc3545';
|
||||
}
|
||||
|
||||
|
||||
// Filter options
|
||||
getDepartmentOptions() {
|
||||
return [
|
||||
{ label: 'Engineering', value: 'Engineering' },
|
||||
{ label: 'Marketing', value: 'Marketing' },
|
||||
{ label: 'Sales', value: 'Sales' },
|
||||
{ label: 'HR', value: 'HR' },
|
||||
{ label: 'Finance', value: 'Finance' }
|
||||
];
|
||||
}
|
||||
|
||||
getStatusOptions() {
|
||||
return [
|
||||
{ label: 'Active', value: 'active' },
|
||||
{ label: 'Inactive', value: 'inactive' },
|
||||
{ label: 'Pending', value: 'pending' }
|
||||
];
|
||||
}
|
||||
|
||||
getLocationOptions() {
|
||||
return [
|
||||
{ label: 'New York', value: 'New York' },
|
||||
{ label: 'San Francisco', value: 'San Francisco' },
|
||||
{ label: 'London', value: 'London' },
|
||||
{ label: 'Toronto', value: 'Toronto' },
|
||||
{ label: 'Sydney', value: 'Sydney' }
|
||||
];
|
||||
}
|
||||
|
||||
// Demo actions
|
||||
addRandomEmployee(): void {
|
||||
const names = ['John Doe', 'Jane Smith', 'Mike Johnson', 'Sarah Wilson', 'Tom Anderson', 'Lisa Brown', 'David Clark', 'Emma Davis'];
|
||||
const departments = ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance'];
|
||||
const positions = ['Manager', 'Developer', 'Analyst', 'Coordinator', 'Specialist'];
|
||||
const statuses: ('active' | 'inactive' | 'pending')[] = ['active', 'inactive', 'pending'];
|
||||
const locations = ['New York', 'San Francisco', 'London', 'Toronto', 'Sydney'];
|
||||
|
||||
const newEmployee: Employee = {
|
||||
id: Math.max(...this.smallDataset.map(e => e.id), 0) + 1,
|
||||
name: names[Math.floor(Math.random() * names.length)],
|
||||
email: `user${Date.now()}@company.com`,
|
||||
department: departments[Math.floor(Math.random() * departments.length)],
|
||||
position: positions[Math.floor(Math.random() * positions.length)],
|
||||
salary: Math.floor(Math.random() * 100000) + 50000,
|
||||
status: statuses[Math.floor(Math.random() * statuses.length)],
|
||||
joinDate: new Date(2020 + Math.floor(Math.random() * 5), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toISOString().split('T')[0],
|
||||
performance: Math.floor(Math.random() * 41) + 60,
|
||||
location: locations[Math.floor(Math.random() * locations.length)]
|
||||
};
|
||||
|
||||
this.smallDataset = [...this.smallDataset, newEmployee];
|
||||
this.configDataset = [...this.configDataset, newEmployee];
|
||||
this.lastAction = `Added new employee: ${newEmployee.name}`;
|
||||
}
|
||||
|
||||
generateLargeDataset(): void {
|
||||
this.largeDataset = this.createLargeDataset(10000);
|
||||
this.performanceMetrics = {
|
||||
renderTime: Math.floor(Math.random() * 50) + 10,
|
||||
sortTime: 0,
|
||||
filterTime: 0,
|
||||
visibleRows: Math.min(10, this.largeDataset.length)
|
||||
};
|
||||
this.lastAction = `Generated ${this.largeDataset.length.toLocaleString()} employee records`;
|
||||
}
|
||||
|
||||
clearAllEmployees(): void {
|
||||
this.smallDataset = [];
|
||||
this.largeDataset = [];
|
||||
this.configDataset = [];
|
||||
this.performanceMetrics = null;
|
||||
this.lastAction = 'Cleared all employee data';
|
||||
}
|
||||
|
||||
resetDemo(): void {
|
||||
this.initializeData();
|
||||
this.lastAction = 'Demo reset to initial state';
|
||||
}
|
||||
|
||||
// Data generation
|
||||
private initializeData(): void {
|
||||
this.smallDataset = this.createSmallDataset();
|
||||
this.configDataset = [...this.smallDataset];
|
||||
this.largeDataset = this.createLargeDataset(50000);
|
||||
this.performanceMetrics = {
|
||||
renderTime: Math.floor(Math.random() * 30) + 10,
|
||||
sortTime: 0,
|
||||
filterTime: 0,
|
||||
visibleRows: Math.min(10, this.largeDataset.length)
|
||||
};
|
||||
}
|
||||
|
||||
private createSmallDataset(): Employee[] {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Alice Johnson',
|
||||
email: 'alice.johnson@company.com',
|
||||
department: 'Engineering',
|
||||
position: 'Senior Developer',
|
||||
salary: 95000,
|
||||
status: 'active',
|
||||
joinDate: '2022-01-15',
|
||||
performance: 92,
|
||||
location: 'San Francisco'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Bob Smith',
|
||||
email: 'bob.smith@company.com',
|
||||
department: 'Marketing',
|
||||
position: 'Marketing Manager',
|
||||
salary: 78000,
|
||||
status: 'active',
|
||||
joinDate: '2021-06-10',
|
||||
performance: 85,
|
||||
location: 'New York'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Carol Williams',
|
||||
email: 'carol.williams@company.com',
|
||||
department: 'Sales',
|
||||
position: 'Sales Representative',
|
||||
salary: 65000,
|
||||
status: 'pending',
|
||||
joinDate: '2023-03-22',
|
||||
performance: 78,
|
||||
location: 'London'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'David Brown',
|
||||
email: 'david.brown@company.com',
|
||||
department: 'HR',
|
||||
position: 'HR Specialist',
|
||||
salary: 58000,
|
||||
status: 'active',
|
||||
joinDate: '2022-08-05',
|
||||
performance: 88,
|
||||
location: 'Toronto'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Eve Davis',
|
||||
email: 'eve.davis@company.com',
|
||||
department: 'Finance',
|
||||
position: 'Financial Analyst',
|
||||
salary: 72000,
|
||||
status: 'active',
|
||||
joinDate: '2021-11-18',
|
||||
performance: 91,
|
||||
location: 'Sydney'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Frank Miller',
|
||||
email: 'frank.miller@company.com',
|
||||
department: 'Engineering',
|
||||
position: 'DevOps Engineer',
|
||||
salary: 88000,
|
||||
status: 'inactive',
|
||||
joinDate: '2020-04-12',
|
||||
performance: 82,
|
||||
location: 'San Francisco'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private createLargeDataset(count: number): Employee[] {
|
||||
const dataset: Employee[] = [];
|
||||
const firstNames = ['Alice', 'Bob', 'Carol', 'David', 'Eve', 'Frank', 'Grace', 'Henry', 'Ivy', 'Jack'];
|
||||
const lastNames = ['Johnson', 'Smith', 'Williams', 'Brown', 'Davis', 'Miller', 'Wilson', 'Moore', 'Taylor', 'Anderson'];
|
||||
const departments = ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance'];
|
||||
const positions = ['Manager', 'Senior Developer', 'Developer', 'Analyst', 'Coordinator', 'Specialist', 'Representative'];
|
||||
const statuses: ('active' | 'inactive' | 'pending')[] = ['active', 'inactive', 'pending'];
|
||||
const locations = ['New York', 'San Francisco', 'London', 'Toronto', 'Sydney'];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)];
|
||||
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)];
|
||||
|
||||
dataset.push({
|
||||
id: i + 1000,
|
||||
name: `${firstName} ${lastName}`,
|
||||
email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@company.com`,
|
||||
department: departments[Math.floor(Math.random() * departments.length)],
|
||||
position: positions[Math.floor(Math.random() * positions.length)],
|
||||
salary: Math.floor(Math.random() * 100000) + 50000,
|
||||
status: statuses[Math.floor(Math.random() * statuses.length)],
|
||||
joinDate: new Date(2020 + Math.floor(Math.random() * 5), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toISOString().split('T')[0],
|
||||
performance: Math.floor(Math.random() * 41) + 60,
|
||||
location: locations[Math.floor(Math.random() * locations.length)]
|
||||
});
|
||||
}
|
||||
|
||||
return dataset;
|
||||
}
|
||||
|
||||
deleteEmployee(id: number): void {
|
||||
this.smallDataset = this.smallDataset.filter(emp => emp.id !== id);
|
||||
this.configDataset = this.configDataset.filter(emp => emp.id !== id);
|
||||
this.lastAction = `Deleted employee with ID: ${id}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-lg;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
p {
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-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-card-radius;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
|
||||
h3 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
align-items: flex-start;
|
||||
|
||||
&--spaced {
|
||||
justify-content: space-around;
|
||||
|
||||
.demo-example {
|
||||
min-width: 200px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-example {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-md;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
min-height: 120px;
|
||||
position: relative;
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0 0 $semantic-spacing-component-sm 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
border: $semantic-border-width-1 solid $semantic-color-primary;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
box-shadow: $semantic-shadow-button-rest;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
border-color: $semantic-color-border-primary;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: $semantic-color-surface-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-log {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
padding: $semantic-spacing-component-md;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
|
||||
&__empty {
|
||||
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);
|
||||
color: $semantic-color-text-tertiary;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__entry {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-xs;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-family: $semantic-typography-font-family-mono;
|
||||
font-size: map-get($semantic-typography-caption, font-size);
|
||||
color: $semantic-color-text-tertiary;
|
||||
flex-shrink: 0;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
&__message {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
&--spaced {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-example {
|
||||
min-height: auto;
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure positioned FAB menus are visible above demo content
|
||||
:host {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FabMenuComponent, FabMenuDirection, FabMenuItem, FabMenuPosition, FabMenuSize, FabMenuVariant } from '../../../../../ui-essentials/src/lib/components/buttons/fab-menu';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-fab-menu-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FabMenuComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Floating Action Button Menu Demo</h2>
|
||||
<p>An interactive floating action button menu that expands to reveal multiple action items.</p>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-example">
|
||||
<h4>{{ size | titlecase }}</h4>
|
||||
<ui-fab-menu
|
||||
[size]="size"
|
||||
[menuItems]="basicMenuItems"
|
||||
[triggerIcon]="plusIcon"
|
||||
[closeIcon]="closeIcon"
|
||||
triggerLabel="Open {{ size }} menu"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
</ui-fab-menu>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Color Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Color Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-example">
|
||||
<h4>{{ variant | titlecase }}</h4>
|
||||
<ui-fab-menu
|
||||
[variant]="variant"
|
||||
[menuItems]="getVariantMenuItems(variant)"
|
||||
[triggerIcon]="plusIcon"
|
||||
[closeIcon]="closeIcon"
|
||||
[triggerLabel]="variant + ' menu'"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
</ui-fab-menu>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Direction Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Direction Variants</h3>
|
||||
<div class="demo-row demo-row--spaced">
|
||||
@for (direction of directions; track direction) {
|
||||
<div class="demo-example">
|
||||
<h4>{{ direction | titlecase }}</h4>
|
||||
<ui-fab-menu
|
||||
[direction]="direction"
|
||||
[menuItems]="basicMenuItems"
|
||||
[triggerIcon]="plusIcon"
|
||||
[closeIcon]="closeIcon"
|
||||
[triggerLabel]="direction + ' direction menu'"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
</ui-fab-menu>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Position Variants (Fixed Positioning) -->
|
||||
<section class="demo-section">
|
||||
<h3>Fixed Positions</h3>
|
||||
<p>These examples show fixed positioning variants (click to activate):</p>
|
||||
<div class="demo-row">
|
||||
@for (position of positions; track position) {
|
||||
<button
|
||||
class="demo-button"
|
||||
(click)="togglePositionDemo(position)">
|
||||
Show {{ position | titlecase }} FAB Menu
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (activePositionDemo) {
|
||||
<ui-fab-menu
|
||||
[position]="activePositionDemo"
|
||||
[menuItems]="positionMenuItems"
|
||||
[triggerIcon]="menuIcon"
|
||||
[closeIcon]="closeIcon"
|
||||
[triggerLabel]="activePositionDemo + ' positioned menu'"
|
||||
(opened)="onMenuOpened()"
|
||||
(closed)="onMenuClosed()"
|
||||
(itemClicked)="handlePositionItemClick($event)">
|
||||
</ui-fab-menu>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- States -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-example">
|
||||
<h4>Disabled</h4>
|
||||
<ui-fab-menu
|
||||
[disabled]="true"
|
||||
[menuItems]="basicMenuItems"
|
||||
[triggerIcon]="plusIcon"
|
||||
triggerLabel="Disabled menu">
|
||||
</ui-fab-menu>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>No Backdrop</h4>
|
||||
<ui-fab-menu
|
||||
[backdrop]="false"
|
||||
[menuItems]="basicMenuItems"
|
||||
[triggerIcon]="plusIcon"
|
||||
[closeIcon]="closeIcon"
|
||||
triggerLabel="Menu without backdrop"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
</ui-fab-menu>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Stay Open</h4>
|
||||
<ui-fab-menu
|
||||
[closeOnItemClick]="false"
|
||||
[menuItems]="basicMenuItems"
|
||||
[triggerIcon]="plusIcon"
|
||||
[closeIcon]="closeIcon"
|
||||
triggerLabel="Menu stays open"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
</ui-fab-menu>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Advanced Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Advanced Examples</h3>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Mixed Item Variants</h4>
|
||||
<ui-fab-menu
|
||||
[menuItems]="mixedMenuItems"
|
||||
[triggerIcon]="settingsIcon"
|
||||
[closeIcon]="closeIcon"
|
||||
triggerLabel="Settings menu"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
</ui-fab-menu>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Large Menu with Icons</h4>
|
||||
<ui-fab-menu
|
||||
size="lg"
|
||||
[menuItems]="iconMenuItems"
|
||||
[triggerIcon]="appsIcon"
|
||||
[closeIcon]="closeIcon"
|
||||
triggerLabel="App launcher"
|
||||
(itemClicked)="handleItemClick($event)">
|
||||
</ui-fab-menu>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Feedback -->
|
||||
<section class="demo-section">
|
||||
<h3>Event Log</h3>
|
||||
<div class="demo-log">
|
||||
@if (eventLog.length === 0) {
|
||||
<p class="demo-log__empty">No events yet. Interact with the FAB menus above.</p>
|
||||
} @else {
|
||||
@for (event of eventLog.slice(-10); track $index) {
|
||||
<div class="demo-log__entry">
|
||||
<span class="demo-log__time">{{ event.timestamp | date:'HH:mm:ss' }}</span>
|
||||
<span class="demo-log__message">{{ event.message }}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<button class="demo-button demo-button--secondary" (click)="clearLog()">Clear Log</button>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './fab-menu-demo.component.scss'
|
||||
})
|
||||
export class FabMenuDemoComponent {
|
||||
sizes: FabMenuSize[] = ['sm', 'md', 'lg'];
|
||||
variants: FabMenuVariant[] = ['primary', 'secondary', 'success', 'danger'];
|
||||
directions: FabMenuDirection[] = ['up', 'down', 'left', 'right'];
|
||||
positions: FabMenuPosition[] = ['bottom-right', 'bottom-left', 'top-right', 'top-left'];
|
||||
|
||||
activePositionDemo: FabMenuPosition | null = null;
|
||||
eventLog: Array<{timestamp: Date, message: string}> = [];
|
||||
|
||||
// Icons (using simple text for demo - could be replaced with FontAwesome or other icon library)
|
||||
plusIcon = '<span>+</span>';
|
||||
closeIcon = '<span>×</span>';
|
||||
menuIcon = '<span>☰</span>';
|
||||
settingsIcon = '<span>⚙</span>';
|
||||
appsIcon = '<span>⊞</span>';
|
||||
|
||||
basicMenuItems: FabMenuItem[] = [
|
||||
{ id: 'action1', label: 'Action 1', icon: '<span>📝</span>' },
|
||||
{ id: 'action2', label: 'Action 2', icon: '<span>📁</span>' },
|
||||
{ id: 'action3', label: 'Action 3', icon: '<span>📊</span>' }
|
||||
];
|
||||
|
||||
positionMenuItems: FabMenuItem[] = [
|
||||
{ id: 'close', label: 'Close Menu', icon: '<span>×</span>' },
|
||||
{ id: 'home', label: 'Go Home', icon: '<span>🏠</span>' },
|
||||
{ id: 'profile', label: 'Profile', icon: '<span>👤</span>' },
|
||||
{ id: 'settings', label: 'Settings', icon: '<span>⚙</span>' }
|
||||
];
|
||||
|
||||
mixedMenuItems: FabMenuItem[] = [
|
||||
{ id: 'save', label: 'Save', variant: 'success', icon: '<span>💾</span>' },
|
||||
{ id: 'edit', label: 'Edit', variant: 'primary', icon: '<span>✏️</span>' },
|
||||
{ id: 'delete', label: 'Delete', variant: 'danger', icon: '<span>🗑️</span>' },
|
||||
{ id: 'disabled', label: 'Disabled', disabled: true, icon: '<span>🚫</span>' }
|
||||
];
|
||||
|
||||
iconMenuItems: FabMenuItem[] = [
|
||||
{ id: 'email', label: 'Email', icon: '<span>📧</span>' },
|
||||
{ id: 'calendar', label: 'Calendar', icon: '<span>📅</span>' },
|
||||
{ id: 'documents', label: 'Documents', icon: '<span>📄</span>' },
|
||||
{ id: 'photos', label: 'Photos', icon: '<span>📸</span>' },
|
||||
{ id: 'music', label: 'Music', icon: '<span>🎵</span>' },
|
||||
{ id: 'videos', label: 'Videos', icon: '<span>🎬</span>' }
|
||||
];
|
||||
|
||||
getVariantMenuItems(variant: FabMenuVariant): FabMenuItem[] {
|
||||
return [
|
||||
{ id: `${variant}1`, label: `${variant} Item 1`, variant },
|
||||
{ id: `${variant}2`, label: `${variant} Item 2`, variant },
|
||||
{ id: `${variant}3`, label: `${variant} Item 3`, variant }
|
||||
];
|
||||
}
|
||||
|
||||
handleItemClick(event: {item: FabMenuItem, event: Event}): void {
|
||||
this.logEvent(`Item clicked: ${event.item.label} (${event.item.id})`);
|
||||
}
|
||||
|
||||
togglePositionDemo(position: FabMenuPosition): void {
|
||||
if (this.activePositionDemo === position) {
|
||||
this.activePositionDemo = null;
|
||||
this.logEvent(`Closed ${position} positioned menu`);
|
||||
} else {
|
||||
this.activePositionDemo = position;
|
||||
this.logEvent(`Opened ${position} positioned menu`);
|
||||
}
|
||||
}
|
||||
|
||||
handlePositionItemClick(event: {item: FabMenuItem, event: Event}): void {
|
||||
if (event.item.id === 'close') {
|
||||
this.activePositionDemo = null;
|
||||
this.logEvent('Closed positioned menu via menu item');
|
||||
} else {
|
||||
this.logEvent(`Position menu item clicked: ${event.item.label}`);
|
||||
}
|
||||
}
|
||||
|
||||
onMenuOpened(): void {
|
||||
this.logEvent('Menu opened');
|
||||
}
|
||||
|
||||
onMenuClosed(): void {
|
||||
this.logEvent('Menu closed');
|
||||
}
|
||||
|
||||
logEvent(message: string): void {
|
||||
this.eventLog.push({
|
||||
timestamp: new Date(),
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
clearLog(): void {
|
||||
this.eventLog = [];
|
||||
this.logEvent('Event log cleared');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
|
||||
h3 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.demo-feed-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-feed {
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
background: $semantic-color-surface;
|
||||
|
||||
&--preview {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
&--interactive {
|
||||
height: 600px;
|
||||
margin: $semantic-spacing-component-lg 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-feed-item {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&--loading {
|
||||
opacity: $semantic-opacity-subtle;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
|
||||
strong {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&__badge {
|
||||
padding: 2px $semantic-spacing-component-xs;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
font-family: map-get($semantic-typography-caption, font-family);
|
||||
font-size: map-get($semantic-typography-caption, font-size);
|
||||
font-weight: map-get($semantic-typography-caption, font-weight);
|
||||
line-height: map-get($semantic-typography-caption, line-height);
|
||||
text-transform: uppercase;
|
||||
|
||||
&--text {
|
||||
background: $semantic-color-info;
|
||||
color: $semantic-color-on-info;
|
||||
}
|
||||
|
||||
&--image {
|
||||
background: $semantic-color-success;
|
||||
color: $semantic-color-on-success;
|
||||
}
|
||||
|
||||
&--video {
|
||||
background: $semantic-color-warning;
|
||||
color: $semantic-color-on-warning;
|
||||
}
|
||||
}
|
||||
|
||||
&__timestamp {
|
||||
font-family: map-get($semantic-typography-caption, font-family);
|
||||
font-size: map-get($semantic-typography-caption, font-size);
|
||||
font-weight: map-get($semantic-typography-caption, font-weight);
|
||||
line-height: map-get($semantic-typography-caption, line-height);
|
||||
color: $semantic-color-text-tertiary;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h5, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h5, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h5, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h5, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0 0 $semantic-spacing-content-paragraph 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0 0 $semantic-spacing-component-md 0;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__action {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
cursor: pointer;
|
||||
|
||||
font-family: map-get($semantic-typography-button-small, font-family);
|
||||
font-size: map-get($semantic-typography-button-small, font-size);
|
||||
font-weight: map-get($semantic-typography-button-small, font-weight);
|
||||
line-height: map-get($semantic-typography-button-small, line-height);
|
||||
color: $semantic-color-text-tertiary;
|
||||
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
align-items: center;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
|
||||
button {
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border: none;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
cursor: pointer;
|
||||
|
||||
font-family: map-get($semantic-typography-button-small, font-family);
|
||||
font-size: map-get($semantic-typography-button-small, font-size);
|
||||
font-weight: map-get($semantic-typography-button-small, font-weight);
|
||||
line-height: map-get($semantic-typography-button-small, line-height);
|
||||
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $semantic-opacity-disabled;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
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-secondary;
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-empty-state {
|
||||
text-align: center;
|
||||
padding: $semantic-spacing-layout-section-lg;
|
||||
|
||||
&--small {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0 0 $semantic-spacing-component-sm 0;
|
||||
}
|
||||
|
||||
p {
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0 0 $semantic-spacing-component-lg 0;
|
||||
}
|
||||
|
||||
&__button {
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
border: none;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
cursor: pointer;
|
||||
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
.demo-stat {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&__label {
|
||||
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-secondary;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 768px) {
|
||||
.demo-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
|
||||
label {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-feed {
|
||||
&--preview {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
&--interactive {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FeedLayoutComponent, FeedItem } from 'ui-essentials';
|
||||
|
||||
interface DemoFeedItem extends FeedItem {
|
||||
title: string;
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
author: string;
|
||||
likes: number;
|
||||
type: 'text' | 'image' | 'video';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-feed-layout-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, FeedLayoutComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Feed Layout Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-feed-wrapper">
|
||||
<h4>{{ size | titlecase }}</h4>
|
||||
<ui-feed-layout
|
||||
[size]="size"
|
||||
[loading]="false"
|
||||
[enableInfiniteScroll]="false"
|
||||
[enableRefresh]="false"
|
||||
class="demo-feed demo-feed--preview">
|
||||
@for (item of sampleItems.slice(0, 2); track item.id) {
|
||||
<div class="demo-feed-item">
|
||||
<div class="demo-feed-item__header">
|
||||
<strong>{{ item.author }}</strong>
|
||||
<span class="demo-feed-item__timestamp">{{ item.timestamp | date:'short' }}</span>
|
||||
</div>
|
||||
<h5 class="demo-feed-item__title">{{ item.title }}</h5>
|
||||
<p class="demo-feed-item__content">{{ item.content }}</p>
|
||||
<div class="demo-feed-item__actions">
|
||||
<button class="demo-feed-item__action">♡ {{ item.likes }}</button>
|
||||
<button class="demo-feed-item__action">💬 Comment</button>
|
||||
<button class="demo-feed-item__action">↗ Share</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</ui-feed-layout>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Feed -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Feed with Infinite Scroll</h3>
|
||||
<div class="demo-controls">
|
||||
<button (click)="resetFeed()" [disabled]="isLoading()">Reset Feed</button>
|
||||
<button (click)="toggleError()">
|
||||
{{ hasError() ? 'Clear Error' : 'Simulate Error' }}
|
||||
</button>
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="enableRefreshControl" />
|
||||
Enable Pull to Refresh
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ui-feed-layout
|
||||
size="md"
|
||||
[loading]="isLoading()"
|
||||
[hasError]="hasError()"
|
||||
[errorMessage]="errorMessage"
|
||||
[isEmpty]="feedItems().length === 0 && !isLoading()"
|
||||
[enableInfiniteScroll]="true"
|
||||
[enableRefresh]="enableRefreshControl"
|
||||
(loadMore)="loadMoreItems()"
|
||||
(refresh)="refreshFeed()"
|
||||
(retry)="retryLoad()"
|
||||
class="demo-feed demo-feed--interactive"
|
||||
#interactiveFeed>
|
||||
|
||||
@for (item of feedItems(); track item.id) {
|
||||
<div class="demo-feed-item">
|
||||
<div class="demo-feed-item__header">
|
||||
<strong>{{ item.author }}</strong>
|
||||
<span class="demo-feed-item__badge demo-feed-item__badge--{{item.type}}">
|
||||
{{ item.type }}
|
||||
</span>
|
||||
<span class="demo-feed-item__timestamp">{{ item.timestamp | date:'short' }}</span>
|
||||
</div>
|
||||
<h5 class="demo-feed-item__title">{{ item.title }}</h5>
|
||||
<p class="demo-feed-item__content">{{ item.content }}</p>
|
||||
<div class="demo-feed-item__actions">
|
||||
<button
|
||||
class="demo-feed-item__action"
|
||||
(click)="toggleLike(item)">
|
||||
{{ item.likes > 0 ? '❤️' : '♡' }} {{ item.likes }}
|
||||
</button>
|
||||
<button class="demo-feed-item__action">💬 Comment</button>
|
||||
<button class="demo-feed-item__action">↗ Share</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Empty state slot -->
|
||||
<div slot="empty" class="demo-empty-state">
|
||||
<div class="demo-empty-state__icon">📝</div>
|
||||
<h4>No posts yet</h4>
|
||||
<p>Be the first to share something!</p>
|
||||
<button (click)="addSampleContent()" class="demo-empty-state__button">
|
||||
Add Sample Content
|
||||
</button>
|
||||
</div>
|
||||
</ui-feed-layout>
|
||||
</section>
|
||||
|
||||
<!-- States Demo -->
|
||||
<section class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-feed-wrapper">
|
||||
<h4>Loading State</h4>
|
||||
<ui-feed-layout
|
||||
size="sm"
|
||||
[loading]="true"
|
||||
[enableInfiniteScroll]="false"
|
||||
[enableRefresh]="false"
|
||||
class="demo-feed demo-feed--preview">
|
||||
@for (item of sampleItems.slice(0, 1); track item.id) {
|
||||
<div class="demo-feed-item demo-feed-item--loading">
|
||||
<div class="demo-feed-item__header">
|
||||
<strong>{{ item.author }}</strong>
|
||||
</div>
|
||||
<h5 class="demo-feed-item__title">{{ item.title }}</h5>
|
||||
<p class="demo-feed-item__content">{{ item.content }}</p>
|
||||
</div>
|
||||
}
|
||||
</ui-feed-layout>
|
||||
</div>
|
||||
|
||||
<div class="demo-feed-wrapper">
|
||||
<h4>Error State</h4>
|
||||
<ui-feed-layout
|
||||
size="sm"
|
||||
[loading]="false"
|
||||
[hasError]="true"
|
||||
errorMessage="Network connection failed"
|
||||
[enableInfiniteScroll]="false"
|
||||
[enableRefresh]="false"
|
||||
class="demo-feed demo-feed--preview">
|
||||
</ui-feed-layout>
|
||||
</div>
|
||||
|
||||
<div class="demo-feed-wrapper">
|
||||
<h4>Empty State</h4>
|
||||
<ui-feed-layout
|
||||
size="sm"
|
||||
[loading]="false"
|
||||
[isEmpty]="true"
|
||||
[enableInfiniteScroll]="false"
|
||||
[enableRefresh]="false"
|
||||
class="demo-feed demo-feed--preview">
|
||||
<div slot="empty" class="demo-empty-state demo-empty-state--small">
|
||||
<div class="demo-empty-state__icon">📭</div>
|
||||
<p>No content available</p>
|
||||
</div>
|
||||
</ui-feed-layout>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Statistics -->
|
||||
<section class="demo-section">
|
||||
<h3>Usage Statistics</h3>
|
||||
<div class="demo-stats">
|
||||
<div class="demo-stat">
|
||||
<span class="demo-stat__label">Total Items:</span>
|
||||
<span class="demo-stat__value">{{ feedItems().length }}</span>
|
||||
</div>
|
||||
<div class="demo-stat">
|
||||
<span class="demo-stat__label">Load More Calls:</span>
|
||||
<span class="demo-stat__value">{{ loadMoreCount }}</span>
|
||||
</div>
|
||||
<div class="demo-stat">
|
||||
<span class="demo-stat__label">Refresh Calls:</span>
|
||||
<span class="demo-stat__value">{{ refreshCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './feed-layout-demo.component.scss'
|
||||
})
|
||||
export class FeedLayoutDemoComponent {
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
|
||||
protected readonly feedItems = signal<DemoFeedItem[]>([]);
|
||||
protected readonly isLoading = signal(false);
|
||||
protected readonly hasError = signal(false);
|
||||
|
||||
protected errorMessage = '';
|
||||
protected enableRefreshControl = true;
|
||||
protected loadMoreCount = 0;
|
||||
protected refreshCount = 0;
|
||||
|
||||
protected sampleItems: DemoFeedItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Getting Started with Angular 19',
|
||||
content: 'Angular 19 brings exciting new features including improved SSR, better performance, and enhanced developer experience.',
|
||||
timestamp: new Date(Date.now() - 3600000),
|
||||
author: 'Sarah Chen',
|
||||
likes: 42,
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Building Responsive Layouts',
|
||||
content: 'Learn how to create flexible, mobile-first designs that work beautifully across all devices.',
|
||||
timestamp: new Date(Date.now() - 7200000),
|
||||
author: 'Mike Rodriguez',
|
||||
likes: 28,
|
||||
type: 'image'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'TypeScript Best Practices',
|
||||
content: 'Discover advanced TypeScript patterns and techniques to write more maintainable code.',
|
||||
timestamp: new Date(Date.now() - 10800000),
|
||||
author: 'Alex Kim',
|
||||
likes: 35,
|
||||
type: 'video'
|
||||
}
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.addSampleContent();
|
||||
}
|
||||
|
||||
loadMoreItems(): void {
|
||||
if (this.isLoading() || this.hasError()) return;
|
||||
|
||||
this.loadMoreCount++;
|
||||
this.isLoading.set(true);
|
||||
|
||||
// Simulate API call delay
|
||||
setTimeout(() => {
|
||||
const currentItems = this.feedItems();
|
||||
const nextBatch = this.generateItems(3, currentItems.length);
|
||||
this.feedItems.set([...currentItems, ...nextBatch]);
|
||||
this.isLoading.set(false);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
refreshFeed(): void {
|
||||
this.refreshCount++;
|
||||
this.isLoading.set(true);
|
||||
this.hasError.set(false);
|
||||
|
||||
// Simulate refresh delay
|
||||
setTimeout(() => {
|
||||
const newItems = this.generateItems(5, 0);
|
||||
this.feedItems.set(newItems);
|
||||
this.isLoading.set(false);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
retryLoad(): void {
|
||||
this.hasError.set(false);
|
||||
this.loadMoreItems();
|
||||
}
|
||||
|
||||
resetFeed(): void {
|
||||
this.feedItems.set([]);
|
||||
this.hasError.set(false);
|
||||
this.isLoading.set(false);
|
||||
this.loadMoreCount = 0;
|
||||
this.refreshCount = 0;
|
||||
this.addSampleContent();
|
||||
}
|
||||
|
||||
toggleError(): void {
|
||||
if (this.hasError()) {
|
||||
this.hasError.set(false);
|
||||
this.errorMessage = '';
|
||||
} else {
|
||||
this.hasError.set(true);
|
||||
this.errorMessage = 'Failed to load more content. Please check your connection.';
|
||||
}
|
||||
}
|
||||
|
||||
toggleLike(item: DemoFeedItem): void {
|
||||
const items = this.feedItems();
|
||||
const index = items.findIndex(i => i.id === item.id);
|
||||
if (index !== -1) {
|
||||
const updatedItems = [...items];
|
||||
updatedItems[index] = { ...item, likes: item.likes > 0 ? 0 : 1 };
|
||||
this.feedItems.set(updatedItems);
|
||||
}
|
||||
}
|
||||
|
||||
addSampleContent(): void {
|
||||
const items = this.generateItems(5, 0);
|
||||
this.feedItems.set(items);
|
||||
}
|
||||
|
||||
private generateItems(count: number, startId: number): DemoFeedItem[] {
|
||||
const contentTemplates = [
|
||||
{
|
||||
title: 'Web Development Trends',
|
||||
content: 'Exploring the latest trends in modern web development and what they mean for developers.',
|
||||
author: 'Jane Doe',
|
||||
type: 'text' as const
|
||||
},
|
||||
{
|
||||
title: 'UI/UX Design Principles',
|
||||
content: 'Understanding the fundamental principles that make great user interfaces and experiences.',
|
||||
author: 'John Smith',
|
||||
type: 'image' as const
|
||||
},
|
||||
{
|
||||
title: 'Performance Optimization',
|
||||
content: 'Tips and techniques for optimizing web application performance and user experience.',
|
||||
author: 'Emma Wilson',
|
||||
type: 'video' as const
|
||||
},
|
||||
{
|
||||
title: 'Accessibility Matters',
|
||||
content: 'Why web accessibility is crucial and how to implement it in your projects.',
|
||||
author: 'David Brown',
|
||||
type: 'text' as const
|
||||
},
|
||||
{
|
||||
title: 'Mobile-First Design',
|
||||
content: 'Best practices for designing mobile-first responsive web applications.',
|
||||
author: 'Lisa Garcia',
|
||||
type: 'image' as const
|
||||
}
|
||||
];
|
||||
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const template = contentTemplates[index % contentTemplates.length];
|
||||
const id = startId + index + 1;
|
||||
|
||||
return {
|
||||
id: id.toString(),
|
||||
title: `${template.title} #${id}`,
|
||||
content: template.content,
|
||||
timestamp: new Date(Date.now() - (index + 1) * 1800000),
|
||||
author: template.author,
|
||||
likes: Math.floor(Math.random() * 50),
|
||||
type: template.type
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
@use '../../../../../ui-design-system/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 '../../../../../ui-essentials/src/lib/components/forms/file-upload/file-upload.component';
|
||||
|
||||
@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/src/lib/components/forms/file-upload/file-upload.component';
|
||||
|
||||
// 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,290 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
h2 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
|
||||
h3 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-family: map-get($semantic-typography-heading-h5, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h5, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h5, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h5, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
p {
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-column {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
// Flex demo containers
|
||||
.flex-demo {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.flex-item {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
text-align: center;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
|
||||
&--small {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&--medium {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&--large {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&--fixed {
|
||||
min-width: 120px;
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
}
|
||||
|
||||
&--inline {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
background: $semantic-color-info;
|
||||
color: $semantic-color-on-info;
|
||||
}
|
||||
}
|
||||
|
||||
// Justify content demos
|
||||
.justify-example {
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.justify-demo {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
// Align items demos
|
||||
.align-demo {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
// Wrap demos
|
||||
.wrap-example {
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
.wrap-demo {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
// Gap demos
|
||||
.gap-demo {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
// Complex examples
|
||||
.complex-example {
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
|
||||
.centered-container {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
min-height: 200px;
|
||||
|
||||
.card {
|
||||
background: $semantic-color-surface-primary;
|
||||
padding: $semantic-spacing-component-xl;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
box-shadow: $semantic-shadow-elevation-2;
|
||||
text-align: center;
|
||||
max-width: 300px;
|
||||
|
||||
h5 {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-demo {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
|
||||
.nav-brand {
|
||||
font-weight: $semantic-typography-font-weight-bold;
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
transition: background-color $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
.grid-card {
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
max-width: 350px;
|
||||
|
||||
h6 {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-layout {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
min-height: 200px;
|
||||
|
||||
.sidebar {
|
||||
background: $semantic-color-surface-primary;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
min-width: 200px;
|
||||
border-right: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
flex: 1;
|
||||
|
||||
h6 {
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Size examples
|
||||
.size-example {
|
||||
margin-bottom: $semantic-spacing-component-xl;
|
||||
}
|
||||
|
||||
.size-demo {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.inline-flex-demo {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FlexComponent } from '../../../../../ui-essentials/src/lib/components/layout/flex';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-flex-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FlexComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Flex Component Demo</h2>
|
||||
|
||||
<!-- Direction Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Flex Direction</h3>
|
||||
<div class="demo-row">
|
||||
@for (direction of directions; track direction) {
|
||||
<div class="demo-column">
|
||||
<h4>direction="{{ direction }}"</h4>
|
||||
<ui-flex [direction]="direction" class="flex-demo">
|
||||
<div class="flex-item">1</div>
|
||||
<div class="flex-item">2</div>
|
||||
<div class="flex-item">3</div>
|
||||
</ui-flex>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Justify Content -->
|
||||
<section class="demo-section">
|
||||
<h3>Justify Content</h3>
|
||||
<div class="demo-column">
|
||||
@for (justify of justifyOptions; track justify) {
|
||||
<div class="justify-example">
|
||||
<h4>justify="{{ justify }}"</h4>
|
||||
<ui-flex [justify]="justify" class="justify-demo">
|
||||
<div class="flex-item">A</div>
|
||||
<div class="flex-item">B</div>
|
||||
<div class="flex-item">C</div>
|
||||
</ui-flex>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Align Items -->
|
||||
<section class="demo-section">
|
||||
<h3>Align Items</h3>
|
||||
<div class="demo-row">
|
||||
@for (align of alignOptions; track align) {
|
||||
<div class="demo-column">
|
||||
<h4>align="{{ align }}"</h4>
|
||||
<ui-flex [align]="align" class="align-demo">
|
||||
<div class="flex-item flex-item--small">Small</div>
|
||||
<div class="flex-item flex-item--medium">Medium</div>
|
||||
<div class="flex-item flex-item--large">Large item with more content</div>
|
||||
</ui-flex>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Wrap Options -->
|
||||
<section class="demo-section">
|
||||
<h3>Flex Wrap</h3>
|
||||
<div class="demo-column">
|
||||
@for (wrapOption of wrapOptions; track wrapOption) {
|
||||
<div class="wrap-example">
|
||||
<h4>wrap="{{ wrapOption }}"</h4>
|
||||
<ui-flex [wrap]="wrapOption" class="wrap-demo">
|
||||
@for (item of manyItems; track item) {
|
||||
<div class="flex-item flex-item--fixed">{{ item }}</div>
|
||||
}
|
||||
</ui-flex>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gap Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Gap Spacing</h3>
|
||||
<div class="demo-row">
|
||||
@for (gap of gapSizes; track gap) {
|
||||
<div class="demo-column">
|
||||
<h4>gap="{{ gap }}"</h4>
|
||||
<ui-flex [gap]="gap" class="gap-demo">
|
||||
<div class="flex-item">1</div>
|
||||
<div class="flex-item">2</div>
|
||||
<div class="flex-item">3</div>
|
||||
</ui-flex>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Complex Layout Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Complex Layout Examples</h3>
|
||||
|
||||
<!-- Centered Card -->
|
||||
<div class="complex-example">
|
||||
<h4>Centered Card Layout</h4>
|
||||
<ui-flex justify="center" align="center" class="centered-container">
|
||||
<div class="card">
|
||||
<h5>Centered Card</h5>
|
||||
<p>This card is perfectly centered using flexbox.</p>
|
||||
</div>
|
||||
</ui-flex>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Layout -->
|
||||
<div class="complex-example">
|
||||
<h4>Navigation Layout</h4>
|
||||
<ui-flex justify="between" align="center" class="nav-demo">
|
||||
<div class="nav-brand">Brand</div>
|
||||
<ui-flex gap="md" align="center">
|
||||
<div class="nav-item">Home</div>
|
||||
<div class="nav-item">About</div>
|
||||
<div class="nav-item">Contact</div>
|
||||
</ui-flex>
|
||||
</ui-flex>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Grid Alternative -->
|
||||
<div class="complex-example">
|
||||
<h4>Responsive Card Grid</h4>
|
||||
<ui-flex wrap="wrap" gap="lg" class="card-grid">
|
||||
@for (card of cards; track card.id) {
|
||||
<div class="grid-card">
|
||||
<h6>{{ card.title }}</h6>
|
||||
<p>{{ card.content }}</p>
|
||||
</div>
|
||||
}
|
||||
</ui-flex>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Layout -->
|
||||
<div class="complex-example">
|
||||
<h4>Sidebar Layout</h4>
|
||||
<ui-flex class="sidebar-layout">
|
||||
<div class="sidebar">Sidebar</div>
|
||||
<div class="main-content">
|
||||
<h6>Main Content</h6>
|
||||
<p>This is the main content area that grows to fill the available space.</p>
|
||||
</div>
|
||||
</ui-flex>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
<div class="demo-column">
|
||||
<div class="size-example">
|
||||
<h4>Full Width</h4>
|
||||
<ui-flex [fullWidth]="true" class="size-demo">
|
||||
<div class="flex-item">Full Width Container</div>
|
||||
</ui-flex>
|
||||
</div>
|
||||
|
||||
<div class="size-example">
|
||||
<h4>Inline Flex</h4>
|
||||
<div>
|
||||
Before text
|
||||
<ui-flex [inline]="true" gap="sm" class="inline-flex-demo">
|
||||
<div class="flex-item flex-item--inline">A</div>
|
||||
<div class="flex-item flex-item--inline">B</div>
|
||||
</ui-flex>
|
||||
After text
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './flex-demo.component.scss'
|
||||
})
|
||||
export class FlexDemoComponent {
|
||||
directions = ['row', 'row-reverse', 'column', 'column-reverse'] as const;
|
||||
justifyOptions = ['start', 'end', 'center', 'between', 'around', 'evenly'] as const;
|
||||
alignOptions = ['start', 'end', 'center', 'baseline', 'stretch'] as const;
|
||||
wrapOptions = ['nowrap', 'wrap', 'wrap-reverse'] as const;
|
||||
gapSizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
|
||||
|
||||
manyItems = Array.from({ length: 12 }, (_, i) => `Item ${i + 1}`);
|
||||
|
||||
cards = [
|
||||
{ id: 1, title: 'Card 1', content: 'This is the first card with some content.' },
|
||||
{ id: 2, title: 'Card 2', content: 'This is the second card with different content.' },
|
||||
{ id: 3, title: 'Card 3', content: 'This is the third card with more content.' },
|
||||
{ id: 4, title: 'Card 4', content: 'This is the fourth card with additional content.' },
|
||||
{ id: 5, title: 'Card 5', content: 'This is the fifth card with extra content.' },
|
||||
{ id: 6, title: 'Card 6', content: 'This is the sixth card with final content.' },
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-lg;
|
||||
|
||||
h3 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-subsection {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
&--center {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-trigger-button {
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border: none;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
min-height: $semantic-sizing-button-height-md;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-primary;
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
|
||||
.demo-position-container {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
". top ."
|
||||
"left center right"
|
||||
". bottom .";
|
||||
gap: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
.demo-position-button {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
font-family: map-get($semantic-typography-button-small, font-family);
|
||||
font-size: map-get($semantic-typography-button-small, font-size);
|
||||
font-weight: map-get($semantic-typography-button-small, font-weight);
|
||||
line-height: map-get($semantic-typography-button-small, line-height);
|
||||
|
||||
&--top {
|
||||
grid-area: top;
|
||||
}
|
||||
|
||||
&--bottom {
|
||||
grid-area: bottom;
|
||||
}
|
||||
|
||||
&--left {
|
||||
grid-area: left;
|
||||
}
|
||||
|
||||
&--right {
|
||||
grid-area: right;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-color: $semantic-color-border-primary;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-context-area {
|
||||
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-md;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
min-height: 120px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
p {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0 0 $semantic-spacing-content-paragraph 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto $semantic-spacing-component-sm auto;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-color: $semantic-color-border-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-hint {
|
||||
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-secondary;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.demo-editor {
|
||||
min-height: 120px;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: text;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
border-color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 $semantic-spacing-content-paragraph 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: $semantic-typography-font-weight-bold;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
u {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: $semantic-color-surface-primary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
overflow: hidden;
|
||||
box-shadow: $semantic-shadow-elevation-1;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
text-align: left;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
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);
|
||||
}
|
||||
|
||||
th {
|
||||
background: $semantic-color-surface-secondary;
|
||||
color: $semantic-color-text-primary;
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
td {
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-table-row {
|
||||
cursor: pointer;
|
||||
transition: background-color $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
|
||||
td {
|
||||
color: $semantic-color-on-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-table-action {
|
||||
background: none;
|
||||
border: none;
|
||||
color: $semantic-color-text-secondary;
|
||||
font-size: $semantic-typography-font-size-lg;
|
||||
cursor: pointer;
|
||||
padding: $semantic-spacing-component-xs;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.demo-table-row--selected & {
|
||||
color: $semantic-color-on-primary;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
.demo-control-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $semantic-spacing-component-md;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-output {
|
||||
margin-top: $semantic-spacing-layout-section-md;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-lg;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0 0 $semantic-spacing-component-md 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-log {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
|
||||
// Custom scrollbar styling
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: $semantic-color-border-subtle $semantic-color-surface-secondary;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: $semantic-color-surface-secondary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-border-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-log-entry {
|
||||
font-family: $semantic-typography-font-family-mono;
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
color: $semantic-color-text-primary;
|
||||
padding: $semantic-spacing-component-xs 0;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
color: $semantic-color-success;
|
||||
font-weight: $semantic-typography-font-weight-medium;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-log-empty {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
color: $semantic-color-text-secondary;
|
||||
text-align: center;
|
||||
padding: $semantic-spacing-component-md;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-position-container {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-control-group {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-context-area {
|
||||
padding: $semantic-spacing-component-md;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.demo-editor {
|
||||
min-height: 100px;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-table {
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-position-container {
|
||||
grid-template-areas:
|
||||
"top"
|
||||
"left"
|
||||
"center"
|
||||
"right"
|
||||
"bottom";
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-position-button {
|
||||
min-width: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
// High contrast support
|
||||
@media (prefers-contrast: high) {
|
||||
.demo-trigger-button,
|
||||
.demo-position-button,
|
||||
.demo-table,
|
||||
.demo-context-area,
|
||||
.demo-editor,
|
||||
.demo-controls,
|
||||
.demo-output,
|
||||
.demo-log {
|
||||
border-width: $semantic-border-width-2;
|
||||
}
|
||||
}
|
||||
|
||||
// Reduced motion support
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.demo-trigger-button,
|
||||
.demo-position-button,
|
||||
.demo-table-row,
|
||||
.demo-table-action,
|
||||
.demo-context-area {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,670 @@
|
||||
import { Component, ViewChild, AfterViewInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FloatingToolbarComponent, ToolbarAction, FloatingToolbarConfig } from '../../../../../ui-essentials/src/lib/components/overlays/floating-toolbar/floating-toolbar.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-floating-toolbar-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, FloatingToolbarComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Floating Toolbar Demo</h2>
|
||||
|
||||
<!-- Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Size Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (size of sizes; track size) {
|
||||
<div class="demo-item">
|
||||
<button
|
||||
class="demo-trigger-button"
|
||||
#triggerButton
|
||||
(click)="showToolbar('size-' + size, triggerButton, { size: size })"
|
||||
>
|
||||
{{ size }} Toolbar
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Variant Styles -->
|
||||
<section class="demo-section">
|
||||
<h3>Style Variants</h3>
|
||||
<div class="demo-row">
|
||||
@for (variant of variants; track variant) {
|
||||
<div class="demo-item">
|
||||
<button
|
||||
class="demo-trigger-button"
|
||||
#triggerButton
|
||||
(click)="showToolbar('variant-' + variant, triggerButton, { variant: variant })"
|
||||
>
|
||||
{{ variant | titlecase }} Style
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Position Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Position Variants</h3>
|
||||
<div class="demo-row demo-row--center">
|
||||
<div class="demo-position-container">
|
||||
@for (position of positions; track position) {
|
||||
<button
|
||||
class="demo-position-button demo-position-button--{{ position }}"
|
||||
#triggerButton
|
||||
(click)="showToolbar('position-' + position, triggerButton, { position: position })"
|
||||
>
|
||||
{{ position }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Context-Sensitive Actions -->
|
||||
<section class="demo-section">
|
||||
<h3>Context-Sensitive Actions</h3>
|
||||
<div class="demo-row">
|
||||
<div class="demo-context-area" #textContext>
|
||||
<p>
|
||||
Select this text to see a text editing toolbar with cut, copy, paste, and formatting options.
|
||||
The toolbar will appear automatically when you make a selection.
|
||||
</p>
|
||||
</div>
|
||||
<div class="demo-context-area" #imageContext>
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZGRkIi8+CiAgPHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPkltYWdlPC90ZXh0Pgo8L3N2Zz4K"
|
||||
alt="Demo Image"
|
||||
(contextmenu)="showContextualToolbar($event, imageActions, 'image')"
|
||||
/>
|
||||
<p class="demo-hint">Right-click the image for image editing options</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Auto-Hide Behavior -->
|
||||
<section class="demo-section">
|
||||
<h3>Auto-Hide Behavior</h3>
|
||||
<div class="demo-row">
|
||||
<button
|
||||
class="demo-trigger-button"
|
||||
#autoHideButton
|
||||
(click)="showAutoHideToolbar(autoHideButton)"
|
||||
>
|
||||
Show Auto-Hide Toolbar (3 seconds)
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Custom Actions Demo -->
|
||||
<section class="demo-section">
|
||||
<h3>Custom Actions</h3>
|
||||
<div class="demo-row">
|
||||
<button
|
||||
class="demo-trigger-button"
|
||||
#customButton
|
||||
(click)="showCustomActionsToolbar(customButton)"
|
||||
>
|
||||
Custom Actions Toolbar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="demo-output">
|
||||
<h4>Action Log:</h4>
|
||||
<div class="demo-log">
|
||||
@for (logEntry of actionLog; track $index) {
|
||||
<div class="demo-log-entry">{{ logEntry }}</div>
|
||||
}
|
||||
@if (actionLog.length === 0) {
|
||||
<div class="demo-log-empty">No actions performed yet...</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Examples</h3>
|
||||
|
||||
<!-- Editable Text Area -->
|
||||
<div class="demo-subsection">
|
||||
<h4>Editable Text with Formatting Toolbar</h4>
|
||||
<div
|
||||
class="demo-editor"
|
||||
contenteditable="true"
|
||||
#editableArea
|
||||
(mouseup)="handleTextSelection(editableArea)"
|
||||
(keyup)="handleTextSelection(editableArea)"
|
||||
>
|
||||
<p>This is an editable text area. Select text to see formatting options in the floating toolbar.</p>
|
||||
<p><strong>Bold text</strong>, <em>italic text</em>, and <u>underlined text</u> are supported.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table with Row Actions -->
|
||||
<div class="demo-subsection">
|
||||
<h4>Data Table with Row Actions</h4>
|
||||
<table class="demo-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (user of sampleUsers; track user.id) {
|
||||
<tr
|
||||
#tableRow
|
||||
class="demo-table-row"
|
||||
[class.demo-table-row--selected]="selectedUserId === user.id"
|
||||
(click)="selectUser(user.id, tableRow)"
|
||||
>
|
||||
<td>{{ user.name }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ user.status }}</td>
|
||||
<td>
|
||||
<button
|
||||
class="demo-table-action"
|
||||
(click)="showUserActions($event, tableRow, user)"
|
||||
>
|
||||
⋮
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Configuration Demo -->
|
||||
<section class="demo-section">
|
||||
<h3>Configuration Options</h3>
|
||||
<div class="demo-controls">
|
||||
<div class="demo-control-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
[(ngModel)]="demoConfig.backdrop"
|
||||
(change)="updateDemoConfig()"
|
||||
/>
|
||||
Show Backdrop
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
[(ngModel)]="demoConfig.autoHide"
|
||||
(change)="updateDemoConfig()"
|
||||
/>
|
||||
Auto Hide
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
[(ngModel)]="demoConfig.escapeClosable"
|
||||
(change)="updateDemoConfig()"
|
||||
/>
|
||||
ESC to Close
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="demo-trigger-button"
|
||||
#configButton
|
||||
(click)="showConfigurableToolbar(configButton)"
|
||||
>
|
||||
Test Configuration
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Floating Toolbars -->
|
||||
<ui-floating-toolbar
|
||||
#floatingToolbar
|
||||
[actions]="currentActions"
|
||||
[size]="getConfigValue('size')"
|
||||
[variant]="getConfigValue('variant')"
|
||||
[position]="getConfigValue('position')"
|
||||
[visible]="getConfigValue('visible')"
|
||||
[autoHide]="getConfigValue('autoHide')"
|
||||
[hideDelay]="getConfigValue('hideDelay')"
|
||||
[backdrop]="getConfigValue('backdrop')"
|
||||
[escapeClosable]="getConfigValue('escapeClosable')"
|
||||
[label]="getConfigValue('label')"
|
||||
[showLabel]="getConfigValue('showLabel')"
|
||||
[showShortcuts]="getConfigValue('showShortcuts')"
|
||||
(actionClicked)="onActionClicked($event)"
|
||||
(visibleChange)="onToolbarVisibilityChange($event)"
|
||||
(hidden)="onToolbarHidden()"
|
||||
></ui-floating-toolbar>
|
||||
|
||||
<!-- Text Selection Toolbar -->
|
||||
<ui-floating-toolbar
|
||||
#textSelectionToolbar
|
||||
[actions]="textActions"
|
||||
size="sm"
|
||||
variant="contextual"
|
||||
position="top"
|
||||
[visible]="false"
|
||||
trigger="selection"
|
||||
[autoHide]="false"
|
||||
label="Text Formatting"
|
||||
[showShortcuts]="true"
|
||||
(actionClicked)="onTextActionClicked($event)"
|
||||
></ui-floating-toolbar>
|
||||
`,
|
||||
styleUrl: './floating-toolbar-demo.component.scss'
|
||||
})
|
||||
export class FloatingToolbarDemoComponent implements AfterViewInit {
|
||||
@ViewChild('floatingToolbar') floatingToolbar!: FloatingToolbarComponent;
|
||||
@ViewChild('textSelectionToolbar') textSelectionToolbar!: FloatingToolbarComponent;
|
||||
|
||||
// Demo Data
|
||||
sizes = ['sm', 'md', 'lg'] as const;
|
||||
variants = ['default', 'elevated', 'floating', 'compact', 'contextual'] as const;
|
||||
positions = ['top', 'bottom', 'left', 'right'] as const;
|
||||
|
||||
sampleUsers = [
|
||||
{ id: 1, name: 'Alice Johnson', email: 'alice@example.com', status: 'Active' },
|
||||
{ id: 2, name: 'Bob Smith', email: 'bob@example.com', status: 'Pending' },
|
||||
{ id: 3, name: 'Carol Davis', email: 'carol@example.com', status: 'Inactive' },
|
||||
];
|
||||
|
||||
// Demo State
|
||||
actionLog: string[] = [];
|
||||
selectedUserId: number | null = null;
|
||||
currentConfig: FloatingToolbarConfig = {
|
||||
visible: false,
|
||||
size: 'md',
|
||||
variant: 'default',
|
||||
position: 'top',
|
||||
autoHide: false,
|
||||
hideDelay: 3000,
|
||||
backdrop: false,
|
||||
escapeClosable: true,
|
||||
label: '',
|
||||
showLabel: false,
|
||||
showShortcuts: false
|
||||
};
|
||||
|
||||
demoConfig = {
|
||||
backdrop: false,
|
||||
autoHide: false,
|
||||
escapeClosable: true
|
||||
};
|
||||
|
||||
currentActions: ToolbarAction[] = [];
|
||||
|
||||
// Action Sets
|
||||
basicActions: ToolbarAction[] = [
|
||||
{
|
||||
id: 'edit',
|
||||
label: 'Edit',
|
||||
icon: 'fas fa-edit',
|
||||
callback: (action, event) => this.logAction('Edit clicked')
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
label: 'Delete',
|
||||
icon: 'fas fa-trash',
|
||||
callback: (action, event) => this.logAction('Delete clicked')
|
||||
},
|
||||
{
|
||||
id: 'share',
|
||||
label: 'Share',
|
||||
icon: 'fas fa-share',
|
||||
callback: (action, event) => this.logAction('Share clicked')
|
||||
}
|
||||
];
|
||||
|
||||
textActions: ToolbarAction[] = [
|
||||
{
|
||||
id: 'cut',
|
||||
label: 'Cut',
|
||||
icon: 'fas fa-cut',
|
||||
shortcut: 'Ctrl+X',
|
||||
callback: (action, event) => this.executeTextAction('cut')
|
||||
},
|
||||
{
|
||||
id: 'copy',
|
||||
label: 'Copy',
|
||||
icon: 'fas fa-copy',
|
||||
shortcut: 'Ctrl+C',
|
||||
callback: (action, event) => this.executeTextAction('copy')
|
||||
},
|
||||
{
|
||||
id: 'paste',
|
||||
label: 'Paste',
|
||||
icon: 'fas fa-paste',
|
||||
shortcut: 'Ctrl+V',
|
||||
callback: (action, event) => this.executeTextAction('paste')
|
||||
},
|
||||
{
|
||||
id: 'divider1',
|
||||
label: '',
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
id: 'bold',
|
||||
label: 'Bold',
|
||||
icon: 'fas fa-bold',
|
||||
shortcut: 'Ctrl+B',
|
||||
callback: (action, event) => this.executeTextAction('bold')
|
||||
},
|
||||
{
|
||||
id: 'italic',
|
||||
label: 'Italic',
|
||||
icon: 'fas fa-italic',
|
||||
shortcut: 'Ctrl+I',
|
||||
callback: (action, event) => this.executeTextAction('italic')
|
||||
},
|
||||
{
|
||||
id: 'underline',
|
||||
label: 'Underline',
|
||||
icon: 'fas fa-underline',
|
||||
shortcut: 'Ctrl+U',
|
||||
callback: (action, event) => this.executeTextAction('underline')
|
||||
}
|
||||
];
|
||||
|
||||
imageActions: ToolbarAction[] = [
|
||||
{
|
||||
id: 'resize',
|
||||
label: 'Resize',
|
||||
icon: 'fas fa-expand-arrows-alt',
|
||||
callback: (action, event) => this.logAction('Resize image')
|
||||
},
|
||||
{
|
||||
id: 'crop',
|
||||
label: 'Crop',
|
||||
icon: 'fas fa-crop',
|
||||
callback: (action, event) => this.logAction('Crop image')
|
||||
},
|
||||
{
|
||||
id: 'filters',
|
||||
label: 'Filters',
|
||||
icon: 'fas fa-filter',
|
||||
callback: (action, event) => this.logAction('Apply filters')
|
||||
},
|
||||
{
|
||||
id: 'divider2',
|
||||
label: '',
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
id: 'download',
|
||||
label: 'Download',
|
||||
icon: 'fas fa-download',
|
||||
callback: (action, event) => this.logAction('Download image')
|
||||
}
|
||||
];
|
||||
|
||||
customActions: ToolbarAction[] = [
|
||||
{
|
||||
id: 'action1',
|
||||
label: 'Action 1',
|
||||
icon: 'fas fa-star',
|
||||
tooltip: 'This is action 1',
|
||||
callback: (action, event) => this.logAction('Custom Action 1 executed')
|
||||
},
|
||||
{
|
||||
id: 'action2',
|
||||
label: 'Action 2',
|
||||
icon: 'fas fa-heart',
|
||||
tooltip: 'This is action 2 with a longer tooltip',
|
||||
disabled: false,
|
||||
callback: (action, event) => this.logAction('Custom Action 2 executed')
|
||||
},
|
||||
{
|
||||
id: 'divider3',
|
||||
label: '',
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
id: 'action3',
|
||||
label: 'Disabled Action',
|
||||
icon: 'fas fa-ban',
|
||||
disabled: true,
|
||||
tooltip: 'This action is disabled',
|
||||
callback: (action, event) => this.logAction('This should not execute')
|
||||
},
|
||||
{
|
||||
id: 'action4',
|
||||
label: 'Toggle Action',
|
||||
icon: 'fas fa-toggle-on',
|
||||
callback: (action, event) => this.toggleAction(action)
|
||||
}
|
||||
];
|
||||
|
||||
userActions: ToolbarAction[] = [
|
||||
{
|
||||
id: 'view',
|
||||
label: 'View Details',
|
||||
icon: 'fas fa-eye',
|
||||
callback: (action, event) => this.logAction('View user details')
|
||||
},
|
||||
{
|
||||
id: 'edit-user',
|
||||
label: 'Edit User',
|
||||
icon: 'fas fa-edit',
|
||||
callback: (action, event) => this.logAction('Edit user')
|
||||
},
|
||||
{
|
||||
id: 'delete-user',
|
||||
label: 'Delete User',
|
||||
icon: 'fas fa-trash',
|
||||
callback: (action, event) => this.logAction('Delete user')
|
||||
}
|
||||
];
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
// Setup text selection toolbar
|
||||
this.setupTextSelectionToolbar();
|
||||
}
|
||||
|
||||
showToolbar(id: string, triggerElement: HTMLElement, config: Partial<FloatingToolbarConfig>): void {
|
||||
console.log('Demo: showToolbar called', { id, config, basicActions: this.basicActions });
|
||||
this.currentConfig = {
|
||||
...this.currentConfig,
|
||||
...config,
|
||||
visible: true
|
||||
};
|
||||
this.currentActions = [...this.basicActions];
|
||||
console.log('Demo: config and actions set', {
|
||||
currentConfig: this.currentConfig,
|
||||
currentActions: this.currentActions
|
||||
});
|
||||
this.floatingToolbar.setAnchorElement(triggerElement);
|
||||
}
|
||||
|
||||
showAutoHideToolbar(triggerElement: HTMLElement): void {
|
||||
this.currentConfig = {
|
||||
...this.currentConfig,
|
||||
visible: true,
|
||||
autoHide: true,
|
||||
hideDelay: 3000,
|
||||
label: 'Auto-hide in 3 seconds'
|
||||
};
|
||||
this.currentActions = [...this.basicActions];
|
||||
this.floatingToolbar.setAnchorElement(triggerElement);
|
||||
}
|
||||
|
||||
showCustomActionsToolbar(triggerElement: HTMLElement): void {
|
||||
this.currentConfig = {
|
||||
...this.currentConfig,
|
||||
visible: true,
|
||||
autoHide: false,
|
||||
showShortcuts: true,
|
||||
label: 'Custom Actions'
|
||||
};
|
||||
this.currentActions = [...this.customActions];
|
||||
this.floatingToolbar.setAnchorElement(triggerElement);
|
||||
}
|
||||
|
||||
showContextualToolbar(event: MouseEvent, actions: ToolbarAction[], context: string): void {
|
||||
event.preventDefault();
|
||||
|
||||
// Create temporary anchor at mouse position
|
||||
const tempAnchor = {
|
||||
getBoundingClientRect: () => ({
|
||||
top: event.clientY,
|
||||
left: event.clientX,
|
||||
right: event.clientX,
|
||||
bottom: event.clientY,
|
||||
width: 0,
|
||||
height: 0
|
||||
} as DOMRect)
|
||||
} as HTMLElement;
|
||||
|
||||
this.currentConfig = {
|
||||
...this.currentConfig,
|
||||
visible: true,
|
||||
variant: 'contextual',
|
||||
position: 'bottom',
|
||||
autoHide: true,
|
||||
hideDelay: 5000,
|
||||
label: `${context} actions`
|
||||
};
|
||||
this.currentActions = [...actions];
|
||||
this.floatingToolbar.setAnchorElement(tempAnchor);
|
||||
}
|
||||
|
||||
showConfigurableToolbar(triggerElement: HTMLElement): void {
|
||||
this.currentConfig = {
|
||||
...this.currentConfig,
|
||||
...this.demoConfig,
|
||||
visible: true,
|
||||
label: 'Configured Toolbar'
|
||||
};
|
||||
this.currentActions = [...this.basicActions];
|
||||
this.floatingToolbar.setAnchorElement(triggerElement);
|
||||
}
|
||||
|
||||
updateDemoConfig(): void {
|
||||
// Update the demo configuration
|
||||
Object.assign(this.currentConfig, this.demoConfig);
|
||||
}
|
||||
|
||||
handleTextSelection(element: HTMLElement): void {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString().trim()) {
|
||||
const range = selection.getRangeAt(0);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
if (rect.width > 0 && rect.height > 0) {
|
||||
// Create anchor at selection
|
||||
const selectionAnchor = {
|
||||
getBoundingClientRect: () => rect
|
||||
} as HTMLElement;
|
||||
|
||||
this.textSelectionToolbar.setAnchorElement(selectionAnchor);
|
||||
this.textSelectionToolbar.show();
|
||||
}
|
||||
} else {
|
||||
this.textSelectionToolbar.hide();
|
||||
}
|
||||
}
|
||||
|
||||
selectUser(userId: number, rowElement: HTMLElement): void {
|
||||
this.selectedUserId = userId;
|
||||
}
|
||||
|
||||
showUserActions(event: Event, rowElement: HTMLElement, user: any): void {
|
||||
event.stopPropagation();
|
||||
|
||||
this.currentConfig = {
|
||||
...this.currentConfig,
|
||||
visible: true,
|
||||
variant: 'elevated',
|
||||
position: 'left',
|
||||
label: `Actions for ${user.name}`
|
||||
};
|
||||
this.currentActions = [...this.userActions];
|
||||
this.floatingToolbar.setAnchorElement(rowElement);
|
||||
}
|
||||
|
||||
onActionClicked(event: { action: ToolbarAction; event: Event }): void {
|
||||
this.logAction(`Action clicked: ${event.action.label}`);
|
||||
}
|
||||
|
||||
onTextActionClicked(event: { action: ToolbarAction; event: Event }): void {
|
||||
this.logAction(`Text action: ${event.action.label}`);
|
||||
}
|
||||
|
||||
onToolbarVisibilityChange(visible: boolean): void {
|
||||
this.currentConfig.visible = visible;
|
||||
}
|
||||
|
||||
onToolbarHidden(): void {
|
||||
this.logAction('Toolbar hidden');
|
||||
}
|
||||
|
||||
executeTextAction(command: string): void {
|
||||
try {
|
||||
document.execCommand(command, false);
|
||||
this.logAction(`Text formatting: ${command}`);
|
||||
} catch (error) {
|
||||
this.logAction(`Text formatting failed: ${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
toggleAction(action: ToolbarAction): void {
|
||||
const isToggled = action.icon === 'fas fa-toggle-on';
|
||||
action.icon = isToggled ? 'fas fa-toggle-off' : 'fas fa-toggle-on';
|
||||
this.logAction(`Toggle action: ${isToggled ? 'OFF' : 'ON'}`);
|
||||
}
|
||||
|
||||
private setupTextSelectionToolbar(): void {
|
||||
document.addEventListener('selectionchange', () => {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || !selection.toString().trim()) {
|
||||
this.textSelectionToolbar.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getConfigValue<K extends keyof FloatingToolbarConfig>(key: K): NonNullable<FloatingToolbarConfig[K]> {
|
||||
const defaultValues: Required<FloatingToolbarConfig> = {
|
||||
size: 'md',
|
||||
variant: 'default',
|
||||
position: 'top',
|
||||
autoPosition: true,
|
||||
visible: false,
|
||||
actions: [],
|
||||
context: undefined as any,
|
||||
trigger: 'manual',
|
||||
autoHide: false,
|
||||
hideDelay: 3000,
|
||||
showAnimation: 'slide-fade',
|
||||
backdrop: false,
|
||||
backdropClosable: true,
|
||||
escapeClosable: true,
|
||||
offset: 12,
|
||||
label: '',
|
||||
showLabel: false,
|
||||
showShortcuts: false
|
||||
};
|
||||
|
||||
const value = this.currentConfig[key];
|
||||
return (value !== undefined ? value : defaultValues[key]) as NonNullable<FloatingToolbarConfig[K]>;
|
||||
}
|
||||
|
||||
private logAction(message: string): void {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
this.actionLog.unshift(`[${timestamp}] ${message}`);
|
||||
|
||||
// Keep only the last 10 entries
|
||||
if (this.actionLog.length > 10) {
|
||||
this.actionLog = this.actionLog.slice(0, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<div class="demo-container">
|
||||
<h1>UI Font Manager Demo</h1>
|
||||
<p class="demo-description">
|
||||
Dynamically load Google Fonts and switch between font themes with smooth transitions.
|
||||
Select a font preset to apply it globally to the entire application.
|
||||
</p>
|
||||
|
||||
<!-- Font Preset Selection -->
|
||||
<section class="demo-section">
|
||||
<h2>Font Preset Selection</h2>
|
||||
<p class="preset-description">
|
||||
Choose from curated font combinations. The selected preset will be applied to the entire application.
|
||||
</p>
|
||||
|
||||
<div class="preset-controls">
|
||||
<div class="preset-selector">
|
||||
<label for="preset-select">Choose Font Preset:</label>
|
||||
<select
|
||||
id="preset-select"
|
||||
[value]="selectedPreset"
|
||||
(change)="onPresetChange($event)">
|
||||
<option value="">Select a preset...</option>
|
||||
<option *ngFor="let preset of fontPresets" [value]="preset.key">
|
||||
{{preset.name}} - {{preset.description}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="transition-selector" *ngIf="selectedPreset">
|
||||
<label for="transition-select">Transition Effect:</label>
|
||||
<select id="transition-select" [(ngModel)]="selectedTransition">
|
||||
<option value="fade">Fade</option>
|
||||
<option value="scale">Scale</option>
|
||||
<option value="typewriter">Typewriter</option>
|
||||
</select>
|
||||
<button
|
||||
class="apply-btn"
|
||||
(click)="applyPresetWithTransition()">
|
||||
Apply with {{selectedTransition | titlecase}} Effect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Preset Display -->
|
||||
<div class="current-preset" *ngIf="currentPreset">
|
||||
<h4>Currently Applied: {{currentPreset.name}}</h4>
|
||||
<div class="preset-fonts">
|
||||
<div class="font-assignment">
|
||||
<span class="font-type">Sans-serif:</span>
|
||||
<span class="font-name">{{currentPreset.fonts.sans}}</span>
|
||||
</div>
|
||||
<div class="font-assignment">
|
||||
<span class="font-type">Serif:</span>
|
||||
<span class="font-name">{{currentPreset.fonts.serif}}</span>
|
||||
</div>
|
||||
<div class="font-assignment">
|
||||
<span class="font-type">Monospace:</span>
|
||||
<span class="font-name">{{currentPreset.fonts.mono}}</span>
|
||||
</div>
|
||||
<div class="font-assignment">
|
||||
<span class="font-type">Display:</span>
|
||||
<span class="font-name">{{currentPreset.fonts.display}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Typography Preview -->
|
||||
<section class="demo-section">
|
||||
<h2>Typography Preview</h2>
|
||||
<p class="preview-description">See how the selected fonts look in a real application context.</p>
|
||||
|
||||
<div class="typography-showcase">
|
||||
<div class="type-sample">
|
||||
<h1 style="font-family: var(--font-family-display)">Display Heading (H1)</h1>
|
||||
<h2 style="font-family: var(--font-family-sans)">Sans-serif Heading (H2)</h2>
|
||||
<h3 style="font-family: var(--font-family-serif)">Serif Heading (H3)</h3>
|
||||
|
||||
<p style="font-family: var(--font-family-sans)">
|
||||
This is a paragraph using the sans-serif font. It demonstrates how readable body text
|
||||
appears with the currently selected font combination. Lorem ipsum dolor sit amet,
|
||||
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
</p>
|
||||
|
||||
<blockquote style="font-family: var(--font-family-serif)">
|
||||
"This is a quote using the serif font, which often provides better readability
|
||||
for longer text passages and adds elegance to quoted content."
|
||||
</blockquote>
|
||||
|
||||
<pre><code style="font-family: var(--font-family-mono)">// Code example with monospace font
|
||||
function applyFontTheme(theme) {
|
||||
document.documentElement.style.setProperty('--font-family-sans', theme.sans);
|
||||
document.documentElement.style.setProperty('--font-family-serif', theme.serif);
|
||||
document.documentElement.style.setProperty('--font-family-mono', theme.mono);
|
||||
}</code></pre>
|
||||
|
||||
<div class="font-info">
|
||||
Current fonts: Sans (var(--font-family-sans)) | Serif (var(--font-family-serif)) |
|
||||
Mono (var(--font-family-mono)) | Display (var(--font-family-display))
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Available Font Presets -->
|
||||
<section class="demo-section">
|
||||
<h2>Available Font Presets</h2>
|
||||
<div class="themes-grid">
|
||||
<div
|
||||
*ngFor="let preset of fontPresets"
|
||||
class="theme-card"
|
||||
[class.active]="preset.key === selectedPreset">
|
||||
|
||||
<h4>{{preset.name}}</h4>
|
||||
<p class="theme-description">{{preset.description}}</p>
|
||||
|
||||
<div class="theme-fonts">
|
||||
<div class="theme-font">
|
||||
<strong>Sans:</strong>
|
||||
<span>{{preset.fonts.sans}}</span>
|
||||
</div>
|
||||
<div class="theme-font">
|
||||
<strong>Serif:</strong>
|
||||
<span>{{preset.fonts.serif}}</span>
|
||||
</div>
|
||||
<div class="theme-font">
|
||||
<strong>Mono:</strong>
|
||||
<span>{{preset.fonts.mono}}</span>
|
||||
</div>
|
||||
<div class="theme-font">
|
||||
<strong>Display:</strong>
|
||||
<span>{{preset.fonts.display}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="apply-theme-btn"
|
||||
(click)="applyPreset(preset.key)">
|
||||
Apply {{preset.name}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Popular Individual Fonts -->
|
||||
<section class="demo-section">
|
||||
<h3>Popular Individual Fonts</h3>
|
||||
<p class="section-description">Load individual fonts to test and preview them.</p>
|
||||
|
||||
<div class="font-controls">
|
||||
<div class="font-grid">
|
||||
<button
|
||||
*ngFor="let font of popularFonts"
|
||||
class="font-button"
|
||||
[class.loaded]="isLoaded(font)"
|
||||
(click)="loadSingleFont(font)">
|
||||
|
||||
{{font}}
|
||||
<span class="load-status" *ngIf="isLoaded(font)">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Event Log -->
|
||||
<section class="demo-section" *ngIf="eventLog.length > 0">
|
||||
<h3>Event Log</h3>
|
||||
<div class="event-log">
|
||||
<div
|
||||
*ngFor="let event of eventLog.slice(-10)"
|
||||
class="event-item"
|
||||
[class]="event.type">
|
||||
<span class="event-time">{{event.time}}</span>
|
||||
<span class="event-message">{{event.message}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,580 @@
|
||||
.demo-container {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
font-size: 1.1rem;
|
||||
color: #666;
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin: 3rem 0;
|
||||
padding: 2rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
background: #fafafa;
|
||||
|
||||
h2, h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Loading State Display
|
||||
.state-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.state-item {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #2196F3;
|
||||
|
||||
strong {
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #FF9800;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.idle {
|
||||
color: #4CAF50;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// Manual Font Controls
|
||||
.font-controls {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.font-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.font-button {
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.9rem;
|
||||
|
||||
&:hover {
|
||||
border-color: #2196F3;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&.loaded {
|
||||
border-color: #4CAF50;
|
||||
background: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.load-status {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
color: #4CAF50;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// Transition Controls
|
||||
.transition-controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.transition-btn {
|
||||
padding: 1rem;
|
||||
border: 2px solid #2196F3;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
color: #2196F3;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.scale {
|
||||
border-color: #FF9800;
|
||||
color: #FF9800;
|
||||
|
||||
&:hover {
|
||||
background: #FF9800;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&.typewriter {
|
||||
border-color: #9C27B0;
|
||||
color: #9C27B0;
|
||||
|
||||
&:hover {
|
||||
background: #9C27B0;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Typography Showcase
|
||||
.typography-showcase {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.type-sample {
|
||||
padding: 2rem;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f5f5f5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
|
||||
code {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.font-info {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
// Available Themes
|
||||
.themes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
padding: 1.5rem;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #2196F3;
|
||||
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.1);
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #333;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-description {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.theme-fonts {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.theme-font {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.25rem 0;
|
||||
font-size: 0.85rem;
|
||||
|
||||
strong {
|
||||
color: #333;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
& + .theme-font {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.apply-theme-btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #2196F3;
|
||||
border-radius: 6px;
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background: #1976D2;
|
||||
border-color: #1976D2;
|
||||
}
|
||||
}
|
||||
|
||||
// Event Log
|
||||
.event-log {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.event-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
&.info {
|
||||
color: #2196F3;
|
||||
}
|
||||
}
|
||||
|
||||
.event-time {
|
||||
color: #666;
|
||||
font-size: 0.8rem;
|
||||
min-width: 80px;
|
||||
font-family: var(--font-family-mono, 'Courier New', monospace);
|
||||
}
|
||||
|
||||
.event-message {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
// Font Preset Controls
|
||||
.preset-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.preset-selector {
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
background: white;
|
||||
transition: border-color 0.3s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #2196F3;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.transition-selector {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: end;
|
||||
flex-wrap: wrap;
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.apply-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #1976D2;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Current Preset Display
|
||||
.current-preset {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: #e8f5e8;
|
||||
border: 1px solid #4CAF50;
|
||||
border-radius: 12px;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #2e7d32;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.preset-fonts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.font-assignment {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
|
||||
.font-type {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.font-name {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
// Theme card active state
|
||||
.theme-card.active {
|
||||
border-color: #4CAF50;
|
||||
background: #f1f8e9;
|
||||
|
||||
h4 {
|
||||
color: #2e7d32;
|
||||
}
|
||||
}
|
||||
|
||||
// Section descriptions
|
||||
.preset-description,
|
||||
.preview-description,
|
||||
.section-description {
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
// Font transition animations
|
||||
:global(html.font-transition-fade) {
|
||||
* {
|
||||
transition: font-family 0.8s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.font-transition-scale) {
|
||||
* {
|
||||
transition: font-family 0.6s ease-in-out, transform 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
body {
|
||||
transform: scale(1.02);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
&.font-transition-scale body {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.font-transition-typewriter) {
|
||||
* {
|
||||
transition: font-family 0.5s steps(10, end);
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced blockquote styling
|
||||
blockquote {
|
||||
margin: 1.5rem 0;
|
||||
padding: 1rem 1.5rem;
|
||||
border-left: 4px solid #2196F3;
|
||||
background: #f8f9fa;
|
||||
font-style: italic;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '"';
|
||||
font-size: 3rem;
|
||||
color: #2196F3;
|
||||
position: absolute;
|
||||
top: -0.5rem;
|
||||
left: 1rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.font-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
}
|
||||
|
||||
.transition-controls {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.themes-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.state-info {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.type-sample {
|
||||
padding: 1.5rem;
|
||||
|
||||
h1 { font-size: 2rem; }
|
||||
h2 { font-size: 1.75rem; }
|
||||
h3 { font-size: 1.5rem; }
|
||||
h4 { font-size: 1.25rem; }
|
||||
}
|
||||
|
||||
.preset-controls {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.transition-selector {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 1rem;
|
||||
|
||||
select,
|
||||
.apply-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.preset-fonts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
// Mock font combinations since ui-font-manager is not yet available
|
||||
const FONT_COMBINATIONS = {
|
||||
'Modern Clean': {
|
||||
sans: 'Inter',
|
||||
serif: 'Playfair Display',
|
||||
mono: 'JetBrains Mono',
|
||||
display: 'Inter'
|
||||
},
|
||||
'Classic Editorial': {
|
||||
sans: 'Source Sans Pro',
|
||||
serif: 'Lora',
|
||||
mono: 'Source Code Pro',
|
||||
display: 'Source Sans Pro'
|
||||
},
|
||||
'Friendly Modern': {
|
||||
sans: 'Poppins',
|
||||
serif: 'Merriweather',
|
||||
mono: 'Fira Code',
|
||||
display: 'Poppins'
|
||||
},
|
||||
'Professional': {
|
||||
sans: 'Roboto',
|
||||
serif: 'EB Garamond',
|
||||
mono: 'Roboto Mono',
|
||||
display: 'Roboto'
|
||||
}
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-font-manager-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h1>UI Font Manager Demo</h1>
|
||||
<p class="demo-description">
|
||||
Dynamically load Google Fonts and switch between font themes with smooth transitions.
|
||||
Select a font preset to apply it globally to the entire application.
|
||||
</p>
|
||||
|
||||
<!-- Font Preset Selection -->
|
||||
<section class="demo-section">
|
||||
<h2>Font Preset Selection</h2>
|
||||
<p class="preset-description">
|
||||
Choose from curated font combinations. The selected preset will be applied to the entire application.
|
||||
</p>
|
||||
|
||||
<div class="preset-controls">
|
||||
<div class="preset-selector">
|
||||
<label for="preset-select">Choose Font Preset:</label>
|
||||
<select
|
||||
id="preset-select"
|
||||
[value]="selectedPreset"
|
||||
(change)="onPresetChange($event)">
|
||||
<option value="">Select a preset...</option>
|
||||
<option *ngFor="let preset of fontPresets" [value]="preset.key">
|
||||
{{preset.name}} - {{preset.description}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="transition-selector" *ngIf="selectedPreset">
|
||||
<label for="transition-select">Transition Effect:</label>
|
||||
<select id="transition-select" [(ngModel)]="selectedTransition">
|
||||
<option value="fade">Fade</option>
|
||||
<option value="scale">Scale</option>
|
||||
<option value="typewriter">Typewriter</option>
|
||||
</select>
|
||||
<button
|
||||
class="apply-btn"
|
||||
(click)="applyPresetWithTransition()">
|
||||
Apply with {{selectedTransition | titlecase}} Effect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Preset Display -->
|
||||
<div class="current-preset" *ngIf="currentPreset">
|
||||
<h4>Currently Applied: {{currentPreset.name}}</h4>
|
||||
<div class="preset-fonts">
|
||||
<div class="font-assignment">
|
||||
<span class="font-type">Sans-serif:</span>
|
||||
<span class="font-name">{{currentPreset.fonts.sans}}</span>
|
||||
</div>
|
||||
<div class="font-assignment">
|
||||
<span class="font-type">Serif:</span>
|
||||
<span class="font-name">{{currentPreset.fonts.serif}}</span>
|
||||
</div>
|
||||
<div class="font-assignment">
|
||||
<span class="font-type">Monospace:</span>
|
||||
<span class="font-name">{{currentPreset.fonts.mono}}</span>
|
||||
</div>
|
||||
<div class="font-assignment">
|
||||
<span class="font-type">Display:</span>
|
||||
<span class="font-name">{{currentPreset.fonts.display}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Typography Preview -->
|
||||
<section class="demo-section">
|
||||
<h2>Typography Preview</h2>
|
||||
<p class="preview-description">See how the selected fonts look in a real application context.</p>
|
||||
|
||||
<div class="typography-showcase">
|
||||
<div class="type-sample">
|
||||
<h1 style="font-family: var(--font-family-display)">Display Heading (H1)</h1>
|
||||
<h2 style="font-family: var(--font-family-sans)">Sans-serif Heading (H2)</h2>
|
||||
<h3 style="font-family: var(--font-family-serif)">Serif Heading (H3)</h3>
|
||||
|
||||
<p style="font-family: var(--font-family-sans)">
|
||||
This is a paragraph using the sans-serif font. It demonstrates how readable body text
|
||||
appears with the currently selected font combination. Lorem ipsum dolor sit amet,
|
||||
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
</p>
|
||||
|
||||
<blockquote style="font-family: var(--font-family-serif)">
|
||||
"This is a quote using the serif font, which often provides better readability
|
||||
for longer text passages and adds elegance to quoted content."
|
||||
</blockquote>
|
||||
|
||||
<div class="font-info">
|
||||
Current fonts: Sans (var(--font-family-sans)) | Serif (var(--font-family-serif)) |
|
||||
Mono (var(--font-family-mono)) | Display (var(--font-family-display))
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Available Font Presets -->
|
||||
<section class="demo-section">
|
||||
<h2>Available Font Presets</h2>
|
||||
<div class="themes-grid">
|
||||
<div
|
||||
*ngFor="let preset of fontPresets"
|
||||
class="theme-card"
|
||||
[class.active]="preset.key === selectedPreset">
|
||||
|
||||
<h4>{{preset.name}}</h4>
|
||||
<p class="theme-description">{{preset.description}}</p>
|
||||
|
||||
<div class="theme-fonts">
|
||||
<div class="theme-font">
|
||||
<strong>Sans:</strong>
|
||||
<span>{{preset.fonts.sans}}</span>
|
||||
</div>
|
||||
<div class="theme-font">
|
||||
<strong>Serif:</strong>
|
||||
<span>{{preset.fonts.serif}}</span>
|
||||
</div>
|
||||
<div class="theme-font">
|
||||
<strong>Mono:</strong>
|
||||
<span>{{preset.fonts.mono}}</span>
|
||||
</div>
|
||||
<div class="theme-font">
|
||||
<strong>Display:</strong>
|
||||
<span>{{preset.fonts.display}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="apply-theme-btn"
|
||||
(click)="applyPreset(preset.key)">
|
||||
Apply {{preset.name}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Popular Individual Fonts -->
|
||||
<section class="demo-section">
|
||||
<h3>Popular Individual Fonts</h3>
|
||||
<p class="section-description">Load individual fonts to test and preview them.</p>
|
||||
|
||||
<div class="font-controls">
|
||||
<div class="font-grid">
|
||||
<button
|
||||
*ngFor="let font of popularFonts"
|
||||
class="font-button"
|
||||
[class.loaded]="isLoaded(font)"
|
||||
(click)="loadSingleFont(font)">
|
||||
|
||||
{{font}}
|
||||
<span class="load-status" *ngIf="isLoaded(font)">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Event Log -->
|
||||
<section class="demo-section" *ngIf="eventLog.length > 0">
|
||||
<h3>Event Log</h3>
|
||||
<div class="event-log">
|
||||
<div
|
||||
*ngFor="let event of eventLog.slice(-10)"
|
||||
class="event-item"
|
||||
[class]="event.type">
|
||||
<span class="event-time">{{event.time}}</span>
|
||||
<span class="event-message">{{event.message}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './font-manager-demo.component.scss'
|
||||
})
|
||||
export class FontManagerDemoComponent implements OnInit, OnDestroy {
|
||||
popularFonts = ['Inter', 'Roboto', 'Playfair Display', 'JetBrains Mono', 'Montserrat', 'Lora'];
|
||||
loadedFonts = new Set<string>();
|
||||
|
||||
// Font preset management
|
||||
fontPresets = [
|
||||
{
|
||||
key: 'modern-clean',
|
||||
name: 'Modern Clean',
|
||||
description: 'Contemporary and minimal design with Inter and Playfair Display',
|
||||
fonts: FONT_COMBINATIONS['Modern Clean']
|
||||
},
|
||||
{
|
||||
key: 'classic-editorial',
|
||||
name: 'Classic Editorial',
|
||||
description: 'Traditional publishing style with Source Sans Pro and Lora',
|
||||
fonts: FONT_COMBINATIONS['Classic Editorial']
|
||||
},
|
||||
{
|
||||
key: 'friendly-modern',
|
||||
name: 'Friendly Modern',
|
||||
description: 'Approachable and warm with Poppins and Merriweather',
|
||||
fonts: FONT_COMBINATIONS['Friendly Modern']
|
||||
},
|
||||
{
|
||||
key: 'professional',
|
||||
name: 'Professional',
|
||||
description: 'Corporate and reliable with Roboto and EB Garamond',
|
||||
fonts: FONT_COMBINATIONS['Professional']
|
||||
},
|
||||
{
|
||||
key: 'system',
|
||||
name: 'System Default',
|
||||
description: 'Use system fonts for optimal performance',
|
||||
fonts: {
|
||||
sans: 'system-ui',
|
||||
serif: 'ui-serif',
|
||||
mono: 'ui-monospace',
|
||||
display: 'system-ui'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
selectedPreset: string = '';
|
||||
selectedTransition: 'fade' | 'scale' | 'typewriter' = 'fade';
|
||||
currentPreset: any = null;
|
||||
|
||||
eventLog: Array<{time: string, message: string, type: 'success' | 'error' | 'info'}> = [];
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.logEvent('Component initialized', 'info');
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
onThemeChanged(themeName: string): void {
|
||||
this.logEvent(`Theme changed to: ${themeName}`, 'success');
|
||||
}
|
||||
|
||||
onFontLoaded(fontName: string): void {
|
||||
this.logEvent(`Font loaded: ${fontName}`, 'success');
|
||||
this.loadedFonts.add(fontName);
|
||||
}
|
||||
|
||||
onFontError(event: {fontName: string, error: string}): void {
|
||||
this.logEvent(`Font error - ${event.fontName}: ${event.error}`, 'error');
|
||||
}
|
||||
|
||||
loadSingleFont(fontName: string): void {
|
||||
this.logEvent(`Loading font: ${fontName}`, 'info');
|
||||
|
||||
// Load font via Google Fonts API
|
||||
this.loadGoogleFont(fontName);
|
||||
this.loadedFonts.add(fontName);
|
||||
this.logEvent(`Successfully loaded: ${fontName}`, 'success');
|
||||
}
|
||||
|
||||
applyTheme(themeName: string): void {
|
||||
this.logEvent(`Applying theme: ${themeName}`, 'info');
|
||||
this.logEvent(`Successfully applied theme: ${themeName}`, 'success');
|
||||
}
|
||||
|
||||
isLoaded(fontName: string): boolean {
|
||||
return this.loadedFonts.has(fontName);
|
||||
}
|
||||
|
||||
private loadGoogleFont(fontName: string): void {
|
||||
const link = document.createElement('link');
|
||||
link.href = `https://fonts.googleapis.com/css2?family=${fontName.replace(' ', '+')}&display=swap`;
|
||||
link.rel = 'stylesheet';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
onPresetChange(event: Event): void {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
this.selectedPreset = target.value;
|
||||
|
||||
if (this.selectedPreset) {
|
||||
const preset = this.fontPresets.find(p => p.key === this.selectedPreset);
|
||||
if (preset) {
|
||||
this.logEvent(`Selected preset: ${preset.name}`, 'info');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyPreset(presetKey: string): void {
|
||||
const preset = this.fontPresets.find(p => p.key === presetKey);
|
||||
if (!preset) {
|
||||
this.logEvent(`Preset not found: ${presetKey}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedPreset = presetKey;
|
||||
this.logEvent(`Applying preset: ${preset.name}`, 'info');
|
||||
|
||||
// Apply fonts to document root for global effect
|
||||
this.applyFontsGlobally(preset.fonts);
|
||||
this.currentPreset = preset;
|
||||
}
|
||||
|
||||
applyPresetWithTransition(): void {
|
||||
const preset = this.fontPresets.find(p => p.key === this.selectedPreset);
|
||||
if (!preset) return;
|
||||
|
||||
this.logEvent(`Applying preset "${preset.name}" with ${this.selectedTransition} transition`, 'info');
|
||||
|
||||
// Apply fonts with transition effect
|
||||
const fontVariables = {
|
||||
'--font-family-sans': this.buildGlobalFontStack(preset.fonts.sans),
|
||||
'--font-family-serif': this.buildGlobalFontStack(preset.fonts.serif),
|
||||
'--font-family-mono': this.buildGlobalFontStack(preset.fonts.mono),
|
||||
'--font-family-display': this.buildGlobalFontStack(preset.fonts.display)
|
||||
};
|
||||
|
||||
this.applyFontsWithTransition(fontVariables, this.selectedTransition);
|
||||
this.currentPreset = preset;
|
||||
}
|
||||
|
||||
private applyFontsGlobally(fonts: any): void {
|
||||
const root = document.documentElement;
|
||||
|
||||
// Set CSS custom properties on the document root
|
||||
root.style.setProperty('--font-family-sans', this.buildGlobalFontStack(fonts.sans));
|
||||
root.style.setProperty('--font-family-serif', this.buildGlobalFontStack(fonts.serif));
|
||||
root.style.setProperty('--font-family-mono', this.buildGlobalFontStack(fonts.mono));
|
||||
root.style.setProperty('--font-family-display', this.buildGlobalFontStack(fonts.display));
|
||||
|
||||
// Also load the fonts if they're Google Fonts
|
||||
if (fonts.sans && fonts.sans !== 'system-ui') {
|
||||
this.loadSingleFont(fonts.sans);
|
||||
}
|
||||
if (fonts.serif && fonts.serif !== 'ui-serif') {
|
||||
this.loadSingleFont(fonts.serif);
|
||||
}
|
||||
if (fonts.mono && fonts.mono !== 'ui-monospace') {
|
||||
this.loadSingleFont(fonts.mono);
|
||||
}
|
||||
if (fonts.display && fonts.display !== 'system-ui') {
|
||||
this.loadSingleFont(fonts.display);
|
||||
}
|
||||
|
||||
this.logEvent(`Applied fonts globally to document root`, 'success');
|
||||
}
|
||||
|
||||
private applyFontsWithTransition(fontVariables: Record<string, string>, transitionType: 'fade' | 'scale' | 'typewriter'): void {
|
||||
const root = document.documentElement;
|
||||
|
||||
// Add transition class
|
||||
root.classList.add(`font-transition-${transitionType}`);
|
||||
|
||||
// Apply font changes
|
||||
Object.entries(fontVariables).forEach(([property, value]) => {
|
||||
root.style.setProperty(property, value);
|
||||
});
|
||||
|
||||
// Remove transition class after animation
|
||||
setTimeout(() => {
|
||||
root.classList.remove(`font-transition-${transitionType}`);
|
||||
this.logEvent(`${transitionType} transition completed`, 'success');
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private buildGlobalFontStack(fontName: string): string {
|
||||
// Handle system fonts
|
||||
if (fontName === 'system-ui') {
|
||||
return 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
||||
}
|
||||
if (fontName === 'ui-serif') {
|
||||
return 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif';
|
||||
}
|
||||
if (fontName === 'ui-monospace') {
|
||||
return 'ui-monospace, SFMono-Regular, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
||||
}
|
||||
|
||||
// For Google Fonts, add appropriate fallbacks
|
||||
const fontMap: Record<string, string> = {
|
||||
'Inter': `"Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`,
|
||||
'Roboto': `"Roboto", Arial, Helvetica, sans-serif`,
|
||||
'Poppins': `"Poppins", Arial, Helvetica, sans-serif`,
|
||||
'Source Sans Pro': `"Source Sans Pro", Arial, Helvetica, sans-serif`,
|
||||
'Montserrat': `"Montserrat", Arial, Helvetica, sans-serif`,
|
||||
|
||||
'Playfair Display': `"Playfair Display", Georgia, "Times New Roman", serif`,
|
||||
'Lora': `"Lora", Georgia, "Times New Roman", serif`,
|
||||
'Merriweather': `"Merriweather", Georgia, "Times New Roman", serif`,
|
||||
'EB Garamond': `"EB Garamond", Georgia, "Times New Roman", serif`,
|
||||
|
||||
'JetBrains Mono': `"JetBrains Mono", Consolas, Monaco, "Courier New", monospace`,
|
||||
'Source Code Pro': `"Source Code Pro", Consolas, Monaco, "Courier New", monospace`,
|
||||
'Fira Code': `"Fira Code", Consolas, Monaco, "Courier New", monospace`,
|
||||
'Roboto Mono': `"Roboto Mono", Consolas, Monaco, "Courier New", monospace`,
|
||||
|
||||
'Oswald': `"Oswald", Impact, "Arial Black", sans-serif`,
|
||||
'Raleway': `"Raleway", Arial, Helvetica, sans-serif`
|
||||
};
|
||||
|
||||
return fontMap[fontName] || `"${fontName}", sans-serif`;
|
||||
}
|
||||
|
||||
private logEvent(message: string, type: 'success' | 'error' | 'info'): void {
|
||||
const time = new Date().toLocaleTimeString();
|
||||
this.eventLog.push({ time, message, type });
|
||||
|
||||
// Keep only last 50 events
|
||||
if (this.eventLog.length > 50) {
|
||||
this.eventLog = this.eventLog.slice(-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './font-manager-demo.component';
|
||||
@@ -0,0 +1,522 @@
|
||||
@import "../../../../../ui-design-system/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/button.component';
|
||||
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 '../../../../../ui-design-system/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/form-field.component';
|
||||
|
||||
@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/src/lib/components/forms/form-field/form-field.component';
|
||||
|
||||
// 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,274 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-xl;
|
||||
|
||||
h3 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
padding-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
p {
|
||||
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);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
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;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
button {
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $semantic-opacity-disabled;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selection-info {
|
||||
margin-top: $semantic-spacing-component-lg;
|
||||
padding: $semantic-spacing-component-md;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
p {
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
padding: $semantic-spacing-content-line-tight 0;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
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);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.event-log {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-radius-md;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
overflow: hidden;
|
||||
|
||||
.event-controls {
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
background: $semantic-color-surface-secondary;
|
||||
|
||||
button {
|
||||
padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-radius-sm;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
font-family: map-get($semantic-typography-button-small, font-family);
|
||||
font-size: map-get($semantic-typography-button-small, font-size);
|
||||
font-weight: map-get($semantic-typography-button-small, font-weight);
|
||||
line-height: map-get($semantic-typography-button-small, line-height);
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
box-shadow: $semantic-shadow-button-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.event-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
|
||||
.no-events {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
text-align: center;
|
||||
color: $semantic-color-text-tertiary;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.event-item {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr auto;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.event-type {
|
||||
font-weight: $semantic-typography-font-weight-semibold;
|
||||
color: $semantic-color-primary;
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
}
|
||||
|
||||
.event-details {
|
||||
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);
|
||||
}
|
||||
|
||||
.event-time {
|
||||
color: $semantic-color-text-tertiary;
|
||||
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);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.event-item {
|
||||
grid-template-columns: 1fr;
|
||||
gap: $semantic-spacing-content-line-tight;
|
||||
|
||||
.event-time {
|
||||
justify-self: end;
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.event-type {
|
||||
grid-row: 2;
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.event-details {
|
||||
grid-row: 3;
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { GalleryGridComponent, GalleryGridItem } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-gallery-grid-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, GalleryGridComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>Gallery Grid Demo</h2>
|
||||
<p>A responsive media grid with lightbox functionality for organizing and displaying images.</p>
|
||||
|
||||
<!-- Column Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Column Variants</h3>
|
||||
<div class="demo-grid">
|
||||
@for (columnCount of columnVariants; track columnCount) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ columnCount }} Columns</h4>
|
||||
<ui-gallery-grid
|
||||
[items]="sampleItems.slice(0, 6)"
|
||||
[columns]="columnCount">
|
||||
</ui-gallery-grid>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gap Size Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Gap Sizes</h3>
|
||||
<div class="demo-grid">
|
||||
@for (gap of gapSizes; track gap) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ gap | titlecase }} Gap</h4>
|
||||
<ui-gallery-grid
|
||||
[items]="sampleItems.slice(0, 6)"
|
||||
[columns]="3"
|
||||
[gap]="gap">
|
||||
</ui-gallery-grid>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Object Fit Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Object Fit Options</h3>
|
||||
<div class="demo-grid">
|
||||
@for (fit of objectFitOptions; track fit) {
|
||||
<div class="demo-item">
|
||||
<h4>{{ fit | titlecase }} Fit</h4>
|
||||
<ui-gallery-grid
|
||||
[items]="landscapeItems"
|
||||
[columns]="3"
|
||||
[objectFit]="fit">
|
||||
</ui-gallery-grid>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Auto-fit Responsive -->
|
||||
<section class="demo-section">
|
||||
<h3>Auto-fit Responsive</h3>
|
||||
<p>Automatically adjusts columns based on available space (resize window to see effect):</p>
|
||||
<ui-gallery-grid
|
||||
[items]="sampleItems"
|
||||
columns="auto-fit"
|
||||
[gap]="'md'">
|
||||
</ui-gallery-grid>
|
||||
</section>
|
||||
|
||||
<!-- Masonry Layout -->
|
||||
<section class="demo-section">
|
||||
<h3>Masonry Layout</h3>
|
||||
<p>Items align to top with varying heights:</p>
|
||||
<ui-gallery-grid
|
||||
[items]="masonryItems"
|
||||
[columns]="4"
|
||||
[masonry]="true"
|
||||
[gap]="'sm'">
|
||||
</ui-gallery-grid>
|
||||
</section>
|
||||
|
||||
<!-- Interactive Features -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Features</h3>
|
||||
|
||||
<div class="demo-controls">
|
||||
<button (click)="toggleOverlay()" [class.active]="showOverlay">
|
||||
{{ showOverlay ? 'Hide' : 'Show' }} Overlay
|
||||
</button>
|
||||
<button (click)="toggleZoomIndicator()" [class.active]="showZoomIndicator">
|
||||
{{ showZoomIndicator ? 'Hide' : 'Show' }} Zoom Indicator
|
||||
</button>
|
||||
<button (click)="toggleSelection()" [class.active]="selectionEnabled">
|
||||
{{ selectionEnabled ? 'Disable' : 'Enable' }} Selection
|
||||
</button>
|
||||
<button (click)="selectAll()" [disabled]="!selectionEnabled">
|
||||
Select All
|
||||
</button>
|
||||
<button (click)="deselectAll()" [disabled]="!selectionEnabled">
|
||||
Deselect All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ui-gallery-grid
|
||||
[items]="interactiveItems"
|
||||
[columns]="4"
|
||||
[showOverlay]="showOverlay"
|
||||
[showZoomIndicator]="showZoomIndicator"
|
||||
(itemClick)="handleItemClick($event)"
|
||||
(itemSelect)="handleItemSelect($event)"
|
||||
(imageLoad)="handleImageLoad($event)"
|
||||
(imageError)="handleImageError($event)">
|
||||
</ui-gallery-grid>
|
||||
|
||||
@if (selectedItems.length > 0) {
|
||||
<div class="selection-info">
|
||||
<p>Selected Items: {{ selectedItems.length }}</p>
|
||||
<ul>
|
||||
@for (item of selectedItems; track item.id) {
|
||||
<li>{{ item.title || item.alt || 'Item ' + item.id }}</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Empty State -->
|
||||
<section class="demo-section">
|
||||
<h3>Empty State</h3>
|
||||
<ui-gallery-grid
|
||||
[items]="[]"
|
||||
[columns]="3"
|
||||
emptyText="No photos in this album yet">
|
||||
</ui-gallery-grid>
|
||||
</section>
|
||||
|
||||
<!-- Loading State -->
|
||||
<section class="demo-section">
|
||||
<h3>Loading State</h3>
|
||||
<ui-gallery-grid
|
||||
[items]="loadingItems"
|
||||
[columns]="3">
|
||||
</ui-gallery-grid>
|
||||
</section>
|
||||
|
||||
<!-- Event Log -->
|
||||
<section class="demo-section">
|
||||
<h3>Event Log</h3>
|
||||
<div class="event-log">
|
||||
<div class="event-controls">
|
||||
<button (click)="clearEventLog()">Clear Log</button>
|
||||
</div>
|
||||
<div class="event-list">
|
||||
@if (eventLog.length === 0) {
|
||||
<p class="no-events">No events yet. Interact with the gallery above to see events.</p>
|
||||
} @else {
|
||||
@for (event of eventLog; track event.id) {
|
||||
<div class="event-item">
|
||||
<span class="event-type">{{ event.type }}</span>
|
||||
<span class="event-details">{{ event.details }}</span>
|
||||
<span class="event-time">{{ event.timestamp | date:'HH:mm:ss' }}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './gallery-grid-demo.component.scss'
|
||||
})
|
||||
export class GalleryGridDemoComponent {
|
||||
columnVariants = [2, 3, 4] as const;
|
||||
gapSizes = ['sm', 'md', 'lg'] as const;
|
||||
objectFitOptions = ['cover', 'contain', 'fill'] as const;
|
||||
|
||||
showOverlay = true;
|
||||
showZoomIndicator = true;
|
||||
selectionEnabled = false;
|
||||
selectedItems: GalleryGridItem[] = [];
|
||||
eventLog: Array<{id: number; type: string; details: string; timestamp: Date}> = [];
|
||||
|
||||
sampleItems: GalleryGridItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
src: 'https://picsum.photos/400/300?random=1',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=1',
|
||||
alt: 'Sample image 1',
|
||||
title: 'Mountain Landscape',
|
||||
caption: 'Beautiful mountain vista at sunset'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
src: 'https://picsum.photos/400/300?random=2',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=2',
|
||||
alt: 'Sample image 2',
|
||||
title: 'Ocean Waves',
|
||||
caption: 'Crashing waves on a sandy beach'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
src: 'https://picsum.photos/400/300?random=3',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=3',
|
||||
alt: 'Sample image 3',
|
||||
title: 'Forest Path',
|
||||
caption: 'Winding trail through dense woods'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
src: 'https://picsum.photos/400/300?random=4',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=4',
|
||||
alt: 'Sample image 4',
|
||||
title: 'City Skyline',
|
||||
caption: 'Urban architecture at night'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
src: 'https://picsum.photos/400/300?random=5',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=5',
|
||||
alt: 'Sample image 5',
|
||||
title: 'Desert Dunes',
|
||||
caption: 'Rolling sand dunes in morning light'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
src: 'https://picsum.photos/400/300?random=6',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=6',
|
||||
alt: 'Sample image 6',
|
||||
title: 'Lake Reflection',
|
||||
caption: 'Mirror-like water surface'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
src: 'https://picsum.photos/400/300?random=7',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=7',
|
||||
alt: 'Sample image 7',
|
||||
title: 'Flower Field',
|
||||
caption: 'Colorful wildflowers in bloom'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
src: 'https://picsum.photos/400/300?random=8',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=8',
|
||||
alt: 'Sample image 8',
|
||||
title: 'Snow Peaks',
|
||||
caption: 'Snow-capped mountain range'
|
||||
}
|
||||
];
|
||||
|
||||
landscapeItems: GalleryGridItem[] = [
|
||||
{
|
||||
id: 'l1',
|
||||
src: 'https://picsum.photos/800/400?random=11',
|
||||
thumbnail: 'https://picsum.photos/200/100?random=11',
|
||||
alt: 'Landscape image 1',
|
||||
title: 'Wide Landscape',
|
||||
caption: 'Panoramic view'
|
||||
},
|
||||
{
|
||||
id: 'l2',
|
||||
src: 'https://picsum.photos/800/400?random=12',
|
||||
thumbnail: 'https://picsum.photos/200/100?random=12',
|
||||
alt: 'Landscape image 2',
|
||||
title: 'River Valley',
|
||||
caption: 'Flowing water through hills'
|
||||
},
|
||||
{
|
||||
id: 'l3',
|
||||
src: 'https://picsum.photos/800/400?random=13',
|
||||
thumbnail: 'https://picsum.photos/200/100?random=13',
|
||||
alt: 'Landscape image 3',
|
||||
title: 'Coastal View',
|
||||
caption: 'Rocky coastline'
|
||||
}
|
||||
];
|
||||
|
||||
masonryItems: GalleryGridItem[] = [
|
||||
{
|
||||
id: 'm1',
|
||||
src: 'https://picsum.photos/300/400?random=21',
|
||||
thumbnail: 'https://picsum.photos/150/200?random=21',
|
||||
alt: 'Tall image 1',
|
||||
title: 'Portrait 1'
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
src: 'https://picsum.photos/300/200?random=22',
|
||||
thumbnail: 'https://picsum.photos/150/100?random=22',
|
||||
alt: 'Wide image 1',
|
||||
title: 'Landscape 1'
|
||||
},
|
||||
{
|
||||
id: 'm3',
|
||||
src: 'https://picsum.photos/300/500?random=23',
|
||||
thumbnail: 'https://picsum.photos/150/250?random=23',
|
||||
alt: 'Very tall image',
|
||||
title: 'Portrait 2'
|
||||
},
|
||||
{
|
||||
id: 'm4',
|
||||
src: 'https://picsum.photos/300/300?random=24',
|
||||
thumbnail: 'https://picsum.photos/150/150?random=24',
|
||||
alt: 'Square image',
|
||||
title: 'Square 1'
|
||||
},
|
||||
{
|
||||
id: 'm5',
|
||||
src: 'https://picsum.photos/300/350?random=25',
|
||||
thumbnail: 'https://picsum.photos/150/175?random=25',
|
||||
alt: 'Tall image 2',
|
||||
title: 'Portrait 3'
|
||||
},
|
||||
{
|
||||
id: 'm6',
|
||||
src: 'https://picsum.photos/300/250?random=26',
|
||||
thumbnail: 'https://picsum.photos/150/125?random=26',
|
||||
alt: 'Medium image',
|
||||
title: 'Landscape 2'
|
||||
}
|
||||
];
|
||||
|
||||
interactiveItems: GalleryGridItem[] = this.sampleItems.slice(0, 8).map(item => ({
|
||||
...item,
|
||||
selected: false
|
||||
}));
|
||||
|
||||
loadingItems: GalleryGridItem[] = [
|
||||
{
|
||||
id: 'loading1',
|
||||
src: 'https://picsum.photos/400/300?random=31',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=31',
|
||||
alt: 'Loading image 1',
|
||||
title: 'Loading Item 1',
|
||||
loading: true
|
||||
},
|
||||
{
|
||||
id: 'loading2',
|
||||
src: 'https://picsum.photos/400/300?random=32',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=32',
|
||||
alt: 'Loading image 2',
|
||||
title: 'Loading Item 2',
|
||||
loading: true
|
||||
},
|
||||
{
|
||||
id: 'loading3',
|
||||
src: 'https://picsum.photos/400/300?random=33',
|
||||
thumbnail: 'https://picsum.photos/200/150?random=33',
|
||||
alt: 'Loading image 3',
|
||||
title: 'Loaded Item',
|
||||
loading: false
|
||||
}
|
||||
];
|
||||
|
||||
private eventCounter = 0;
|
||||
|
||||
toggleOverlay(): void {
|
||||
this.showOverlay = !this.showOverlay;
|
||||
}
|
||||
|
||||
toggleZoomIndicator(): void {
|
||||
this.showZoomIndicator = !this.showZoomIndicator;
|
||||
}
|
||||
|
||||
toggleSelection(): void {
|
||||
this.selectionEnabled = !this.selectionEnabled;
|
||||
if (!this.selectionEnabled) {
|
||||
this.deselectAll();
|
||||
}
|
||||
}
|
||||
|
||||
selectAll(): void {
|
||||
this.interactiveItems.forEach(item => item.selected = true);
|
||||
this.updateSelectedItems();
|
||||
}
|
||||
|
||||
deselectAll(): void {
|
||||
this.interactiveItems.forEach(item => item.selected = false);
|
||||
this.updateSelectedItems();
|
||||
}
|
||||
|
||||
handleItemClick(event: { item: GalleryGridItem; event: MouseEvent }): void {
|
||||
this.addEvent('Item Click', `Clicked item: ${event.item.title || event.item.id}`);
|
||||
|
||||
if (this.selectionEnabled) {
|
||||
event.item.selected = !event.item.selected;
|
||||
this.updateSelectedItems();
|
||||
this.addEvent('Item Selection', `${event.item.selected ? 'Selected' : 'Deselected'} item: ${event.item.title || event.item.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
handleItemSelect(item: GalleryGridItem): void {
|
||||
this.updateSelectedItems();
|
||||
this.addEvent('Item Select', `Selected item: ${item.title || item.id}`);
|
||||
}
|
||||
|
||||
handleImageLoad(event: { item: GalleryGridItem; event: Event }): void {
|
||||
this.addEvent('Image Load', `Loaded image: ${event.item.title || event.item.id}`);
|
||||
}
|
||||
|
||||
handleImageError(event: { item: GalleryGridItem; event: Event }): void {
|
||||
this.addEvent('Image Error', `Failed to load image: ${event.item.title || event.item.id}`);
|
||||
}
|
||||
|
||||
clearEventLog(): void {
|
||||
this.eventLog = [];
|
||||
}
|
||||
|
||||
private updateSelectedItems(): void {
|
||||
this.selectedItems = this.interactiveItems.filter(item => item.selected);
|
||||
}
|
||||
|
||||
private addEvent(type: string, details: string): void {
|
||||
this.eventLog.unshift({
|
||||
id: ++this.eventCounter,
|
||||
type,
|
||||
details,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
// Keep only the last 20 events
|
||||
if (this.eventLog.length > 20) {
|
||||
this.eventLog = this.eventLog.slice(0, 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-lg;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-xl;
|
||||
|
||||
h3 {
|
||||
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);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-example {
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
|
||||
h4 {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
}
|
||||
|
||||
.demo-grid-item {
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
|
||||
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);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
min-height: 80px;
|
||||
position: relative;
|
||||
|
||||
small {
|
||||
position: absolute;
|
||||
bottom: $semantic-spacing-component-xs;
|
||||
right: $semantic-spacing-component-xs;
|
||||
font-size: $semantic-typography-font-size-xs;
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: $semantic-color-secondary;
|
||||
color: $semantic-color-on-secondary;
|
||||
border-color: $semantic-color-secondary;
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: $semantic-color-success;
|
||||
color: $semantic-color-on-success;
|
||||
border-color: $semantic-color-success;
|
||||
}
|
||||
|
||||
&--info {
|
||||
background: $semantic-color-info;
|
||||
color: $semantic-color-on-info;
|
||||
border-color: $semantic-color-info;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background: $semantic-color-warning;
|
||||
color: $semantic-color-on-warning;
|
||||
border-color: $semantic-color-warning;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: $semantic-color-danger;
|
||||
color: $semantic-color-on-danger;
|
||||
border-color: $semantic-color-danger;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
.demo-control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
|
||||
label {
|
||||
font-family: map-get($semantic-typography-label, font-family);
|
||||
font-size: map-get($semantic-typography-label, font-size);
|
||||
font-weight: map-get($semantic-typography-label, font-weight);
|
||||
line-height: map-get($semantic-typography-label, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
select,
|
||||
input[type="checkbox"] {
|
||||
padding: $semantic-spacing-interactive-input-padding-y $semantic-spacing-interactive-input-padding-x;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-primary;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
font-family: map-get($semantic-typography-input, font-family);
|
||||
font-size: map-get($semantic-typography-input, font-size);
|
||||
font-weight: map-get($semantic-typography-input, font-weight);
|
||||
line-height: map-get($semantic-typography-input, line-height);
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
border-color: $semantic-color-border-focus;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
height: $semantic-sizing-touch-minimum;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.demo-grid-item {
|
||||
min-height: 60px;
|
||||
padding: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { GridContainerComponent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-grid-container-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, GridContainerComponent],
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<h2>GridContainer Demo</h2>
|
||||
<p class="demo-description">
|
||||
Advanced CSS Grid wrapper with template areas support, responsive column/row definitions, and auto-placement algorithms.
|
||||
</p>
|
||||
|
||||
<!-- Basic Grid Examples -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Grid Layouts</h3>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Auto-fit Grid (Responsive)</h4>
|
||||
<ui-grid-container columns="auto-fit" gap="md">
|
||||
@for (item of basicItems; track item.id) {
|
||||
<div class="demo-grid-item demo-grid-item--primary">{{ item.label }}</div>
|
||||
}
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Fixed 3-Column Grid</h4>
|
||||
<ui-grid-container [columns]="3" gap="lg">
|
||||
@for (item of basicItems; track item.id) {
|
||||
<div class="demo-grid-item demo-grid-item--secondary">{{ item.label }}</div>
|
||||
}
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gap Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Gap Sizes</h3>
|
||||
<div class="demo-row">
|
||||
@for (gap of gaps; track gap) {
|
||||
<div class="demo-example">
|
||||
<h4>{{ gap }} Gap</h4>
|
||||
<ui-grid-container [columns]="3" [gap]="gap">
|
||||
@for (item of shortItems; track item.id) {
|
||||
<div class="demo-grid-item demo-grid-item--success">{{ item.label }}</div>
|
||||
}
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Column Variants -->
|
||||
<section class="demo-section">
|
||||
<h3>Column Layouts</h3>
|
||||
@for (colCount of columnCounts; track colCount) {
|
||||
<div class="demo-example">
|
||||
<h4>{{ colCount }} Columns</h4>
|
||||
<ui-grid-container [columns]="colCount" gap="md">
|
||||
@for (item of getItemsForColumns(colCount); track item.id) {
|
||||
<div class="demo-grid-item demo-grid-item--info">{{ item.label }}</div>
|
||||
}
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Auto Grid Types -->
|
||||
<section class="demo-section">
|
||||
<h3>Auto Grid Types</h3>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Auto-fit (Items stretch to fill space)</h4>
|
||||
<ui-grid-container columns="auto-fit" gap="md">
|
||||
@for (item of autoItems; track item.id) {
|
||||
<div class="demo-grid-item demo-grid-item--warning">{{ item.label }}</div>
|
||||
}
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Auto-fill (Empty columns maintained)</h4>
|
||||
<ui-grid-container columns="auto-fill" gap="md">
|
||||
@for (item of autoItems; track item.id) {
|
||||
<div class="demo-grid-item demo-grid-item--danger">{{ item.label }}</div>
|
||||
}
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Advanced Features -->
|
||||
<section class="demo-section">
|
||||
<h3>Advanced Features</h3>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Dense Grid (Auto-placement fills gaps)</h4>
|
||||
<ui-grid-container [columns]="4" gap="md" [dense]="true">
|
||||
<div class="demo-grid-item demo-grid-item--primary ui-grid-item--span-2">Wide Item (2 cols)</div>
|
||||
<div class="demo-grid-item demo-grid-item--secondary">Item 1</div>
|
||||
<div class="demo-grid-item demo-grid-item--success ui-grid-item--row-span-2">Tall Item</div>
|
||||
<div class="demo-grid-item demo-grid-item--info">Item 2</div>
|
||||
<div class="demo-grid-item demo-grid-item--warning">Item 3</div>
|
||||
<div class="demo-grid-item demo-grid-item--danger ui-grid-item--span-3">Extra Wide (3 cols)</div>
|
||||
<div class="demo-grid-item demo-grid-item--primary">Item 4</div>
|
||||
<div class="demo-grid-item demo-grid-item--secondary">Item 5</div>
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Grid with Template Areas</h4>
|
||||
<ui-grid-container
|
||||
customColumns="1fr 2fr 1fr"
|
||||
customRows="auto auto 1fr auto"
|
||||
templateAreas="'header header header' 'sidebar main aside' 'sidebar main aside' 'footer footer footer'"
|
||||
gap="md"
|
||||
padding="lg">
|
||||
<div class="demo-grid-item demo-grid-item--primary" style="grid-area: header;">Header</div>
|
||||
<div class="demo-grid-item demo-grid-item--secondary" style="grid-area: sidebar;">Sidebar</div>
|
||||
<div class="demo-grid-item demo-grid-item--info" style="grid-area: main;">Main Content</div>
|
||||
<div class="demo-grid-item demo-grid-item--success" style="grid-area: aside;">Aside</div>
|
||||
<div class="demo-grid-item demo-grid-item--warning" style="grid-area: footer;">Footer</div>
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Alignment Options -->
|
||||
<section class="demo-section">
|
||||
<h3>Alignment Options</h3>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Justify Content: Center</h4>
|
||||
<ui-grid-container [columns]="2" gap="md" justifyContent="center" style="height: 200px;">
|
||||
<div class="demo-grid-item demo-grid-item--primary">Item 1</div>
|
||||
<div class="demo-grid-item demo-grid-item--secondary">Item 2</div>
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
|
||||
<div class="demo-example">
|
||||
<h4>Align Items: Center</h4>
|
||||
<ui-grid-container [columns]="3" gap="md" alignItems="center" style="height: 150px;">
|
||||
<div class="demo-grid-item demo-grid-item--success">Centered</div>
|
||||
<div class="demo-grid-item demo-grid-item--info">Items</div>
|
||||
<div class="demo-grid-item demo-grid-item--warning">Vertically</div>
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Row Modes -->
|
||||
<section class="demo-section">
|
||||
<h3>Row Modes</h3>
|
||||
@for (rowMode of rowModes; track rowMode) {
|
||||
<div class="demo-example">
|
||||
<h4>{{ rowMode }} Rows</h4>
|
||||
<ui-grid-container [columns]="3" gap="md" [rowMode]="rowMode" style="height: 200px;">
|
||||
<div class="demo-grid-item demo-grid-item--primary">
|
||||
@if (rowMode === 'min-content') {
|
||||
Short
|
||||
} @else if (rowMode === 'max-content') {
|
||||
This is longer content to demonstrate max-content behavior
|
||||
} @else {
|
||||
Content for {{ rowMode }}
|
||||
}
|
||||
</div>
|
||||
<div class="demo-grid-item demo-grid-item--secondary">Item 2</div>
|
||||
<div class="demo-grid-item demo-grid-item--success">Item 3</div>
|
||||
</ui-grid-container>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Interactive Example -->
|
||||
<section class="demo-section">
|
||||
<h3>Interactive Grid Builder</h3>
|
||||
|
||||
<div class="demo-controls">
|
||||
<div class="demo-control-group">
|
||||
<label>Columns:</label>
|
||||
<select [(ngModel)]="interactiveConfig.columns">
|
||||
@for (col of allColumns; track col) {
|
||||
<option [value]="col">{{ col }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="demo-control-group">
|
||||
<label>Gap:</label>
|
||||
<select [(ngModel)]="interactiveConfig.gap">
|
||||
@for (gap of gaps; track gap) {
|
||||
<option [value]="gap">{{ gap }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="demo-control-group">
|
||||
<label>Dense:</label>
|
||||
<input type="checkbox" [(ngModel)]="interactiveConfig.dense">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ui-grid-container
|
||||
[columns]="interactiveConfig.columns"
|
||||
[gap]="interactiveConfig.gap"
|
||||
[dense]="interactiveConfig.dense"
|
||||
style="border: 2px dashed #ccc; min-height: 200px;">
|
||||
@for (item of interactiveItems; track item.id) {
|
||||
<div
|
||||
class="demo-grid-item"
|
||||
[class]="'demo-grid-item--' + item.variant"
|
||||
[class.ui-grid-item--span-2]="item.span === 2"
|
||||
[class.ui-grid-item--span-3]="item.span === 3"
|
||||
[class.ui-grid-item--row-span-2]="item.rowSpan === 2">
|
||||
{{ item.label }}
|
||||
@if (item.span > 1) {
|
||||
<small>({{ item.span }} cols)</small>
|
||||
}
|
||||
@if (item.rowSpan > 1) {
|
||||
<small>({{ item.rowSpan }} rows)</small>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</ui-grid-container>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './grid-container-demo.component.scss'
|
||||
})
|
||||
export class GridContainerDemoComponent {
|
||||
gaps = ['sm', 'md', 'lg'] as const;
|
||||
columnCounts = [1, 2, 3, 4, 5, 6] as const;
|
||||
allColumns = [1, 2, 3, 4, 5, 6, 'auto-fit', 'auto-fill'] as const;
|
||||
rowModes = ['auto', 'equal', 'min-content', 'max-content'] as const;
|
||||
|
||||
basicItems = [
|
||||
{ id: 1, label: 'Item 1' },
|
||||
{ id: 2, label: 'Item 2' },
|
||||
{ id: 3, label: 'Item 3' },
|
||||
{ id: 4, label: 'Item 4' },
|
||||
{ id: 5, label: 'Item 5' },
|
||||
{ id: 6, label: 'Item 6' },
|
||||
{ id: 7, label: 'Item 7' },
|
||||
{ id: 8, label: 'Item 8' }
|
||||
];
|
||||
|
||||
shortItems = [
|
||||
{ id: 1, label: 'A' },
|
||||
{ id: 2, label: 'B' },
|
||||
{ id: 3, label: 'C' }
|
||||
];
|
||||
|
||||
autoItems = [
|
||||
{ id: 1, label: 'Auto Item 1' },
|
||||
{ id: 2, label: 'Auto Item 2' },
|
||||
{ id: 3, label: 'Auto Item 3' },
|
||||
{ id: 4, label: 'Auto Item 4' }
|
||||
];
|
||||
|
||||
interactiveConfig = {
|
||||
columns: 'auto-fit' as const,
|
||||
gap: 'md' as const,
|
||||
dense: false
|
||||
};
|
||||
|
||||
interactiveItems = [
|
||||
{ id: 1, label: 'Regular', variant: 'primary', span: 1, rowSpan: 1 },
|
||||
{ id: 2, label: 'Wide', variant: 'secondary', span: 2, rowSpan: 1 },
|
||||
{ id: 3, label: 'Tall', variant: 'success', span: 1, rowSpan: 2 },
|
||||
{ id: 4, label: 'Extra Wide', variant: 'info', span: 3, rowSpan: 1 },
|
||||
{ id: 5, label: 'Normal', variant: 'warning', span: 1, rowSpan: 1 },
|
||||
{ id: 6, label: 'Regular', variant: 'danger', span: 1, rowSpan: 1 },
|
||||
{ id: 7, label: 'Wide', variant: 'primary', span: 2, rowSpan: 1 },
|
||||
{ id: 8, label: 'Normal', variant: 'secondary', span: 1, rowSpan: 1 }
|
||||
];
|
||||
|
||||
getItemsForColumns(colCount: number) {
|
||||
const count = Math.min(colCount * 2, this.basicItems.length);
|
||||
return this.basicItems.slice(0, count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
@use '../../../../../ui-design-system/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/grid-system/grid-system.component';
|
||||
|
||||
@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 @@
|
||||
<p>hcl-studio-demo works!</p>
|
||||
@@ -0,0 +1,478 @@
|
||||
/**
|
||||
* HCL Studio Demo Styles
|
||||
* Showcases dynamic theming using CSS custom properties
|
||||
*/
|
||||
|
||||
.hcl-studio-demo {
|
||||
padding: 2rem;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-on-surface);
|
||||
min-height: 100vh;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// HEADER STYLES
|
||||
// ==========================================================================
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding: 2rem;
|
||||
background: var(--color-surface-container);
|
||||
border-radius: 1rem;
|
||||
|
||||
h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--color-on-surface-variant);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// CURRENT THEME INFO
|
||||
// ==========================================================================
|
||||
|
||||
.current-theme-info {
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: var(--color-surface-high);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--color-outline-variant);
|
||||
|
||||
h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--color-on-surface);
|
||||
}
|
||||
|
||||
.color-swatches {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
.swatch {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
|
||||
span {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// MODE CONTROLS
|
||||
// ==========================================================================
|
||||
|
||||
.mode-controls {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
|
||||
.mode-toggle-btn {
|
||||
padding: 0.75rem 2rem;
|
||||
border: 2px solid var(--color-outline);
|
||||
border-radius: 2rem;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-on-surface);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-surface-high);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-on-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// THEMES SECTION
|
||||
// ==========================================================================
|
||||
|
||||
.themes-section {
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: var(--color-on-surface);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.theme-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
|
||||
.theme-card {
|
||||
padding: 1rem;
|
||||
background: var(--color-surface-container);
|
||||
border: 2px solid var(--color-outline-variant);
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-container);
|
||||
|
||||
.theme-name {
|
||||
color: var(--color-on-primary-container);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-preview {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
.color-strip {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-name {
|
||||
display: block;
|
||||
text-align: center;
|
||||
color: var(--color-on-surface);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// CUSTOM THEME SECTION
|
||||
// ==========================================================================
|
||||
|
||||
.custom-theme-section {
|
||||
margin-bottom: 3rem;
|
||||
padding: 2rem;
|
||||
background: var(--color-surface-high);
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--color-outline-variant);
|
||||
|
||||
h3 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: var(--color-on-surface);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.custom-inputs {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.color-input-group {
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-on-surface);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
width: 50px;
|
||||
height: 40px;
|
||||
border: 2px solid var(--color-outline);
|
||||
border-radius: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid var(--color-outline);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-on-surface);
|
||||
font-size: 1rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.preview-btn {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-on-secondary);
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
&.apply-btn {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-on-primary);
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--color-on-surface);
|
||||
}
|
||||
|
||||
.preview-swatches {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 0.5rem;
|
||||
|
||||
.preview-swatch {
|
||||
padding: 0.75rem 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// DEMO COMPONENTS
|
||||
// ==========================================================================
|
||||
|
||||
.demo-components {
|
||||
h3 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: var(--color-on-surface);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.component-showcase {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
}
|
||||
|
||||
.showcase-section {
|
||||
padding: 1.5rem;
|
||||
background: var(--color-surface-container);
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid var(--color-outline-variant);
|
||||
|
||||
h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--color-on-surface);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
// Button Styles
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
|
||||
button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-on-primary);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-on-secondary);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-tertiary {
|
||||
background: var(--color-tertiary);
|
||||
color: var(--color-on-tertiary);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Card Styles
|
||||
.card-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.demo-card {
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
&.primary {
|
||||
background: var(--color-primary-container);
|
||||
color: var(--color-on-primary-container);
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: var(--color-secondary-container);
|
||||
color: var(--color-on-secondary-container);
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Surface Styles
|
||||
.surface-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.demo-surface {
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
|
||||
&.surface-low {
|
||||
background: var(--color-surface-low);
|
||||
color: var(--color-on-surface);
|
||||
}
|
||||
|
||||
&.surface-container {
|
||||
background: var(--color-surface-container);
|
||||
color: var(--color-on-surface);
|
||||
}
|
||||
|
||||
&.surface-high {
|
||||
background: var(--color-surface-high);
|
||||
color: var(--color-on-surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// RESPONSIVE ADJUSTMENTS
|
||||
// ==========================================================================
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hcl-studio-demo {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
padding: 1.5rem;
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
}
|
||||
|
||||
.custom-inputs {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
|
||||
.custom-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// TRANSITIONS FOR SMOOTH THEME CHANGES
|
||||
// ==========================================================================
|
||||
|
||||
* {
|
||||
transition: background-color 0.3s ease,
|
||||
color 0.3s ease,
|
||||
border-color 0.3s ease,
|
||||
opacity 0.3s ease;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HclStudioDemoComponent } from './hcl-studio-demo.component';
|
||||
|
||||
describe('HclStudioDemoComponent', () => {
|
||||
let component: HclStudioDemoComponent;
|
||||
let fixture: ComponentFixture<HclStudioDemoComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [HclStudioDemoComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HclStudioDemoComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,288 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
HCLStudioService,
|
||||
DEFAULT_THEMES,
|
||||
ThemePreview,
|
||||
BrandColors,
|
||||
HCLConverter,
|
||||
PaletteGenerator
|
||||
} from 'hcl-studio';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hcl-studio-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
template: `
|
||||
<div class="hcl-studio-demo">
|
||||
<div class="demo-header">
|
||||
<h1>HCL Studio Demo</h1>
|
||||
<p>Dynamic theme switching using HCL color space</p>
|
||||
</div>
|
||||
|
||||
<!-- Current Theme Info -->
|
||||
<div class="current-theme-info" *ngIf="currentTheme">
|
||||
<h3>Current Theme: {{ currentTheme.name }}</h3>
|
||||
<div class="color-swatches">
|
||||
<div class="swatch" [style.background]="currentTheme.primary">
|
||||
<span>Primary</span>
|
||||
</div>
|
||||
<div class="swatch" [style.background]="currentTheme.secondary">
|
||||
<span>Secondary</span>
|
||||
</div>
|
||||
<div class="swatch" [style.background]="currentTheme.tertiary">
|
||||
<span>Tertiary</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode Toggle -->
|
||||
<div class="mode-controls">
|
||||
<button
|
||||
class="mode-toggle-btn"
|
||||
(click)="toggleMode()"
|
||||
[class.dark]="isDarkMode"
|
||||
>
|
||||
{{ isDarkMode ? 'Switch to Light' : 'Switch to Dark' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Built-in Themes -->
|
||||
<div class="themes-section">
|
||||
<h3>Built-in Themes</h3>
|
||||
<div class="theme-grid">
|
||||
<div
|
||||
*ngFor="let theme of availableThemes"
|
||||
class="theme-card"
|
||||
[class.active]="theme.id === currentTheme?.id"
|
||||
(click)="switchTheme(theme.id)"
|
||||
>
|
||||
<div class="theme-preview">
|
||||
<div class="color-strip" [style.background]="theme.primary"></div>
|
||||
<div class="color-strip" [style.background]="theme.secondary"></div>
|
||||
<div class="color-strip" [style.background]="theme.tertiary"></div>
|
||||
</div>
|
||||
<span class="theme-name">{{ theme.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Color Input -->
|
||||
<div class="custom-theme-section">
|
||||
<h3>Create Custom Theme</h3>
|
||||
<div class="custom-inputs">
|
||||
<div class="color-input-group">
|
||||
<label>Primary Color:</label>
|
||||
<input
|
||||
type="color"
|
||||
[(ngModel)]="customColors.primary"
|
||||
(change)="previewCustomTheme()"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="customColors.primary"
|
||||
(input)="previewCustomTheme()"
|
||||
placeholder="#6750A4"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="color-input-group">
|
||||
<label>Secondary Color:</label>
|
||||
<input
|
||||
type="color"
|
||||
[(ngModel)]="customColors.secondary"
|
||||
(change)="previewCustomTheme()"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="customColors.secondary"
|
||||
(input)="previewCustomTheme()"
|
||||
placeholder="#625B71"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="color-input-group">
|
||||
<label>Tertiary Color:</label>
|
||||
<input
|
||||
type="color"
|
||||
[(ngModel)]="customColors.tertiary"
|
||||
(change)="previewCustomTheme()"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="customColors.tertiary"
|
||||
(input)="previewCustomTheme()"
|
||||
placeholder="#7D5260"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="custom-actions">
|
||||
<button class="preview-btn" (click)="previewCustomTheme()">
|
||||
Preview Theme
|
||||
</button>
|
||||
<button class="apply-btn" (click)="applyCustomTheme()">
|
||||
Apply Theme
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Color Preview -->
|
||||
<div *ngIf="colorPreview" class="color-preview">
|
||||
<h4>Generated Palette Preview</h4>
|
||||
<div class="preview-swatches">
|
||||
<div
|
||||
*ngFor="let color of colorPreview | keyvalue"
|
||||
class="preview-swatch"
|
||||
[style.background]="color.value"
|
||||
[style.color]="getContrastColor(color.value)"
|
||||
>
|
||||
{{ color.key }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Demo Components -->
|
||||
<div class="demo-components">
|
||||
<h3>Theme Demo Components</h3>
|
||||
|
||||
<div class="component-showcase">
|
||||
<!-- Buttons -->
|
||||
<div class="showcase-section">
|
||||
<h4>Buttons</h4>
|
||||
<div class="button-group">
|
||||
<button class="btn-primary">Primary Button</button>
|
||||
<button class="btn-secondary">Secondary Button</button>
|
||||
<button class="btn-tertiary">Tertiary Button</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cards -->
|
||||
<div class="showcase-section">
|
||||
<h4>Cards</h4>
|
||||
<div class="card-group">
|
||||
<div class="demo-card primary">
|
||||
<h5>Primary Card</h5>
|
||||
<p>This card uses primary colors</p>
|
||||
</div>
|
||||
<div class="demo-card secondary">
|
||||
<h5>Secondary Card</h5>
|
||||
<p>This card uses secondary colors</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Surfaces -->
|
||||
<div class="showcase-section">
|
||||
<h4>Surface Variations</h4>
|
||||
<div class="surface-group">
|
||||
<div class="demo-surface surface-low">Surface Low</div>
|
||||
<div class="demo-surface surface-container">Surface Container</div>
|
||||
<div class="demo-surface surface-high">Surface High</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./hcl-studio-demo.component.scss']
|
||||
})
|
||||
export class HclStudioDemoComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
// State
|
||||
availableThemes: ThemePreview[] = [];
|
||||
currentTheme: ThemePreview | null = null;
|
||||
isDarkMode = false;
|
||||
|
||||
// Custom theme inputs
|
||||
customColors: BrandColors = {
|
||||
primary: '#6750A4',
|
||||
secondary: '#625B71',
|
||||
tertiary: '#7D5260'
|
||||
};
|
||||
|
||||
colorPreview: { [key: string]: string } | null = null;
|
||||
|
||||
constructor(private hclStudio: HCLStudioService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Initialize HCL Studio with default themes
|
||||
this.hclStudio.initialize({
|
||||
themes: DEFAULT_THEMES,
|
||||
defaultTheme: 'material-purple',
|
||||
autoMode: false
|
||||
});
|
||||
|
||||
// Subscribe to theme changes
|
||||
this.hclStudio.themeState$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(state => {
|
||||
this.availableThemes = state.availableThemes;
|
||||
if (state.currentTheme) {
|
||||
this.currentTheme = {
|
||||
id: state.currentTheme.config.id,
|
||||
name: state.currentTheme.config.name,
|
||||
description: state.currentTheme.config.description,
|
||||
primary: state.currentTheme.config.colors.primary,
|
||||
secondary: state.currentTheme.config.colors.secondary,
|
||||
tertiary: state.currentTheme.config.colors.tertiary
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to mode changes
|
||||
this.hclStudio.currentMode$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(mode => {
|
||||
this.isDarkMode = mode === 'dark';
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
switchTheme(themeId: string): void {
|
||||
this.hclStudio.switchTheme(themeId);
|
||||
}
|
||||
|
||||
toggleMode(): void {
|
||||
this.hclStudio.toggleMode();
|
||||
}
|
||||
|
||||
previewCustomTheme(): void {
|
||||
try {
|
||||
this.colorPreview = this.hclStudio.generateColorPreview(this.customColors);
|
||||
} catch (error) {
|
||||
console.warn('Failed to generate color preview:', error);
|
||||
this.colorPreview = null;
|
||||
}
|
||||
}
|
||||
|
||||
applyCustomTheme(): void {
|
||||
const customTheme = this.hclStudio.createCustomTheme(
|
||||
'custom-theme',
|
||||
'My Custom Theme',
|
||||
this.customColors,
|
||||
'User-created custom theme'
|
||||
);
|
||||
|
||||
this.hclStudio.switchTheme('custom-theme');
|
||||
}
|
||||
|
||||
getContrastColor(backgroundColor: string): string {
|
||||
try {
|
||||
const ratio1 = HCLConverter.getContrastRatio(backgroundColor, '#000000');
|
||||
const ratio2 = HCLConverter.getContrastRatio(backgroundColor, '#FFFFFF');
|
||||
return ratio1 > ratio2 ? '#000000' : '#FFFFFF';
|
||||
} catch {
|
||||
return '#000000';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Demo-specific styles for the icon button showcase
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Danger button styling for destructive actions demo
|
||||
::ng-deep .danger-button {
|
||||
&.ui-icon-button--outlined {
|
||||
color: #dc3545;
|
||||
border-color: #dc3545;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: rgba(220, 53, 69, 0.1);
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline-color: #dc3545;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user